// ============================================================ // long_chess.ino — Long Chess (8×16 board, two boards end-to-end) // // Board A (White) = global rows 0–7 (rows 1–2 hold White pieces) // Board B (Black) = global rows 8–15 (rows 14–15 hold Black pieces) // // The two boards sit end-to-end forming one 8×16 playing field. // Any piece can move anywhere on the full 16-row board. // Legal-move LEDs ripple across BOTH boards simultaneously. // // When a piece crosses to the other physical board, the player // physically carries it over. The receiving board detects the // placement and confirms the move back. // // Higher MAC address = WHITE (Board A). // Lower MAC address = BLACK (Board B). // IO9 = reset / return to discovery at any time. // ============================================================ #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); // ── ESP-NOW protocol ────────────────────────────────────────── // Active board → Passive board: MSG_HIGHLIGHT, MSG_CLEAR, MSG_MOVE // Passive board → Active board: MSG_LIFT, MSG_PLACED, MSG_CANCEL enum MsgType : uint8_t { MSG_HELLO = 0, MSG_READY = 1, MSG_SETUP_DONE = 2, MSG_LIFT = 3, // I detected active-color piece lift on MY board // d[0]=gRow, d[1]=col MSG_HIGHLIGHT = 4, // show legal-move hint on YOUR board // d[0]=gRow, d[1]=col, d[2]=type (0=empty,1=capture) MSG_CLEAR = 5, // clear all highlights on YOUR board MSG_PLACED = 6, // active piece placed on MY board (cross-board move) // d[0]=gRow, d[1]=col MSG_CANCEL = 7, // cross-board lift cancelled (piece returned to origin) MSG_MOVE = 8, // full move for state sync // d[0]=fromGRow, d[1]=fromCol, d[2]=toGRow, d[3]=toCol MSG_RESET = 9 }; struct __attribute__((packed)) Message { MsgType type; uint8_t d[4]; }; uint8_t broadcastAddr[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; uint8_t peerMac[6]; bool peerFound = false, iAmWhite = false; volatile bool msgPending = false; volatile MsgType msgType; volatile uint8_t msgData[4]; uint8_t msgSenderMac[6]; // ── Global 16-row board ─────────────────────────────────────── // Row 0 = White back rank (Board A local row 0) // Row 1 = White pawns (Board A local row 1) // Rows 2–13 empty // Row 14 = Black pawns (Board B local row 6) // Row 15 = Black back rank (Board B local row 7) char gBoard[16][8]; bool sensor[8][8], sensorPrev[8][8]; // Board A (White) physical rows = global rows 0–7 (offset 0) // Board B (Black) physical rows = global rows 8–15 (offset 8) int myOff() { return iAmWhite ? 0 : 8; } int toG(int lr) { return lr + myOff(); } int toLoc(int gr) { return gr - myOff(); } bool onMe(int gr) { return gr >= myOff() && gr < myOff() + 8; } void setGLED(int gr, int c, uint32_t color) { if (onMe(gr)) strip.setPixelColor(toLoc(gr) * 8 + c, color); } // Highlights cache on the passive board (so we can re-draw after illegal attempts) struct HLSq { uint8_t gr, col, type; }; HLSq hlList[64]; int hlCount = 0; bool isHL(int gr, int c) { for (int i = 0; i < hlCount; i++) if (hlList[i].gr == (uint8_t)gr && hlList[i].col == (uint8_t)c) return true; return false; } void initGBoard() { for (int r = 0; r < 16; r++) for (int c = 0; c < 8; c++) gBoard[r][c] = ' '; const char wb[8] = {'R','N','B','Q','K','B','N','R'}; const char bb[8] = {'r','n','b','q','k','b','n','r'}; for (int c = 0; c < 8; c++) { gBoard[0][c]=wb[c]; gBoard[1][c]='P'; } for (int c = 0; c < 8; c++) { gBoard[15][c]=bb[c]; gBoard[14][c]='p'; } } // ── Hardware ────────────────────────────────────────────────── void setColumn(int col) { digitalWrite(RCLK_PIN, LOW); for (int i = 7; 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 boardCombineAnim() { // Stage 1 — thick wave sweeps across both boards end-to-end (global rows 0→15→0). // Both boards run identical timing; each lights only its own physical rows by // converting local row → global row and checking distance from the global center. const int thickness = 3; auto waveFrame = [&](int globalCenter) { strip.clear(); for (int lr = 0; lr < 8; lr++) { int gr = toG(lr); float dist = fabsf((float)(gr - globalCenter)); if (dist < (float)thickness) { float bright = 1.0f - dist / (float)thickness; uint8_t w = (uint8_t)(210.0f * bright); uint8_t b = (uint8_t)(160.0f * bright); for (int c = 0; c < 8; c++) strip.setPixelColor(lr * 8 + c, strip.Color(0, 0, b, w)); } } strip.show(); delay(45); }; // Forward sweep: global row 15+thickness → -thickness (Black side → White side) for (int gc = 15 + thickness; gc >= -thickness; gc--) waveFrame(gc); // Return sweep: global row -thickness → 15+thickness for (int gc = -thickness; gc < 16 + thickness; gc++) waveFrame(gc); // Stage 2 — piece squares pulse white × 3 auto piecePulse = [&](uint8_t b) { strip.clear(); for (int lr = 0; lr < 8; lr++) for (int c = 0; c < 8; c++) if (gBoard[toG(lr)][c] != ' ') strip.setPixelColor(lr * 8 + c, strip.Color(0, 0, 0, b)); strip.show(); }; for (int pulse = 0; pulse < 3; pulse++) { for (int b = 0; b <= 255; b += 17) { piecePulse((uint8_t)b); delay(10); } for (int b = 255; b >= 0; b -= 17) { piecePulse((uint8_t)b); delay(10); } } strip.clear(); strip.show(); delay(250); // Stage 3 — starting-player indication (pieces only) // White board: pieces pulse white ("you go first") // Black board: pieces glow blue ("wait for White") if (iAmWhite) { for (int f = 0; f < 4; f++) { strip.clear(); for (int lr = 0; lr < 8; lr++) for (int c = 0; c < 8; c++) if (gBoard[toG(lr)][c] != ' ') strip.setPixelColor(lr * 8 + c, strip.Color(0, 0, 0, 220)); strip.show(); delay(380); strip.clear(); strip.show(); delay(200); } } else { strip.clear(); for (int lr = 0; lr < 8; lr++) for (int c = 0; c < 8; c++) if (gBoard[toG(lr)][c] != ' ') strip.setPixelColor(lr * 8 + c, strip.Color(0, 0, 190, 0)); strip.show(); delay(2200); strip.clear(); strip.show(); } } void illegalFlash(int gr, int c) { if (!onMe(gr)) return; for (int f = 0; f < 3; f++) { setGLED(gr, c, strip.Color(200,150,0,0)); strip.show(); delay(150); setGLED(gr, c, 0); strip.show(); delay(100); } } void captureAnim(int gr, int c) { if (!onMe(gr)) return; float cx = c, cy = (float)toLoc(gr); for (float r = 0; r < 6.0f; r += 0.8f) { strip.clear(); for (int rr = 0; rr < 8; rr++) for (int cc = 0; cc < 8; cc++) { float dx = cc-cx, dy = rr-cy; if (fabsf(sqrtf(dx*dx+dy*dy) - r) < 0.7f) strip.setPixelColor(rr*8+cc, strip.Color(255,0,0,0)); } strip.show(); delay(60); } strip.clear(); strip.show(); } void promotionAnim(int gr, int c) { if (!onMe(gr)) return; int lc = toLoc(gr); for (int f = 0; f < 5; f++) { strip.setPixelColor(lc*8+c, strip.Color(255,200,0,100)); strip.show(); delay(120); strip.setPixelColor(lc*8+c, 0); strip.show(); delay(80); } } void checkFlash() { for (int f = 0; f < 3; f++) { for (int i = 0; i < LED_COUNT; i++) strip.setPixelColor(i, strip.Color(255,0,0,0)); strip.show(); delay(150); strip.clear(); strip.show(); delay(120); } } void checkmateFlash() { for (int f = 0; f < 8; f++) { for (int i = 0; i < LED_COUNT; i++) strip.setPixelColor(i, strip.Color(255,0,0,0)); strip.show(); delay(200); strip.clear(); strip.show(); delay(180); } } void gameOverAnim(bool iWon) { char myCol = iAmWhite ? 'w' : 'b'; for (int f = 0; f < 10; f++) { for (int lr = 0; lr < 8; lr++) for (int c = 0; c < 8; c++) { int gr = toG(lr); char p = gBoard[gr][c]; if (p == ' ') { strip.setPixelColor(lr*8+c, 0); continue; } bool mine = (myCol=='w') ? (p>='A'&&p<='Z') : (p>='a'&&p<='z'); strip.setPixelColor(lr*8+c, (f%2==0) ? (mine ? strip.Color(0,255,0,0) : strip.Color(255,0,0,0)) : 0); } strip.show(); delay(350); } strip.clear(); strip.show(); } // ── Chess logic — 16-row board ──────────────────────────────── bool isAttacked(int r, int c, char byColor) { int pd = (byColor=='w') ? 1 : -1; char eP = (byColor=='w') ? 'P' : 'p'; int pdc[2] = {-1, 1}; for (int i = 0; i < 2; i++) { int pr = r-pd, pc = c+pdc[i]; if (pr>=0&&pr<16&&pc>=0&&pc<8&&gBoard[pr][pc]==eP) return true; } char eN=(byColor=='w')?'N':'n'; int kd[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=r+kd[i][0], nc=c+kd[i][1]; if (nr>=0&&nr<16&&nc>=0&&nc<8&&gBoard[nr][nc]==eN) return true; } char eK=(byColor=='w')?'K':'k'; int kd2[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=r+kd2[i][0], nc=c+kd2[i][1]; if (nr>=0&&nr<16&&nc>=0&&nc<8&&gBoard[nr][nc]==eK) return true; } char eR=(byColor=='w')?'R':'r', eQ=(byColor=='w')?'Q':'q', eB=(byColor=='w')?'B':'b'; // Straight rays for rook/queen (no reflection) int dirs[4][2]={{1,0},{-1,0},{0,1},{0,-1}}; for (int i=0;i<4;i++){ for (int s=1;s<16;s++){int nr=r+s*dirs[i][0],nc=c+s*dirs[i][1];if(nr<0||nr>=16||nc<0||nc>=8)break;char t=gBoard[nr][nc];if(t!=' '){if(t==eR||t==eQ)return true;break;}} } // Reflecting diagonal rays for bishop/queen int diagDr[4]={1,1,-1,-1}, diagDc[4]={1,-1,1,-1}; for (int i=0;i<4;i++){ int nr=r, nc=c, dc=diagDc[i]; for (int s=0;s<16;s++){ nr+=diagDr[i]; nc+=dc; if(nc<0){nc=-nc;dc=-dc;} if(nc>7){nc=14-nc;dc=-dc;} if(nr<0||nr>=16)break; char t=gBoard[nr][nc]; if(t!=' '){if(t==eB||t==eQ)return true;break;} } } return false; } bool inCheck(char color) { char king=(color=='w')?'K':'k', enemy=(color=='w')?'b':'w'; for (int r=0;r<16;r++) for (int c=0;c<8;c++) if(gBoard[r][c]==king) return isAttacked(r,c,enemy); return false; } void getMoves(int row, int col, int &mc, int mv[][2]) { mc = 0; char piece = gBoard[row][col]; if (piece == ' ') return; char clr = (piece>='a'&&piece<='z') ? 'b' : 'w'; char pt = (piece>='a'&&piece<='z') ? piece-32 : piece; auto canLand = [&](int r, int c) -> bool { if (r<0||r>=16||c<0||c>=8) return false; char t = gBoard[r][c]; return t==' ' || ((t>='a'&&t<='z') != (clr=='b')); }; auto push = [&](int r, int c) { if (canLand(r,c)) { mv[mc][0]=r; mv[mc][1]=c; mc++; } }; auto line = [&](int dr, int dc) { for (int s=1;s<16;s++){ int nr=row+s*dr, nc=col+s*dc; if (nr<0||nr>=16||nc<0||nc>=8) break; char t=gBoard[nr][nc]; if (t==' ') { mv[mc][0]=nr; mv[mc][1]=nc; mc++; } else { if ((t>='a'&&t<='z')!=(clr=='b')){ mv[mc][0]=nr; mv[mc][1]=nc; mc++; } break; } } }; // Diagonal line that reflects off the left (col 0) and right (col 7) walls. // The row direction stays constant; the column direction flips on each bounce. // The ray stops only when it hits the top/bottom boundary or a blocking piece. auto reflLine = [&](int dr, int initDc) { int nr=row, nc=col, dc=initDc; for (int s=0; s<16; s++) { nr += dr; nc += dc; if (nc < 0) { nc = -nc; dc = -dc; } // bounce off left wall if (nc > 7) { nc = 14 - nc; dc = -dc; } // bounce off right wall if (nr < 0 || nr >= 16) break; char t=gBoard[nr][nc]; if (t==' ') { mv[mc][0]=nr; mv[mc][1]=nc; mc++; } else { if ((t>='a'&&t<='z')!=(clr=='b')){ mv[mc][0]=nr; mv[mc][1]=nc; mc++; } break; } } }; switch (pt) { case 'P': { // White pawns move toward row 15; Black toward row 0 int dir=(clr=='w')?1:-1, start=(clr=='w')?1:14; if (row+dir>=0&&row+dir<16&&gBoard[row+dir][col]==' ') { mv[mc][0]=row+dir; mv[mc][1]=col; mc++; if (row==start&&gBoard[row+2*dir][col]==' ') { mv[mc][0]=row+2*dir; mv[mc][1]=col; mc++; } } int capDc[2]={-1,1}; for (int i=0;i<2;i++){ int nr=row+dir, nc=col+capDc[i]; if (nr>=0&&nr<16&&nc>=0&&nc<8) { char t=gBoard[nr][nc]; if (t!=' '&&(t>='a'&&t<='z')!=(clr=='b')) { mv[mc][0]=nr; mv[mc][1]=nc; mc++; } } } break; } case 'R': line(1,0);line(-1,0);line(0,1);line(0,-1); break; case 'B': reflLine(1,1);reflLine(1,-1);reflLine(-1,1);reflLine(-1,-1); break; case 'Q': line(1,0);line(-1,0);line(0,1);line(0,-1); reflLine(1,1);reflLine(1,-1);reflLine(-1,1);reflLine(-1,-1); break; case 'N': { 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(row+nd[i][0],col+nd[i][1]); break; } case 'K': { 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(row+kd[i][0],col+kd[i][1]); break; } } // Filter moves that leave own king in check int legal[64][2]; int lc = 0; for (int i = 0; i < mc; i++) { int tr=mv[i][0], tc=mv[i][1]; char save=gBoard[tr][tc]; gBoard[tr][tc]=piece; gBoard[row][col]=' '; if (!inCheck(clr)) { legal[lc][0]=tr; legal[lc][1]=tc; lc++; } gBoard[row][col]=piece; gBoard[tr][tc]=save; } mc = lc; for (int i=0;i='A'&&p<='Z'):(p>='a'&&p<='z'); if (!mine) continue; int mc=0; int mv[64][2]; getMoves(r,c,mc,mv); if (mc>0) return true; } return false; } bool inLegal(int gr, int c, int mc, int mv[][2]) { for (int i=0;itype; 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) {} 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; } bool macGt(uint8_t *a, uint8_t *b) { for (int i=0;i<6;i++) { if(a[i]>b[i])return true; if(a[i]600) { sendMsg(MSG_HELLO); lastB=millis(); } if (peerFound&&!running&&millis()-lastR>700) { sendMsg(MSG_READY); lastR=millis(); } if (millis()-at>90) { searchAnim(af++); at=millis(); } if (msgPending) { msgPending=false; if (msgType==MSG_HELLO&&!peerFound) { memcpy(peerMac,msgSenderMac,6); peerFound=true; addPeer(peerMac); iAmWhite=macGt(myMac,peerMac); sendMsg(MSG_READY); lastR=millis(); } else if (msgType==MSG_HELLO&&peerFound) { sendMsg(MSG_READY); lastR=millis(); } else if (msgType==MSG_READY) { if (!peerFound) { memcpy(peerMac,msgSenderMac,6); peerFound=true; addPeer(peerMac); iAmWhite=macGt(myMac,peerMac); sendMsg(MSG_READY); lastR=millis(); } if (peerFound) running=true; } } delay(10); } strip.clear(); strip.show(); delay(300); boardCombineAnim(); return true; } // ── Setup — wait for home pieces, sync with peer ────────────── bool setupPhase() { // White: needs pieces on local rows 0–1 (global 0–1) // Black: needs pieces on local rows 6–7 (global 14–15) while (true) { if (digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){strip.clear();strip.show();return false;}} readSensors(); bool ok = true; for (int lr=0;lr<8;lr++) for (int c=0;c<8;c++) { int gr=toG(lr); bool need = (gBoard[gr][c]!=' ') && !sensor[lr][c]; if (need) ok=false; strip.setPixelColor(lr*8+c, need ? strip.Color(0,0,0,80) : 0); } strip.show(); if (ok) break; delay(200); } for (int f=0;f<2;f++) { for (int i=0;i500) { sendMsg(MSG_SETUP_DONE); lastS=millis(); } uint8_t pulse = (uint8_t)(80 + 80*sinf(millis()*0.005f)); strip.clear(); int cx[4]={3*8+3,3*8+4,4*8+3,4*8+4}; for (int i=0;i<4;i++) strip.setPixelColor(cx[i], strip.Color(0,0,pulse,0)); strip.show(); if (msgPending) { msgPending=false; if (msgType==MSG_SETUP_DONE) peerOk=true; else if (msgType==MSG_RESET) { strip.clear(); strip.show(); return false; } } delay(20); } sendMsg(MSG_SETUP_DONE); for (int f=0;f<2;f++) { for (int i=0;i='A'&&p<='Z'):(p>='a'&&p<='z'); if (mine) strip.setPixelColor(lr*8+c, strip.Color(0,0,0,40)); } strip.show(); } // Show legal-move hints with a distance-sorted ripple across both boards. // Local squares light up with a 30 ms delay; remote ones are sent via // MSG_HIGHLIGHT at the same cadence so the passive board mirrors the ripple. void showHints(int liftGR, int liftGC, bool liftOnMe, int mc, int mv[][2]) { // Bubble-sort by global distance from the lifted square 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 < mc; i++) { int gr2=mv[i][0], c2=mv[i][1]; bool cap = (gBoard[gr2][c2] != ' '); uint32_t color = cap ? strip.Color(255,60,0,0) : strip.Color(0,0,0,180); if (onMe(gr2)) { setGLED(gr2, c2, color); strip.show(); delay(30); } else { strip.show(); sendMsg(MSG_HIGHLIGHT, (uint8_t)gr2, (uint8_t)c2, (uint8_t)(cap?1:0)); delay(30); // same cadence keeps ripple in sync on passive board } } strip.show(); } void restoreHintsLocal(int liftGR, int liftGC, bool liftOnMe, int mc, int mv[][2]) { strip.clear(); if (liftOnMe) setGLED(liftGR, liftGC, strip.Color(0,0,0,255)); for (int i=0;i='A'&&p<='Z'):(p>='a'&&p<='z'); if (mine) { liftGR=gr; liftGC=c; liftOnMe=true; break; } } // Other board detected my piece (it had crossed there) if (!liftOnMe && msgPending) { msgPending=false; if (msgType==MSG_LIFT) { liftGR=(int)msgData[0]; liftGC=(int)msgData[1]; liftOnMe=false; } else if (msgType==MSG_RESET) { strip.clear(); strip.show(); return false; } } } // ── Phase 2: process lift once ────────────────────────── if (liftGR != -1 && !hintsShown) { getMoves(liftGR, liftGC, mc, mv); if (mc == 0) { // Pinned or stuck — flash and wait for return illegalFlash(liftGR, liftGC); if (liftOnMe) { while (!sensor[toLoc(liftGR)][liftGC]) { if (digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){sendMsg(MSG_RESET);strip.clear();strip.show();return false;}} readSensors(); memcpy(sensorPrev,sensor,sizeof(sensor)); delay(40); } } else { while (true) { if (digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){sendMsg(MSG_RESET);strip.clear();strip.show();return false;}} if (msgPending){ msgPending=false; if(msgType==MSG_CANCEL)break; if(msgType==MSG_RESET)return false; } delay(20); } } liftGR=-1; liftGC=-1; mc=0; liftOnMe=false; glowMyPieces(); memcpy(sensorPrev,sensor,sizeof(sensor)); continue; } showHints(liftGR, liftGC, liftOnMe, mc, mv); hintsShown=true; } // ── Phase 3: wait for placement ───────────────────────── if (liftGR != -1 && hintsShown) { // Piece returned to origin (local lift only) if (liftOnMe && sensor[toLoc(liftGR)][liftGC] && !sensorPrev[toLoc(liftGR)][liftGC]) { sendMsg(MSG_CLEAR); strip.clear(); strip.show(); liftGR=-1; liftGC=-1; mc=0; hintsShown=false; glowMyPieces(); memcpy(sensorPrev,sensor,sizeof(sensor)); continue; } // Local placement for (int lr2=0;lr2<8;lr2++) for (int c2=0;c2<8;c2++) { if (!sensor[lr2][c2] || sensorPrev[lr2][c2]) continue; int gr2=toG(lr2); if (inLegal(gr2,c2,mc,mv)) { applyMoveToBoard(liftGR,liftGC,gr2,c2); sendMsg(MSG_CLEAR); strip.clear(); if (onMe(liftGR)) setGLED(liftGR, liftGC, strip.Color(0,80,0,0)); setGLED(gr2, c2, strip.Color(0,255,0,0)); strip.show(); delay(500); strip.clear(); strip.show(); sendMsg(MSG_MOVE,(uint8_t)liftGR,(uint8_t)liftGC,(uint8_t)gr2,(uint8_t)c2); readSensors(); memcpy(sensorPrev,sensor,sizeof(sensor)); return true; } else { illegalFlash(gr2, c2); restoreHintsLocal(liftGR,liftGC,liftOnMe,mc,mv); } } // Cross-board placement reported by passive board if (msgPending) { msgPending=false; if (msgType==MSG_PLACED) { int gr2=(int)msgData[0], c2=(int)msgData[1]; if (inLegal(gr2,c2,mc,mv)) { applyMoveToBoard(liftGR,liftGC,gr2,c2); strip.clear(); if (onMe(liftGR)) { setGLED(liftGR,liftGC,strip.Color(0,255,0,0)); strip.show(); delay(400); strip.clear(); strip.show(); } sendMsg(MSG_MOVE,(uint8_t)liftGR,(uint8_t)liftGC,(uint8_t)gr2,(uint8_t)c2); readSensors(); memcpy(sensorPrev,sensor,sizeof(sensor)); return true; } } else if (msgType==MSG_CANCEL && !liftOnMe) { // Remote piece returned to origin sendMsg(MSG_CLEAR); strip.clear(); strip.show(); liftGR=-1; liftGC=-1; mc=0; hintsShown=false; glowMyPieces(); memcpy(sensorPrev,sensor,sizeof(sensor)); continue; } else if (msgType==MSG_RESET) { strip.clear(); strip.show(); return false; } } } memcpy(sensorPrev,sensor,sizeof(sensor)); delay(40); } } // ═══════════════════════════════════════════════════════════════ // PASSIVE TURN — support the other player's move // • Show MSG_HIGHLIGHT squares as they arrive (building the ripple). // • If active player has a piece on MY physical rows, detect its // lift and report MSG_LIFT to the active board. // • If a piece lands on a highlighted square, report MSG_PLACED. // • End when MSG_MOVE arrives and is applied. // ═══════════════════════════════════════════════════════════════ bool handlePassiveTurn() { char activeCol = iAmWhite ? 'b' : 'w'; // the other player's color hlCount=0; strip.clear(); strip.show(); int liftGR=-1, liftGC=-1, liftLR=-1; // cross-board lift I detected while (true) { if (digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){sendMsg(MSG_RESET);strip.clear();strip.show();return false;}} readSensors(); // ── Incoming messages ─────────────────────────────────── if (msgPending) { msgPending=false; switch (msgType) { case MSG_HIGHLIGHT: if (onMe((int)msgData[0])) { bool cap=(msgData[2]==1); setGLED((int)msgData[0],(int)msgData[1], cap ? strip.Color(255,60,0,0) : strip.Color(0,0,0,180)); strip.show(); } if (hlCount<64) { hlList[hlCount].gr=msgData[0]; hlList[hlCount].col=msgData[1]; hlList[hlCount].type=msgData[2]; hlCount++; } break; case MSG_CLEAR: hlCount=0; liftGR=-1; liftGC=-1; liftLR=-1; strip.clear(); strip.show(); break; case MSG_MOVE: applyMoveToBoard((int)msgData[0],(int)msgData[1],(int)msgData[2],(int)msgData[3]); hlCount=0; strip.clear(); strip.show(); { char myCol2=iAmWhite?'w':'b'; if(inCheck(myCol2)) checkFlash(); } readSensors(); memcpy(sensorPrev,sensor,sizeof(sensor)); return true; case MSG_RESET: strip.clear(); strip.show(); return false; default: break; } } // ── Detect active player's piece lift on MY board ─────── if (liftGR == -1) { for (int lr=0;lr<8;lr++) for (int c=0;c<8;c++) { if (!sensorPrev[lr][c] || sensor[lr][c]) continue; int gr=toG(lr); char p=gBoard[gr][c]; if(p==' ')continue; bool isActive=(activeCol=='w')?(p>='A'&&p<='Z'):(p>='a'&&p<='z'); if (isActive) { liftGR=gr; liftGC=c; liftLR=lr; sendMsg(MSG_LIFT,(uint8_t)gr,(uint8_t)c); break; } } } // ── Cross-board lift on MY board: watch for return to origin ── // (active player previously crossed a piece here and is now moving it again) if (liftGR != -1) { if (sensor[liftLR][liftGC] && !sensorPrev[liftLR][liftGC]) { sendMsg(MSG_CANCEL); hlCount=0; strip.clear(); strip.show(); liftGR=-1; liftGC=-1; liftLR=-1; memcpy(sensorPrev,sensor,sizeof(sensor)); continue; } } // ── Watch for placement on any highlighted square ─────────── // Runs whenever highlights exist — covers both: // (a) active player lifted from their own board and crosses here, and // (b) active player lifted a piece already on my board. if (hlCount > 0) { for (int lr2=0;lr2<8;lr2++) for (int c2=0;c2<8;c2++) { if (!sensor[lr2][c2] || sensorPrev[lr2][c2]) continue; int gr2=toG(lr2); if (isHL(gr2,c2)) { setGLED(gr2, c2, strip.Color(0,255,0,0)); strip.show(); sendMsg(MSG_PLACED,(uint8_t)gr2,(uint8_t)c2); // Wait for MSG_MOVE confirmation while (true) { if (digitalRead(RESET_PIN)==LOW){delay(50);if(digitalRead(RESET_PIN)==LOW){sendMsg(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]); hlCount=0; strip.clear(); strip.show(); { char myCol2=iAmWhite?'w':'b'; if(inCheck(myCol2)) checkFlash(); } readSensors(); memcpy(sensorPrev,sensor,sizeof(sensor)); return true; } if (msgType==MSG_RESET) { strip.clear(); strip.show(); return false; } } delay(20); } } else { // Illegal square — flash amber, restore highlights illegalFlash(gr2, c2); strip.clear(); for (int i=0;i