// ============================================================ // King of the Hill — Open Chess // // Standard chess with a second way to win: walk your king onto d4, e4, d5, or // e5. Checkmate still ends the game. You cannot step the king through or into // check to get there — the ordinary legal-move generator already refuses that. // // Button A restart (back to the setup screen) // Button B pause: the board goes dark until A is pressed // // The four centre squares glow faintly the whole game and brighten as either // king closes in. A king move that would claim the hill lights up gold. // // 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 const Color HILL_COLOR = GOLD; // the four centre squares const Color HILL_WIN_COLOR = Color(255, 220, 40, 180); // a king move that claims the hill // d4, e4, d5, e5 — row 0 is rank 1, col 0 is the a-file. const int HILL_SQUARES[4][2] = {{3, 3}, {3, 4}, {4, 3}, {4, 4}}; // ── Game state ────────────────────────────────────────────────────────────── Position pos; bool awaitingSetup = true; bool paused = false; void setup() { begin("king-of-the-hill", "King of the Hill", "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; } } showTurn(); board.sync(); } // ── The hill ──────────────────────────────────────────────────────────────── bool isHill(int row, int col) { return (row == 3 || row == 4) && (col == 3 || col == 4); } // Chebyshev distance to the nearer king. The hill brightens as that number // falls, so a king two squares away already makes its nearest centre square // obviously different from the far one. int nearestKingDist(int row, int col) { int best = 8; for (int r = 0; r < 8; r++) { for (int c = 0; c < 8; c++) { if (chess::kindOf(pos.at(r, c)) != 'K') continue; const int d = max(abs(r - row), abs(c - col)); if (d < best) best = d; } } return best; } Color hillColor(int row, int col) { static const uint8_t BASE[] = {255, 170, 88, 40, 24, 18, 14, 12}; const int dist = nearestKingDist(row, col); const uint8_t base = BASE[dist > 7 ? 7 : dist]; // A slow breath on top of the distance so the hill reads as alive rather // than as four stuck pixels. Period is 1.8 s; peak is about a quarter extra. const uint16_t t = millis() % 1800; const uint8_t wave = t < 900 ? (uint8_t)(t / 6) : (uint8_t)((1800 - t) / 6); const uint8_t extra = (uint16_t)base * wave / 600; const uint16_t level = (uint16_t)base + extra; return HILL_COLOR.scaled(level > 255 ? 255 : (uint8_t)level); } void paintHill() { for (int i = 0; i < 4; i++) { const int row = HILL_SQUARES[i][0], col = HILL_SQUARES[i][1]; lights.set(row, col, hillColor(row, col)); } } bool kingOnHill(char color, int *row, int *col) { const char king = color == 'w' ? 'K' : 'k'; for (int i = 0; i < 4; i++) { const int r = HILL_SQUARES[i][0], c = HILL_SQUARES[i][1]; if (pos.at(r, c) == king) { if (row) *row = r; if (col) *col = c; return true; } } return false; } void celebrateHill(int row, int col, Color winner); void gameOverScreen(Color color); // ── 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++) { if (chess::START_LAYOUT[row][col] != ' ' && !board.occupied(row, col)) { lights.set(row, col, WHITE); } else if (isHill(row, col)) { lights.set(row, col, hillColor(row, col)); } else { lights.set(row, col, OFF); } } } lights.show(); wait(80); } pos.reset(); anim::ripple(3, 3, HILL_COLOR, 45); board.sync(); awaitingSetup = false; sound.gameStart(); app.status("playing", "White to move — walk the king to the centre, or mate"); sendPosition(); showTurn(); } // Lights every piece of the side to move, then the hill on top so the four // centre squares stay visible even when a piece is sitting on one of them. 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); } } paintHill(); 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, options[i].promotion ? PROMO_COLOR : isHill(tr, tc) && chess::kindOf(pos.at(fromRow, fromCol)) == 'K' ? HILL_WIN_COLOR : 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); } } showHints(fromRow, fromCol, options, count); wait(20); } } void showHints(int fromRow, int fromCol, Move *options, int count) { const char piece = pos.at(fromRow, fromCol); lights.clear(); paintHill(); lights.set(fromRow, fromCol, WHITE); for (int i = 0; i < count; i++) { Color color = HINT_COLOR; if (chess::kindOf(piece) == 'K' && isHill(options[i].tr, options[i].tc)) { color = HILL_WIN_COLOR; } else if (options[i].promotion) { color = PROMO_COLOR; } else if (options[i].isCapture()) { color = CAPTURE_COLOR; } lights.set(options[i].tr, options[i].tc, 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(); } // Hill, check, checkmate, stalemate — and the status line for the Connect page. void afterMove() { const char mover = chess::other(pos.turn); int kr = 0, kc = 0; if (kingOnHill(mover, &kr, &kc)) { const bool whiteWon = mover == 'w'; app.result("king-of-the-hill", whiteWon ? "w" : "b"); app.status("over", whiteWon ? "King of the Hill - White wins" : "King of the Hill - Black wins"); celebrateHill(kr, kc, whiteWon ? WHITE_WINS : BLACK_WINS); awaitingSetup = true; return; } 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(); } void celebrateHill(int row, int col, Color winner) { for (int i = 0; i < 5; i++) { lights.clear(); paintHill(); lights.set(row, col, HILL_WIN_COLOR); lights.show(); wait(140); lights.clear(); lights.show(); wait(90); } anim::ripple(row, col, HILL_COLOR, 40); gameOverScreen(winner); } // Holds a colour on every occupied square, with the hill still lit, 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); paintHill(); lights.show(); while (!buttons.anyPressed()) { service(); paintHill(); lights.show(); wait(40); } 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"); }