Skip to content

Phase 2 + 3: full chess rules + UX polish for RP2040 (with self-tests)#11

Open
semichcsc-byte wants to merge 3 commits into
Concept-Bytes:mainfrom
semichcsc-byte:feat/rp2040-rules-and-ux
Open

Phase 2 + 3: full chess rules + UX polish for RP2040 (with self-tests)#11
semichcsc-byte wants to merge 3 commits into
Concept-Bytes:mainfrom
semichcsc-byte:feat/rp2040-rules-and-ux

Conversation

@semichcsc-byte
Copy link
Copy Markdown

Summary

Substantial gameplay upgrade for the official Concept-Bytes firmware on Arduino Nano RP2040 Connect: full chess rules (check, checkmate, castling, en-passant, draws, promotion choice), sensor debounce, AP shutdown, and a 10-test on-boot validation suite. +1107 / -282 lines, 9 files, 10/10 self-tests passing.

Builds on top of:

This PR depends on those landing first (or being squashed in together).

What changes for the user

Before After
Pinned pieces could move and expose own king Legal-move filter rejects them
No check / checkmate / stalemate display King blinks red on check; full-board win/draw animations
No castling Castling works (kingside + queenside, FIDE legal)
No en-passant EP target highlighted pink, capture removes the right pawn
Pawn always auto-promoted to queen Human vs Human: 4 LEDs let you pick Q/R/B/N. Bot mode: still auto-Q (Stockfish 'e7e8q' parsing TBD)
50-move / insufficient material never end the game Auto-detected, draw animation plays
Sensor flicker when sliding pieces Debounce: 3 consecutive scans required to flip state
AP OpenChessBoard stays up forever Shut down when entering Chess Moves / Sensor Test (~100 mA saved)
No way to know if firmware is broken 10 self-tests at boot, red flash on failure

Engine architecture

New ChessEngine API, with board[] mutation centralised in applyMove so castling / EP / state updates can never be missed:

struct GameState { castling rights; enPassant{Row,Col}; halfMoveClock; lastMove; };

void  ChessEngine::getLegalMoves(board, state, row, col, &count, moves);
bool  ChessEngine::isLegalMove(board, state, fromR, fromC, toR, toC);
bool  ChessEngine::isInCheck(board, color);
bool  ChessEngine::isSquareAttacked(board, row, col, byColor);
bool  ChessEngine::hasAnyLegalMove(board, state, color);
GameResult ChessEngine::getGameResult(board, state, colorToMove);
void  ChessEngine::applyMove(board, state, fromR, fromC, toR, toC, promoteTo=0);
int   ChessEngine::runSelfTests();

getPossibleMoves is kept as the pseudo-legal generator (no own-check filter); it's still used internally by isSquareAttacked and getLegalMoves.

Self-tests (run at every boot)

=== ChessEngine self-tests ===
PASS T1: e2 pawn has 2 legal moves
PASS T2: b1 knight has 2 legal moves
PASS T3: no check at start
PASS T4: Fool's Mate detected
PASS T5: pinned rook stayed on file
PASS T6: both castlings legal
PASS T7: no castling in check
PASS T8: en-passant offered
PASS T9: K vs K is draw
PASS T10: kingside castle layout correct
=== Self-tests complete: 10/10 passed ===

These caught one real bug during development (a wrong test position that turned out to be a wrong test position rather than an engine bug — but the same pattern would catch a real regression).

Sensor debounce

Each readSensors() call updates a per-square "stable count". A reading must repeat for 3 consecutive scans before the public state flips. Tunable via SENSOR_DEBOUNCE_SCANS in board_driver.h.

This was visible in serial as flicker (q appearing/disappearing on d8 between scans during board setup) — gone now.

AP shutdown

When entering Chess Moves or Sensor Test, WiFi.end() is called to release the radio. The OpenChessBoard AP was drawing ~100 mA continuously and creating a persistent WiFi network nobody uses once a mode is selected. Bot mode is untouched because PR #9 already does the AP teardown internally.

Promotion choice (Human vs Human)

When a pawn reaches the back rank, four selector LEDs light up on the player's own back rank with distinct colours:

  • a-file = Q (gold)
  • b-file = R (red)
  • c-file = B (blue)
  • d-file = N (white)

Place any piece on one to choose. The selector then waits for you to lift the piece off the menu before re-arming the move detector.

Bot mode still auto-promotes to queen — the Stockfish API string e7e8q includes the promotion piece in its 5th character, but the existing parseMove only grabs the first 4 chars. That's a small follow-up, easy to bolt on but kept out of scope here to keep PR size manageable.

Verification

Sketch uses 150679 bytes (0%) of program storage space.
Global variables use 44640 bytes (16%) of dynamic memory.
10/10 self-tests passed.

Tested on Arduino Nano RP2040 Connect with WiFiNINA firmware 3.0.1:

  • Boot clean, self-tests pass before WiFi setup
  • Game selection works as before
  • Chess Moves Human-vs-Human plays through opening + check + castling
  • Chess Bot Human-vs-AI: WiFi connects via Fix bot mode hang on WiFi connection (closes #5) #9 patch, Stockfish responds, validated bot moves applied via applyMove
  • Sensor flicker on board setup is gone

Trade-offs

  • Sketch +5963 bytes vs the PR RP2040 quality pass: 5 correctness/robustness fixes #10 baseline (still 0% of the 16 MB flash) and +200 bytes RAM for GameState instances and debounce buffers
  • The on-boot animation on a self-test failure is intentionally noisy — it's better than silently corrupting a game
  • Promotion choice in bot mode left as a follow-up (5-char move parsing)
  • 3-fold repetition not yet implemented (would need position hashing + history; bigger memory hit)

1) stockfish_settings.h: medium() depth 6 -> 10
   Bug: serial said 'Medium (Depth 10)' but the struct sent depth=6,
   making Easy and Medium identical. Aligns with the documented intent.

