// ============================================================ // Play Against a Bot — Open Chess // // You are White; the board plays Black. Set up the pieces and the game starts // by itself. Either button restarts it. While it is your move, your pieces // glow softly; while the bot thinks, its pieces do. // // The bot is a real chess engine running on the board (oc_chess.h): negamax // with alpha-beta, move ordering, quiescence, and a time budget. No Wi-Fi. // // 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 ───────────────────────────────────────── // 1 is a beginner that hangs pieces on purpose; 8 thinks for up to six seconds // a move and never blunders. 4 is a club-ish opponent that answers quickly. const int DIFFICULTY = 4; const Color HINT_COLOR = WHITE; // squares your lifted piece can go to const Color CAPTURE_COLOR = RED; // ...and the ones with an enemy piece const Color BOT_COLOR = RED; // the bot's move, for you to play out (red = the bot) const Color ILLEGAL_COLOR = AMBER; // a square you may not move to const Color PROMO_COLOR = GOLD; // pawn reached the last rank const Color TURN_COLOR = GREEN.scaled(70); // soft glow under the side to move // ── Game state ────────────────────────────────────────────────────────────── Position pos; bool awaitingSetup = true; void setup() { begin("play-a-bot", "Play Against a Bot", "2.0.0"); app.onIdentify = sendPosition; // the Connect page asks for the position app.log("Level %d: depth %d, up to %d ms a move. You play White.", DIFFICULTY, chess::Engine::depthFor(DIFFICULTY), chess::Engine::timeMsFor(DIFFICULTY)); } void loop() { service(); if (awaitingSetup) { waitForSetup(); return; } if (buttons.anyPressed()) { restart(); return; } // Your turn: look for a White piece that has just been lifted. for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { if (!board.lifted(row, col) || !chess::isWhite(pos.at(row, col))) continue; Move yours; if (!waitForPlayerMove(row, col, yours)) return; // put back, or reset commitMove(yours); if (gameOver('b')) return; if (pos.inCheck('b')) sound.check(); botTurn(); return; } } board.sync(); } // ── Setup screen ──────────────────────────────────────────────────────────── // Lights every square that still needs a piece until the full starting // position is on the board. void waitForSetup() { app.status("setup", "Place all 32 pieces on their starting squares"); lights.clear(); lights.show(); while (!board.covers(chess::START_LAYOUT)) { service(); 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, and it keeps the easy levels' blunders from // repeating every power cycle. randomSeed(micros()); pos.reset(); anim::ripple(3, 3, WHITE, 45); board.sync(); awaitingSetup = false; sound.gameStart(); app.status("playing", "Your move - you are White"); 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 sendPosition() { char fen[96]; pos.fen(fen); app.state(fen, pos.turn); } // ── Your 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 waitForPlayerMove(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.anyPressed()) { restart(); return false; } // Put back where it came from. if (board.occupied(fromRow, fromCol)) { showTurn(); return false; } for (int i = 0; i < count; i++) { const int tr = options[i].tr, tc = options[i].tc; if (options[i].isCapture()) { // A capture: the enemy piece comes off first, then yours goes down. if (board.lifted(tr, tc)) { lights.clear(); lights.set(tr, tc, BOT_COLOR); lights.show(); while (!board.occupied(tr, tc)) { service(); if (buttons.anyPressed()) { 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.anyPressed()) { restart(); return false; } } showHints(fromRow, fromCol, options, count); } } wait(20); } } // Lights the lifted square, then reveals destinations from nearest to farthest. void showHints(int fromRow, int fromCol, Move *options, int count) { lights.clear(); lights.set(fromRow, fromCol, HINT_COLOR); lights.show(); for (int i = 1; i < count; i++) { // insertion sort by distance Move m = options[i]; int j = i - 1; while (j >= 0 && hintDistance(fromRow, fromCol, options[j]) > hintDistance(fromRow, fromCol, m)) { options[j + 1] = options[j]; j--; } options[j + 1] = m; } for (int i = 0; i < count; i++) { lights.set(options[i].tr, options[i].tc, options[i].isCapture() ? CAPTURE_COLOR : HINT_COLOR); lights.show(); wait(25); } } int hintDistance(int row, int col, const Move &m) { const int dr = m.tr - row, dc = m.tc - col; return dr * dr + dc * dc; } // Applies a move you have physically made and tells the Connect page. 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("White: %s", uci); if (m.promotion) waitForPromotion(m.tr, m.tc); anim::flashSquare(m.tr, m.tc, GREEN, 2, 180); board.sync(); } // ── The bot's move ────────────────────────────────────────────────────────── void botTurn() { app.status("playing", "The bot is thinking..."); showTurn(); // Black's pieces glow while it thinks Move reply; uint32_t nodes = 0; int score = 0; const uint32_t t0 = millis(); const bool found = chess::Engine::choose(pos, 'b', DIFFICULTY, reply, &nodes, &score); if (!found) { gameOver('b'); return; } char uci[6]; reply.uci(uci); app.log("Bot: %s (%lu ms, %lu nodes, eval %d)", uci, millis() - t0, nodes, score); sound.alert(); app.status("playing", "Play the bot's move for it"); if (!playOutBotMove(reply)) return; // reset pressed mid-move if (!gameOver('w')) { if (pos.inCheck('w')) sound.check(); app.status("playing", "Your move"); showTurn(); } } // Flashes the from-square and holds the to-square until you have moved the // bot's piece for it. Returns false if a button interrupted. bool playOutBotMove(const Move &m) { lights.clear(); lights.set(m.tr, m.tc, BOT_COLOR); lights.show(); bool on = true; uint32_t lastFlash = 0; while (board.occupied(m.fr, m.fc)) { // 1. lift the bot's piece service(); if (buttons.anyPressed()) { restart(); return false; } if (millis() - lastFlash > 350) { on = !on; lastFlash = millis(); lights.set(m.fr, m.fc, on ? BOT_COLOR : OFF); lights.show(); } } lights.set(m.fr, m.fc, OFF); lights.show(); if (m.isCapture()) { // 2. remove your captured piece while (board.occupied(m.tr, m.tc)) { service(); if (buttons.anyPressed()) { restart(); return false; } } } while (!board.occupied(m.tr, m.tc)) { // 3. put the bot's piece down service(); if (buttons.anyPressed()) { restart(); return false; } } 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); if (m.promotion) waitForPromotion(m.tr, m.tc); anim::flashSquare(m.tr, m.tc, BOT_COLOR, 2, 180); board.sync(); return true; } // ── Promotion ─────────────────────────────────────────────────────────────── // Pawns always become queens. Waits for the physical swap so the board and // the position agree before play continues. 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)) { // take the pawn off anim::flashSquare(row, col, PROMO_COLOR, 1, 200); } while (!board.occupied(row, col)) { // put the queen on anim::flashSquare(row, col, PROMO_COLOR, 1, 200); } app.log("Promotion complete"); } // ── Game over ─────────────────────────────────────────────────────────────── // Returns true if `sideToMove` has no legal move (mate or stalemate) and the // end screen has run. bool gameOver(char sideToMove) { if (pos.hasLegalMoves(sideToMove)) return false; const bool mate = pos.inCheck(sideToMove); const bool whiteWon = sideToMove == 'b'; if (mate) { app.result("checkmate", whiteWon ? "w" : "b"); app.status("over", whiteWon ? "Checkmate - you win!" : "Checkmate - the bot wins"); app.log(whiteWon ? "CHECKMATE - White wins" : "CHECKMATE - Black wins"); } else { app.result("stalemate", "draw"); app.status("over", "Stalemate - it's a draw"); app.log("STALEMATE - draw"); } // Winners green, losers red, then hold until a button is pressed. const bool winnerIsWhite = mate ? whiteWon : true; for (int flash = 0; flash < 7; flash++) { for (int row = 0; row < 8; row++) for (int col = 0; col < 8; col++) { const char p = pos.at(row, col); if (p == ' ' || flash % 2) { lights.set(row, col, OFF); continue; } lights.set(row, col, chess::isWhite(p) == winnerIsWhite ? GREEN : RED); } lights.show(); wait(400); } while (!buttons.anyPressed()) service(); awaitingSetup = true; return true; }