// ============================================================ // four_player_chess.ino — 4-Player Chess (5 ESP-NOW boards) // // Cross (+) formation: // [P1] top — White pieces // [P2] [CENTER] [P3] // left right — Green / Blue // [P4] bottom — Red pieces // // Global coordinate space (24 × 24, + shaped): // P1 arm : rows 0– 7, cols 8–15 // P2 arm : rows 8–15, cols 0– 7 // CENTER : rows 8–15, cols 8–15 // P3 arm : rows 8–15, cols 16–23 // P4 arm : rows 16–23, cols 8–15 // // Role assignment — automatic by MAC sort (ascending): // index 0 = P1 index 1 = P2 index 2 = P3 // index 3 = P4 index 4 = CENTER // // All player boards: pieces always start on local rows 0–1 // (back rank row 0, pawns row 1). Local row 7 faces center. // Center board shows colored edges throughout the game: // top = P1 (White) left = P2 (Green) // right = P3 (Blue) bottom = P4 (Red) // // No castling / no en passant. Pawns auto-promote to queen. // Player eliminated when their king is captured. // IO9 = reset. Any board reboot triggers auto-reconnect. // ============================================================ #include #include #include #include // ── Hardware ───────────────────────────────────────────────── #define LED_PIN 1 #define LED_COUNT 64 #define BRIGHTNESS 100 #define SER_PIN 45 #define RCLK_PIN 47 #define SRCLK_PIN 46 #define RESET_PIN 9 const int ROW_PINS[8] = {42, 41, 40, 39, 38, 37, 36, 35}; Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_RGBW + NEO_KHZ800); // ── Piece encoding ──────────────────────────────────────────── // uint8_t cell = (player << 4) | type, 0 = empty // player: 1=P1 2=P2 3=P3 4=P4 type: 1-6 below #define PAWN 1 #define KNIGHT 2 #define BISHOP 3 #define ROOK 4 #define QUEEN 5 #define KING 6 inline uint8_t mkPiece(int pl, int ty) { return (uint8_t)((pl<<4)|ty); } inline int plOf(uint8_t p) { return p >> 4; } inline int tyOf(uint8_t p) { return p & 0xF; } inline bool isEm(uint8_t p) { return p == 0; } // ── Roles ───────────────────────────────────────────────────── // 1=P1(top/W) 2=P2(left/G) 3=P3(right/B) 4=P4(bot/R) 5=CENTER uint8_t myRole = 0; // ── Coordinate mapping (supports board rotation per role) ───── // // Every player board uses the same physical orientation: // local row 0 = back rank (far from center) // local row 7 = near-center edge // local col 0-7 = left-to-right from the player's perspective // // Global ← local: // P1: gr=lr, gc=lc+8 // P2: gr=lc+8, gc=lr (board rotated 90° CW in global space) // P3: gr=lc+8, gc=23-lr (board rotated 90° CCW) // P4: gr=23-lr, gc=lc+8 (board flipped, near-center = global row 16) // CENTER: gr=lr+8, gc=lc+8 int gRow(int lr, int lc) { switch(myRole) { case 1: return lr; case 2: return lc + 8; case 3: return lc + 8; case 4: return 23 - lr; case 5: return lr + 8; } return -1; } int gCol(int lr, int lc) { switch(myRole) { case 1: return lc + 8; case 2: return lr; case 3: return 23 - lr; case 4: return lc + 8; case 5: return lc + 8; } return -1; } // Local ← global (inverse of above) int locR(int gr, int gc) { switch(myRole) { case 1: return gr; case 2: return gc; case 3: return 23 - gc; case 4: return 23 - gr; case 5: return gr - 8; } return -1; } int locC(int gr, int gc) { switch(myRole) { case 1: return gc - 8; case 2: return gr - 8; case 3: return gr - 8; case 4: return gc - 8; case 5: return gc - 8; } return -1; } bool onMe(int gr, int gc) { int lr=locR(gr,gc), lc=locC(gr,gc); return lr>=0&&lr<8&&lc>=0&&lc<8; } bool validSq(int r, int c) { if (r<0||r>=24||c<0||c>=24) return false; return (r>=8&&r<=15)||(c>=8&&c<=15); } // Which board owns this global square? (0 = invalid) int squareRole(int gr, int gc) { if (!validSq(gr,gc)) return 0; if (gr <= 7) return 1; // P1 arm if (gc <= 7) return 2; // P2 arm if (gc >= 16) return 3; // P3 arm if (gr >= 16) return 4; // P4 arm return 5; // CENTER } // Pawn movement direction (dr, dc) in GLOBAL coords, indexed by player 1-4 const int PDR[5] = {0, 1, 0, 0, -1}; const int PDC[5] = {0, 0, 1, -1, 0}; // ── Global game state ───────────────────────────────────────── uint8_t gBoard[24][24]; bool sensor[8][8], sensorPrev[8][8]; bool eliminated[5] = {}; void setGLED(int gr, int gc, uint32_t c) { if (onMe(gr,gc)) strip.setPixelColor(locR(gr,gc)*8+locC(gr,gc), c); } uint32_t pColor(int pl) { switch(pl) { case 1: return strip.Color(0,0,0,200); // White case 2: return strip.Color(0,200,0,0); // Green case 3: return strip.Color(0,0,200,0); // Blue case 4: return strip.Color(200,0,0,0); // Red } return 0; } uint32_t dimColor(int pl) { switch(pl) { case 1: return strip.Color(0,0,0,50); case 2: return strip.Color(0,50,0,0); case 3: return strip.Color(0,0,50,0); case 4: return strip.Color(50,0,0,0); } return 0; } // ── ESP-NOW message protocol ────────────────────────────────── enum MsgType : uint8_t { MSG_HELLO=0, MSG_SETUP_DONE, // my pieces are placed / CENTER is ready MSG_LIFT, // d[0]=gr, d[1]=gc MSG_HIGHLIGHT, // d[0]=gr, d[1]=gc, d[2]=type(0=empty,1=capture) MSG_CLEAR, // clear highlights MSG_PLACED, // d[0]=gr, d[1]=gc MSG_CANCEL, // cross-board lift cancelled MSG_MOVE, // d[0]=fromGR, d[1]=fromGC, d[2]=toGR, d[3]=toGC MSG_ELIM, // d[0]=eliminated player # MSG_RESET }; struct __attribute__((packed)) Message { MsgType type; uint8_t srcRole; uint8_t dstRole; uint8_t d[4]; }; uint8_t broadcastAddr[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; uint8_t roleMac[6][6]; bool roleKnown[6] = {}; uint8_t myMac[6]; // Single-slot message queue for game-play messages volatile bool msgPending = false; volatile MsgType msgType; volatile uint8_t msgSrcRole, msgDstRole, msgData[4]; uint8_t msgSenderMac[6]; // Sync counter updated directly in onRecv — never lost even during blocking ops volatile uint8_t gSetupSet = 0; // bit i = role i sent SETUP_DONE // Set by onRecv when a known peer sends MSG_HELLO during gameplay (they rebooted) volatile bool reconnectNeeded = false; // Highlights received on passive board struct HLSq { uint8_t gr, gc, type; }; HLSq hlList[64]; int hlCount = 0; bool isHL(int gr, int gc) { for (int i=0; i=0;i--) { digitalWrite(SRCLK_PIN, LOW); digitalWrite(SER_PIN, (i==col)?HIGH:LOW); digitalWrite(SRCLK_PIN, HIGH); } digitalWrite(RCLK_PIN, HIGH); } void clearCols() { digitalWrite(RCLK_PIN, LOW); for (int i=0;i<8;i++) { digitalWrite(SRCLK_PIN,LOW); digitalWrite(SER_PIN,LOW); digitalWrite(SRCLK_PIN,HIGH); } digitalWrite(RCLK_PIN, HIGH); } void readSensors() { for (int c=0;c<8;c++) { setColumn(c); delayMicroseconds(10); for(int r=0;r<8;r++) sensor[r][c]=(digitalRead(ROW_PINS[r])==LOW); } clearCols(); } // ── Animations ──────────────────────────────────────────────── void searchAnim(int frame) { strip.clear(); int d=frame%15; for(int r=0;r<8;r++) for(int c=0;c<8;c++) if((r+c)==d||(r+c)==d-1) strip.setPixelColor(r*8+c,strip.Color(0,0,120,0)); strip.show(); } void centerEdges() { for(int c=0;c<8;c++) strip.setPixelColor(0*8+c, pColor(1)); for(int c=0;c<8;c++) strip.setPixelColor(7*8+c, pColor(4)); for(int r=0;r<8;r++) strip.setPixelColor(r*8+0, pColor(2)); for(int r=0;r<8;r++) strip.setPixelColor(r*8+7, pColor(3)); strip.show(); } // Non-blocking role flash: processes 10 ms ticks so onRecv still runs void roleFlash(int role) { for (int f=0; f<4; f++) { strip.clear(); if (role==5) { centerEdges(); } else { uint32_t c=pColor(role); if(f%2==0) for(int i=0;i=8&&gc<=15; if(pl==2) return gc==1&&gr>=8&&gr<=15; if(pl==3) return gc==22&&gr>=8&&gr<=15; if(pl==4) return gr==22&&gc>=8&&gc<=15; return false; } bool onPromotion(int pl, int gr, int gc) { if(pl==1) return gr>=16; if(pl==2) return gc>=16; if(pl==3) return gc<=7; if(pl==4) return gr<=7; return false; } void getMovesRaw(int gr, int gc, int &mc, int mv[][2]) { mc = 0; uint8_t piece = gBoard[gr][gc]; if(isEm(piece)) return; int pl=plOf(piece), pt=tyOf(piece); auto canLand = [&](int r,int c)->bool { if(!validSq(r,c)) return false; uint8_t t=gBoard[r][c]; return isEm(t)||plOf(t)!=pl; }; auto push = [&](int r,int c) { if(canLand(r,c)){mv[mc][0]=r;mv[mc][1]=c;mc++;} }; auto slide = [&](int dr,int dc) { for(int s=1;s<24;s++) { int nr=gr+s*dr,nc=gc+s*dc; if(!validSq(nr,nc)) break; uint8_t t=gBoard[nr][nc]; if(isEm(t)){mv[mc][0]=nr;mv[mc][1]=nc;mc++;} else{if(plOf(t)!=pl){mv[mc][0]=nr;mv[mc][1]=nc;mc++;} break;} } }; switch(pt) { case PAWN: { int dr=PDR[pl], dc=PDC[pl], nr=gr+dr, nc=gc+dc; if(validSq(nr,nc)&&isEm(gBoard[nr][nc])) { mv[mc][0]=nr; mv[mc][1]=nc; mc++; if(onPawnStart(pl,gr,gc)) { int r2=nr+dr,c2=nc+dc; if(validSq(r2,c2)&&isEm(gBoard[r2][c2])){mv[mc][0]=r2;mv[mc][1]=c2;mc++;} } } int cr1r,cr1c,cr2r,cr2c; if(dr!=0){cr1r=gr+dr;cr1c=gc-1;cr2r=gr+dr;cr2c=gc+1;} else {cr1r=gr-1; cr1c=gc+dc;cr2r=gr+1; cr2c=gc+dc;} for(int i=0;i<2;i++){int cr=(i==0)?cr1r:cr2r,cc=(i==0)?cr1c:cr2c; if(validSq(cr,cc)&&!isEm(gBoard[cr][cc])&&plOf(gBoard[cr][cc])!=pl){mv[mc][0]=cr;mv[mc][1]=cc;mc++;}} break; } case KNIGHT: { int nd[8][2]={{2,1},{1,2},{-1,2},{-2,1},{-2,-1},{-1,-2},{1,-2},{2,-1}}; for(int i=0;i<8;i++) push(gr+nd[i][0],gc+nd[i][1]); break; } case BISHOP: slide(1,1);slide(1,-1);slide(-1,1);slide(-1,-1); break; case ROOK: slide(1,0);slide(-1,0);slide(0,1);slide(0,-1); break; case QUEEN: slide(1,0);slide(-1,0);slide(0,1);slide(0,-1);slide(1,1);slide(1,-1);slide(-1,1);slide(-1,-1); break; case KING: { int kd[8][2]={{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}; for(int i=0;i<8;i++) push(gr+kd[i][0],gc+kd[i][1]); break; } } } bool isKingInCheck(int player) { int kr=-1, kc=-1; for(int r=0;r<24&&kr==-1;r++) for(int c=0;c<24&&kr==-1;c++) if(validSq(r,c)&&plOf(gBoard[r][c])==player&&tyOf(gBoard[r][c])==KING){kr=r;kc=c;} if(kr==-1) return false; for(int ep=1;ep<=4;ep++) { if(ep==player) continue; int dr=PDR[ep], dc=PDC[ep]; if(dr!=0) { for(int d=-1;d<=1;d+=2){int pr=kr-dr,pc=kc+d;if(validSq(pr,pc)&&plOf(gBoard[pr][pc])==ep&&tyOf(gBoard[pr][pc])==PAWN)return true;} } else { for(int d=-1;d<=1;d+=2){int pr=kr+d,pc=kc-dc;if(validSq(pr,pc)&&plOf(gBoard[pr][pc])==ep&&tyOf(gBoard[pr][pc])==PAWN)return true;} } } int nd[8][2]={{2,1},{1,2},{-1,2},{-2,1},{-2,-1},{-1,-2},{1,-2},{2,-1}}; for(int i=0;i<8;i++){int nr=kr+nd[i][0],nc=kc+nd[i][1];if(validSq(nr,nc)&&!isEm(gBoard[nr][nc])&&plOf(gBoard[nr][nc])!=player&&tyOf(gBoard[nr][nc])==KNIGHT)return true;} int kd[8][2]={{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}; for(int i=0;i<8;i++){int nr=kr+kd[i][0],nc=kc+kd[i][1];if(validSq(nr,nc)&&!isEm(gBoard[nr][nc])&&plOf(gBoard[nr][nc])!=player&&tyOf(gBoard[nr][nc])==KING)return true;} int drs[4]={1,-1,0,0},dcs[4]={0,0,1,-1},drd[4]={1,1,-1,-1},dcd[4]={1,-1,1,-1}; for(int i=0;i<4;i++){ for(int s=1;s<24;s++){int nr=kr+s*drs[i],nc=kc+s*dcs[i];if(!validSq(nr,nc))break;uint8_t t=gBoard[nr][nc];if(!isEm(t)){if(plOf(t)!=player&&(tyOf(t)==ROOK||tyOf(t)==QUEEN))return true;break;}} for(int s=1;s<24;s++){int nr=kr+s*drd[i],nc=kc+s*dcd[i];if(!validSq(nr,nc))break;uint8_t t=gBoard[nr][nc];if(!isEm(t)){if(plOf(t)!=player&&(tyOf(t)==BISHOP||tyOf(t)==QUEEN))return true;break;}} } return false; } void getMoves(int gr, int gc, int &mc, int mv[][2]) { getMovesRaw(gr, gc, mc, mv); uint8_t piece=gBoard[gr][gc]; int pl=plOf(piece); int legal[64][2]; int lc2=0; for(int i=0;i0&&dst<=5&&roleKnown[dst]) esp_now_send(roleMac[dst],(uint8_t*)&msg,sizeof(msg)); else esp_now_send(broadcastAddr,(uint8_t*)&msg,sizeof(msg)); } void sendAll(MsgType type, uint8_t d0=0,uint8_t d1=0,uint8_t d2=0,uint8_t d3=0) { for(int r=1;r<=5;r++) if(r!=myRole&&roleKnown[r]) sendMsg(type,(uint8_t)r,d0,d1,d2,d3); } bool addPeer(uint8_t *mac) { if(esp_now_is_peer_exist(mac)) return true; esp_now_peer_info_t p={}; memcpy(p.peer_addr,mac,6); p.channel=0; p.encrypt=false; return esp_now_add_peer(&p)==ESP_OK; } int macCmp(uint8_t *a, uint8_t *b) { for(int i=0;i<6;i++) { if(a[i]b[i]) return 1; } return 0; } void onRecv(const esp_now_recv_info *ri, const uint8_t *data, int len) { if(len < (int)sizeof(Message)) return; const Message *m = (const Message*)data; if(m->dstRole!=0 && m->dstRole!=myRole) return; // Sync counter updated immediately — never lost even during blocking animations if(m->type==MSG_SETUP_DONE && m->srcRole>=1 && m->srcRole<=5) gSetupSet |= (uint8_t)(1 << m->srcRole); // Auto-reconnect: a known peer has rebooted and is searching again if(m->type==MSG_HELLO && myRole!=0) { for(int r=1; r<=5; r++) { if(roleKnown[r] && macCmp((uint8_t*)ri->src_addr, roleMac[r])==0) { reconnectNeeded=true; break; } } } msgType=m->type; msgSrcRole=m->srcRole; msgDstRole=m->dstRole; msgData[0]=m->d[0]; msgData[1]=m->d[1]; msgData[2]=m->d[2]; msgData[3]=m->d[3]; memcpy(msgSenderMac, ri->src_addr, 6); msgPending=true; } void onSent(const wifi_tx_info_t*, esp_now_send_status_t) {} // ── Discovery — collect 5 MACs, assign roles, sync ──────────── bool discoveryLoop() { reconnectNeeded=false; WiFi.macAddress(myMac); uint8_t allMacs[5][6]; memcpy(allMacs[0],myMac,6); int numMacs=1; memset(roleKnown,0,sizeof(roleKnown)); msgPending=false; gSetupSet=0; unsigned long lastB=0; int af=0; unsigned long at=0; // ── Phase 1: collect 5 unique MACs via MSG_HELLO ────────── while(numMacs < 5) { if(digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW)return false;} if(millis()-lastB > 600) { sendMsg(MSG_HELLO,0); lastB=millis(); } if(millis()-at > 90) { searchAnim(af++); at=millis(); } if(msgPending) { msgPending=false; if(msgType==MSG_HELLO) { bool found=false; for(int i=0;i0) { uint8_t tmp[6]; memcpy(tmp,allMacs[j],6); memcpy(allMacs[j],allMacs[j+1],6); memcpy(allMacs[j+1],tmp,6); } } for(int i=0;i<5;i++) if(macCmp(allMacs[i],myMac)==0) myRole=(uint8_t)(i+1); for(int i=0;i<5;i++) { if(macCmp(allMacs[i],myMac)==0) continue; uint8_t role=(uint8_t)(i+1); memcpy(roleMac[role],allMacs[i],6); roleKnown[role]=true; addPeer(roleMac[role]); } roleKnown[myRole]=true; // Roles are computed deterministically from the sorted MAC list — every // board independently arrives at the same assignment with no handshake. // Flash the role color so players can physically identify their board, // then proceed directly to setupPhase. roleFlash(myRole); strip.clear(); strip.show(); delay(200); return true; } // ── Setup — place pieces, sync all boards ───────────────────── bool setupPhase() { gSetupSet |= (uint8_t)(1 << myRole); // mark myself ready to start counting // All players have their back rank at local row 0 and pawns at local row 1. // Local rows 6-7 face the center — glow them in the player's color as a // visual orientation guide. The CENTER board just shows its colored edges. bool piecesOk = (myRole == 5); while(!piecesOk) { if(digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){strip.clear();strip.show();return false;}} readSensors(); piecesOk=true; strip.clear(); for(int lr=0;lr<8;lr++) for(int lc=0;lc<8;lc++) { int gr=gRow(lr,lc), gc=gCol(lr,lc); bool nearCenter = (lr >= 6); // same for every player role bool hasPiece = sensor[lr][lc]; uint8_t expected = gBoard[gr][gc]; if(nearCenter) { strip.setPixelColor(lr*8+lc, pColor(myRole)); // glow near-center rows } else if(!isEm(expected) && plOf(expected)==myRole && !hasPiece) { piecesOk=false; strip.setPixelColor(lr*8+lc, dimColor(myRole)); // missing piece guide } } strip.show(); if(piecesOk) break; delay(200); } // Green "all pieces placed" flash for(int f=0;f<2;f++) { for(int i=0;i 500) { sendMsg(MSG_SETUP_DONE,0); lastS=millis(); } if(myRole==5) centerEdges(); if(msgPending){msgPending=false; if(msgType==MSG_RESET){strip.clear();strip.show();return false;}} delay(20); } unsigned long graceEnd=millis()+1000; while(millis() 500) { sendMsg(MSG_SETUP_DONE,0); lastS=millis(); } if(msgPending){msgPending=false; if(msgType==MSG_RESET){strip.clear();strip.show();return false;}} delay(20); } for(int f=0;f<2;f++) { for(int i=0;i dr1*dr1+dc1*dc1) { int t0=mv[j][0],t1=mv[j][1]; mv[j][0]=mv[j+1][0]; mv[j][1]=mv[j+1][1]; mv[j+1][0]=t0; mv[j+1][1]=t1; } } strip.clear(); if(liftOnMe) setGLED(liftGR,liftGC,strip.Color(0,0,0,255)); for(int i=0;i 0) { for(int lr=0;lr<8;lr++) for(int lc=0;lc<8;lc++) { if(!sensor[lr][lc]||sensorPrev[lr][lc]) continue; int gr=gRow(lr,lc), gc=gCol(lr,lc); if(isHL(gr,gc)) { setGLED(gr,gc,strip.Color(0,255,0,0)); strip.show(); sendMsg(MSG_PLACED,(uint8_t)activePl,(uint8_t)gr,(uint8_t)gc); while(true) { if(reconnectNeeded){strip.clear();strip.show();return false;} if(digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){sendAll(MSG_RESET);strip.clear();strip.show();return false;}} if(msgPending) { msgPending=false; if(msgType==MSG_MOVE) { applyMoveToBoard((int)msgData[0],(int)msgData[1],(int)msgData[2],(int)msgData[3]); checkEliminations(); hlCount=0; strip.clear(); restorePassiveDisplay(); readSensors(); memcpy(sensorPrev,sensor,sizeof(sensor)); return true; } if(msgType==MSG_RESET){ strip.clear(); strip.show(); return false; } } delay(20); } } else { illegalFlash(gr,gc); strip.clear(); for(int i=0;i