2) chess_bot.cpp::parseStockfishResponse: split HTTP headers from body
   before scanning for JSON. Cloudflare's response includes JSON-shaped
   headers (Nel:, Report-To:) that come before the real body, so the
   previous indexOf("{") grabbed the wrong object. Symptom: 'API request
   was not successful' on a successful response. Fix: locate the body
   after the first blank line (\\r\\n\\r\\n) and parse from there.

3) chess_bot.cpp::makeBotMove: validate the API's move locally before
   mutating board state. Protects against malformed JSON, partial reads,
   side-of-turn mismatches and any future API quirk. On rejection the
   board returns to the player's turn instead of corrupting state.
   Also reset isWhiteTurn on parse/response failure paths so the user
   isn't stuck.

4) OpenChess.ino MODE_GAME_3: replace blind 3s loop with a wait-until-
   piece-lifted gate. Old behaviour spammed the serial at ~3 lines/sec
   forever while a piece sat on the placeholder square (4,3) since
   handleGameSelection re-triggered the same branch every cycle. Now we
   wait for the sensor to clear before re-arming the menu.

5) chess_engine.h: introduce MAX_MOVES_PER_PIECE constant (=28) and use
   it everywhere. Was inconsistent: chess_moves used moves[28], chess_bot
   used moves[27]. Eliminates the off-by-one buffer-overflow risk for
   queens reaching 27 candidates.

All five are RP2040-friendly and apply equally to other WiFiNINA boards
(Nano 33 IoT, MKR WiFi 1010). Verified by recompiling and reflashing
the official OpenChess.ino on an Arduino Nano RP2040 Connect; bot mode
still connects, parses Stockfish replies and plays correctly.
This is a substantial gameplay upgrade for the official Concept-Bytes
firmware path on the Arduino Nano RP2040 Connect. All changes verified
by 10/10 self-tests run at every boot, plus an on-device smoke test.

== Phase 2: chess rules ==

* Check detection: new ChessEngine::isInCheck and isSquareAttacked.
* Legal-move filtering: getLegalMoves wraps getPossibleMoves and
  removes any move that would leave the player's king in check
  (so pinned pieces, illegal king walks, etc. are no longer offered).
* Checkmate / Stalemate detection via getGameResult, with on-board
  game-over animations (white wins: full-board white; black wins:
  red pulse; draw: fireworks).
* Castling: kingside and queenside, full FIDE rules (king and rook
  on home squares, path clear, king not in check, king does not pass
  through attacked square). Castling rights are tracked in the new
  GameState struct and updated automatically by applyMove (also
  cleared if the rook is captured on its starting square).
* En passant: tracked via GameState::enPassantRow/Col, set on a
  2-square pawn advance and cleared otherwise. The capturing pawn's
  destination is shown pink on the LED hint, and applyMove removes
  the captured pawn from its actual square (one rank back from EP
  target).
* 50-move rule via halfMoveClock, reset on pawn move or capture.
* Insufficient material draw: K vs K, and K + single minor piece
  (B or N) vs K.
* Promotion choice: when a pawn reaches the back rank in Human-vs-
  Human mode, four selector LEDs light up on the player's back rank
  (a/b/c/d files = Q/R/B/N). Place any piece on one to choose. Bot
  mode still auto-promotes to queen since the Stockfish API string
  isn't parsed for the optional 5th promotion char yet.

== Phase 3: UX polish ==

* Sensor debounce: a reading must repeat for SENSOR_DEBOUNCE_SCANS=3
  consecutive scans before flipping the public sensor state. Eliminates
  the 'piece flicker' false-positives we saw in serial when sliding
  pieces across squares during board setup.
* AP shutdown after non-bot mode: WiFi.end() is called when entering
  Chess Moves / Sensor Test, dropping the OpenChessBoard AP that was
  drawing ~100 mA and creating an unused WiFi network. Bot mode is
  untouched (it tears down + reopens internally per PR Concept-Bytes#9).
* Self-tests: ChessEngine::runSelfTests runs at boot before WiFi setup
  and prints PASS/FAIL for 10 deterministic positions (initial moves,
  Fool's Mate, pinned rook, both castlings, castle-in-check forbidden,
  en-passant, K-vs-K, kingside castle layout). On any failure the board
  flashes red 5x so the user knows the firmware is broken before they
  start a game.

== Engine API ==

* New: GameState struct, getLegalMoves, isLegalMove, isInCheck,
  isSquareAttacked, hasAnyLegalMove, getGameResult, applyMove,
  isCastlingMove, isEnPassantMove, runSelfTests.
* getPieceColor moved to public.
* All callers in chess_moves.cpp and chess_bot.cpp now go through
  the new API; board mutation lives in ChessEngine::applyMove so
  castling/EP/state updates can never be forgotten.

== Verification ==

  Sketch uses 150679 bytes (0%) of program storage space.
  Global variables use 44640 bytes (16%) of dynamic memory.
  10/10 self-tests passed.

Tested on Arduino Nano RP2040 Connect with WiFiNINA firmware 3.0.1.
Boot clean, mode selection works, chess_moves Human-vs-Human plays
through opening + showcheck + castling correctly, chess_bot Human-vs-
AI still connects via PR Concept-Bytes#9 and applies validated moves.

Builds on PR Concept-Bytes#9 (WiFi AP->STA) and PR Concept-Bytes#10 (5 quality fixes).
Adds OPENCHESS_FW_VERSION '1.0.0-rp2040' macro and prints it (plus the
fork name) in the startup banner. Tagged v1.0.0-rp2040 to match.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant