// ============================================================ // Red Light — Open Chess // // Two players, one board. At the start of each turn the board freezes a // random subset of your pieces: red cannot move, green can. Your plan has // to survive the board deciding which pieces you get. If nothing green has // a legal move, the turn passes. // // Button A restart (back to the setup screen) // Button B during setup: cycle the lock ratio (mild → medium → brutal) // during a game: pause until A is pressed // // Everything hardware-related — sensors, LEDs, battery, the USB link to the // Connect page — is handled by OpenChessCore. This file is only the game. // ============================================================ #include #include using namespace oc; using chess::Move; using chess::Position; // ── Things you might want to change ───────────────────────────────────────── // How many of your pieces lock each turn. Button B on the setup screen cycles // these; the names are what the Connect page prints. const int LOCK_PCTS[] = { 25, 50, 75 }; const char *LOCK_NAMES[] = { "mild", "medium", "brutal" }; const int LOCK_LEVELS = 3; int lockIndex = 1; // medium — half your army, give or take const Color HINT_COLOR = GREEN; // empty squares the piece can reach const Color CAPTURE_COLOR = ORANGE; // squares with an enemy piece const Color ILLEGAL_COLOR = AMBER; // where you may not put it const Color CHECK_COLOR = RED; // the board flashes this on check const Color PROMO_COLOR = GOLD; // pawn reached the last rank const Color WHITE_WINS = WHITE; const Color BLACK_WINS = PURPLE; const Color FROZEN_COLOR = RED; // cannot move this turn const Color FREE_COLOR = GREEN.scaled(90); // soft green: still available // ── Game state ────────────────────────────────────────────────────────────── Position pos; bool frozen[8][8]; bool awaitingSetup = true; bool paused = false; int lockPercent() { return LOCK_PCTS[lockIndex]; } const char *lockName() { return LOCK_NAMES[lockIndex]; } void setup() { begin("red-light", "Red Light", "2.0.0"); app.onIdentify = sendPosition; app.log("Lock ratio %d%% (%s). Press B on the setup screen to change it.", lockPercent(), lockName()); } void loop() { service(); if (paused) { if (buttons.pressed(BUTTON_A)) { paused = false; awaitingSetup = true; } return; } if (awaitingSetup) { waitForSetup(); return; } if (buttons.pressed(BUTTON_A)) { restart(); return; } if (buttons.pressed(BUTTON_B)) { pauseGame(); return; } for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { if (!board.lifted(row, col)) continue; const char piece = pos.at(row, col); if (piece == ' ' || chess::colorOf(piece) != pos.turn) continue; if (frozen[row][col]) { refuseFrozen(row, col); return; } Move move; if (!waitForMove(row, col, move)) return; commitMove(move); afterMove(); return; } } board.sync(); } // ── Setup screen ──────────────────────────────────────────────────────────── void waitForSetup() { char msg[80]; snprintf(msg, sizeof(msg), "Place all 32 pieces. B cycles lock ratio (%s, %d%%)", lockName(), lockPercent()); app.status("setup", msg); lights.clear(); lights.show(); while (!board.covers(chess::START_LAYOUT)) { service(); if (buttons.pressed(BUTTON_B)) { cycleLock(); snprintf(msg, sizeof(msg), "Place all 32 pieces. B cycles lock ratio (%s, %d%%)", lockName(), lockPercent()); app.status("setup", msg); } for (int row = 0; row < 8; row++) for (int col = 0; col < 8; col++) lights.set(row, col, chess::START_LAYOUT[row][col] != ' ' && !board.occupied(row, col) ? WHITE : OFF); lights.show(); wait(80); } // Human timing is good entropy, so the freeze never repeats the same way // every power cycle. randomSeed(micros()); pos.reset(); anim::ripple(3, 3, WHITE, 45); board.sync(); awaitingSetup = false; sound.gameStart(); sendPosition(); startTurn(); } void cycleLock() { lockIndex = (lockIndex + 1) % LOCK_LEVELS; app.log("Lock ratio %d%% (%s)", lockPercent(), lockName()); sound.alert(); // Rank 1 is a bar of the new ratio: red is frozen, green is free. lights.clear(); const int reds = (8 * lockPercent() + 50) / 100; for (int col = 0; col < 8; col++) lights.set(0, col, col < reds ? FROZEN_COLOR : FREE_COLOR); lights.show(); wait(700); } void restart() { anim::flashAll(GREEN, 3); awaitingSetup = true; app.log("New game"); } void pauseGame() { for (int col = 7; col >= 0; col--) { lights.set(0, col, RED); lights.show(); wait(60); } lights.clear(); lights.show(); paused = true; app.status("idle", "Paused - press button A to start a new game"); } void sendPosition() { char fen[96]; pos.fen(fen); app.state(fen, pos.turn); } // ── Freeze ────────────────────────────────────────────────────────────────── // Picks a random subset of the side-to-move's pieces. The count is // round(n × lock%), so 16 pieces at 50% freezes eight. void rollFreeze() { int list[16]; int n = 0; for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { frozen[row][col] = false; const char p = pos.at(row, col); if (p != ' ' && chess::colorOf(p) == pos.turn) list[n++] = squareIndex(row, col); } } for (int i = n - 1; i > 0; i--) { const int j = random(i + 1); const int tmp = list[i]; list[i] = list[j]; list[j] = tmp; } int freezeCount = (n * lockPercent() + 50) / 100; if (freezeCount > n) freezeCount = n; for (int i = 0; i < freezeCount; i++) frozen[list[i] / 8][list[i] % 8] = true; char names[80] = ""; for (int i = 0; i < freezeCount; i++) { char sq[3]; squareName(list[i] / 8, list[i] % 8, sq); if (i) strlcat(names, " ", sizeof(names)); strlcat(names, sq, sizeof(names)); } app.log("%s freeze %d/%d%s%s", pos.turn == 'w' ? "White" : "Black", freezeCount, n, freezeCount ? ": " : "", freezeCount ? names : ""); } bool hasGreenMove() { static Move options[chess::MAX_MOVES]; for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { const char p = pos.at(row, col); if (p == ' ' || chess::colorOf(p) != pos.turn || frozen[row][col]) continue; if (pos.legalMovesFrom(row, col, options) > 0) return true; } } return false; } void showLights() { lights.clear(); for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { const char p = pos.at(row, col); if (p == ' ' || chess::colorOf(p) != pos.turn) continue; lights.set(row, col, frozen[row][col] ? FROZEN_COLOR : FREE_COLOR); } } lights.show(); } // Frozen squares light first, one by one, then the rest go green. That pause // is the board deciding which pieces you get. void revealFreeze() { lights.clear(); lights.show(); sound.alert(); for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { if (!frozen[row][col]) continue; lights.set(row, col, FROZEN_COLOR); lights.show(); wait(35); } } wait(80); showLights(); } void startTurn() { for (int skips = 0; skips < 6; skips++) { rollFreeze(); revealFreeze(); sendPosition(); if (hasGreenMove()) { char msg[80]; const char *side = pos.turn == 'w' ? "White" : "Black"; if (pos.inCheck(pos.turn)) snprintf(msg, sizeof(msg), "%s is in check — green pieces only", side); else snprintf(msg, sizeof(msg), "%s to move — green pieces only", side); app.status("playing", msg); return; } // No green move. In check that is mate: you had to get out and could not. // Otherwise the turn simply passes. if (pos.inCheck(pos.turn)) { const bool whiteWon = pos.turn == 'b'; app.result("checkmate", whiteWon ? "w" : "b"); app.status("over", whiteWon ? "Checkmate - White wins" : "Checkmate - Black wins"); app.log("No green escape from check"); anim::flashAll(CHECK_COLOR, 6, 200, 180); gameOverScreen(whiteWon ? WHITE_WINS : BLACK_WINS); awaitingSetup = true; return; } app.log("%s: no legal green move — turn passes", pos.turn == 'w' ? "White" : "Black"); app.status("playing", pos.turn == 'w' ? "White passes" : "Black passes"); anim::flashAll(FROZEN_COLOR, 1, 120, 80); pos.turn = chess::other(pos.turn); } app.result("stalemate", "draw"); app.status("over", "No green moves left — draw"); anim::flashAll(Color(200, 150, 0), 6, 300, 200); gameOverScreen(Color(200, 150, 0)); awaitingSetup = true; } void refuseFrozen(int row, int col) { sound.illegal(); app.log("Frozen piece"); while (!board.occupied(row, col)) { service(); if (buttons.pressed(BUTTON_A)) { restart(); return; } if (buttons.pressed(BUTTON_B)) { pauseGame(); return; } anim::flashSquare(row, col, FROZEN_COLOR, 1, 120); } showLights(); board.sync(); } // ── Making a move ─────────────────────────────────────────────────────────── bool waitForMove(int fromRow, int fromCol, Move &out) { static Move options[chess::MAX_MOVES]; const int count = pos.legalMovesFrom(fromRow, fromCol, options); showHints(fromRow, fromCol, options, count); for (;;) { service(); if (buttons.pressed(BUTTON_A)) { restart(); return false; } if (buttons.pressed(BUTTON_B)) { pauseGame(); return false; } if (board.occupied(fromRow, fromCol)) { // put back showLights(); return false; } for (int i = 0; i < count; i++) { const int tr = options[i].tr, tc = options[i].tc; if (options[i].isCapture()) { if (board.lifted(tr, tc)) { lights.clear(); lights.set(tr, tc, CAPTURE_COLOR); lights.show(); while (!board.occupied(tr, tc)) { service(); if (buttons.pressed(BUTTON_A)) { restart(); return false; } } out = options[i]; return true; } } else if (board.placed(tr, tc)) { out = options[i]; return true; } } for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { if (!board.placed(row, col) || pos.at(row, col) != ' ') continue; sound.illegal(); while (board.occupied(row, col)) { anim::flashSquare(row, col, ILLEGAL_COLOR, 1, 120); if (buttons.pressed(BUTTON_A)) { restart(); return false; } } showHints(fromRow, fromCol, options, count); } } wait(20); } } void showHints(int fromRow, int fromCol, Move *options, int count) { lights.clear(); lights.set(fromRow, fromCol, WHITE); for (int i = 0; i < count; i++) { lights.set(options[i].tr, options[i].tc, options[i].promotion ? PROMO_COLOR : options[i].isCapture() ? CAPTURE_COLOR : HINT_COLOR); } lights.show(); } void commitMove(const Move &m) { if (m.isCapture()) { sound.capture(); anim::ripple(m.tr, m.tc, CAPTURE_COLOR, 50); } else { sound.move(); } pos.play(m); char uci[6]; m.uci(uci); app.move(uci); app.log("%s: %s", pos.turn == 'b' ? "White" : "Black", uci); if (m.promotion) waitForPromotion(m.tr, m.tc); lights.clear(); lights.show(); board.sync(); } void afterMove() { const char side = pos.turn; if (!pos.hasLegalMoves(side)) { if (pos.inCheck(side)) { const bool whiteWon = side == 'b'; app.result("checkmate", whiteWon ? "w" : "b"); app.status("over", whiteWon ? "Checkmate - White wins" : "Checkmate - Black wins"); anim::flashAll(CHECK_COLOR, 6, 200, 180); gameOverScreen(whiteWon ? WHITE_WINS : BLACK_WINS); } else { app.result("stalemate", "draw"); app.status("over", "Stalemate - draw"); anim::flashAll(Color(200, 150, 0), 6, 300, 200); gameOverScreen(Color(200, 150, 0)); } awaitingSetup = true; return; } if (pos.inCheck(side)) sound.check(); startTurn(); } void gameOverScreen(Color color) { for (int row = 0; row < 8; row++) for (int col = 0; col < 8; col++) lights.set(row, col, pos.at(row, col) != ' ' ? color : OFF); lights.show(); while (!buttons.anyPressed()) service(); lights.clear(); lights.show(); } void waitForPromotion(int row, int col) { sound.promote(); app.status("playing", "Pawn promoted - swap in a queen"); anim::columnChase(col, PROMO_COLOR); while (board.occupied(row, col)) anim::flashSquare(row, col, PROMO_COLOR, 1, 200); while (!board.occupied(row, col)) anim::flashSquare(row, col, PROMO_COLOR, 1, 200); app.log("Promotion complete"); }