// ============================================================ // Classic Chess — Open Chess // // Two players, one board. Lift a piece and its legal squares light up; the // board tracks check, checkmate, and stalemate. Pawns reaching the last rank // become queens. Castling and en passant are not implemented. // // Button A restart (back to the setup screen) // Button B pause: the board goes dark until A is pressed // // Between moves, every piece belonging to the side to move glows softly so // you can always see whose turn it is. // // 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 ───────────────────────────────────────── 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 TURN_COLOR = GREEN.scaled(70); // soft glow under the side to move // ── Game state ────────────────────────────────────────────────────────────── Position pos; bool awaitingSetup = true; bool paused = false; void setup() { begin("classic-chess", "Classic Chess", "2.0.0"); app.onIdentify = sendPosition; } 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; } // Look for a piece of the side to move that has just been lifted. 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; Move move; if (!waitForMove(row, col, move)) return; commitMove(move); afterMove(); return; } } board.sync(); } // ── Setup screen ──────────────────────────────────────────────────────────── void waitForSetup() { app.status("setup", "Place all 32 pieces on their starting squares"); lights.clear(); lights.show(); while (!board.covers(chess::START_LAYOUT)) { service(); if (buttons.pressed(BUTTON_B)) { pauseGame(); return; } 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); } pos.reset(); anim::ripple(3, 3, WHITE, 45); board.sync(); awaitingSetup = false; sound.gameStart(); app.status("playing", "White to move"); sendPosition(); showTurn(); } // Lights every piece of the side to move. This is the resting picture of the // board between moves. void showTurn() { 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) lights.set(row, col, TURN_COLOR); } lights.show(); } void restart() { anim::flashAll(GREEN, 3); awaitingSetup = true; app.log("New game"); } void pauseGame() { // Count down along rank 1 so an accidental press is obvious, then go dark. 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); } // ── Making a move ─────────────────────────────────────────────────────────── // Shows where the lifted piece may go and waits for it to land. Returns false // if the piece went back to its square or a button was pressed. 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 showTurn(); 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)) { // enemy piece removed first 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; } } // Landed somewhere illegal: flash it until the piece is lifted again. 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(); } // Check, checkmate, stalemate — and the status line for the Connect page. 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(); anim::flashAll(CHECK_COLOR, 3, 120, 120); app.status("playing", side == 'w' ? "White is in check" : "Black is in check"); } else { app.status("playing", side == 'w' ? "White to move" : "Black to move"); } showTurn(); } // Holds a colour on every occupied square until a button is pressed. 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(); } // ── Promotion ─────────────────────────────────────────────────────────────── 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); // pawn off while (!board.occupied(row, col)) anim::flashSquare(row, col, PROMO_COLOR, 1, 200); // queen on app.log("Promotion complete"); }