# UI Move List and Playback Mode Plan ## Goal Add UI support for: - A fixed-size move list panel on the left side of the board. - Two move columns: White move and Black move, with turn number on the left. - Standard PGN/SAN notation display. - Scroll support when the move list is longer than the visible panel. - A move-list button that copies the current game as PGN to the clipboard. - A playback-only vertical analysis bar between the board and move list. - A board-orientation control that can render from White's perspective or Black's perspective. - A new playback mode where pieces cannot be moved, and the user can step through a loaded game. - In edit mode, a UI control to choose whose move is next. - Mode-specific paste behavior: - Edit mode: `Ctrl+V` pastes FEN. - Playback mode: `Ctrl+V` pastes PGN. - Play mode: no FEN/PGN paste. ## Collaboration rule The assistant should manage only UI/application-side changes for this feature. Do **not** edit files under `src/chess/` unless explicitly requested. When UI work needs chess-layer functionality, stop and describe the required chess API/data shape so the project owner can implement it. ## Current state - `BoardState` owns the current chess position. - `src/chess/game.zig` has a basic `Game` framework with: - `initial_state` - `state` - `moves` - `time_control` - `clocks` - `status` - UI currently uses `current_state` directly in `src/main.zig`. - Existing modes are: - `play` - `edit` - Existing paste behavior parses clipboard as FEN regardless of mode. - FEN copy is available via `Ctrl+C`. - Desired copy behavior: `Ctrl+C` should continue to copy FEN in every mode. PGN copy should use an explicit move-list button, not replace `Ctrl+C`. ## Desired architecture Eventually, UI should treat `Game` as the game/session model: ```text UI/App ├── current mode: play | edit | playback ├── active Game ├── playback cursor / ply index ├── move list scroll offset ├── board orientation: white perspective | black perspective ├── analysis display state for playback mode └── rendering state ``` The chess layer should provide enough data for the UI to display moves and reconstruct positions, but the UI should not implement chess notation rules itself. ## Stage 0: Board orientation and edit-side-to-move controls ### UI work - Add app/UI state for board orientation: ```text white perspective: current rendering black perspective: visually flipped board, with White pieces/ranks at the top and Black pieces/ranks at the bottom ``` - This must be a purely visual transform. - Keep the same chess square numbering and logical coordinates: - `a1` is still square `0` - White still starts on ranks 1 and 2 logically - Black still starts on ranks 7 and 8 logically - Update all board visual mappings consistently: - square rendering - piece rendering - coordinate labels - mouse hit-testing - hover/selection/check/checkmate highlights - valid/legal move dots - dragging source/target display - promotion popup placement - Add a UI control to toggle orientation, probably near the mode buttons or move list panel. - In edit mode, add a control to choose whose move is next: - White to move - Black to move - Changing side-to-move in edit mode should update only the current board state's active color/turn field and then rebuild rendering. - In non-edit modes, the side-to-move selector should either be hidden or disabled. ### Chess-layer needs No chess-rule changes are required for board orientation. It is visual only. For edit-side-to-move, the UI needs mutable access to the current position's active color. If `BoardState.turn` remains public, no new chess API is required. If you later make it private, add a small setter such as: ```zig pub fn setTurn(self: *BoardState, color: piece.Color) void ``` ## Stage 1: Add playback mode UI shell ### UI work - Add `playback` to the app/input mode enum. - Add a third mode button, likely labeled `VIEW` or `PGN`/`REPLAY`. - In playback mode: - ignore board clicks/drags for moving pieces - no palette editing - no legal-move highlight generation - Keep reset behavior available or decide whether reset resets the active game/playback. ### Chess-layer needs None. ## Stage 2: Make paste mode-specific ### UI work Change clipboard paste behavior: - If mode is `.edit`: - parse clipboard as FEN - replace current board state/game position - If mode is `.playback`: - send clipboard text to a future PGN import function - for now, log that PGN paste is not yet implemented if chess API is missing - If mode is `.play`: - ignore paste or log debug message ### Chess-layer needs Eventually needed: ```zig pub fn loadPgn(allocator: std.mem.Allocator, pgn_text: []const u8) !Game ``` or equivalent parse API. For stage 2, this can be stubbed from the UI side with no chess changes. ## Stage 3: Move list panel with placeholder/static data ### UI work - Define a fixed rectangle left of the board. - Render panel background and border. - Render column headers, for example: ```text # White Black ``` - Add a visible PGN copy button inside or attached to the move list panel. - Clicking it should copy the current game PGN to the clipboard once chess-layer PGN export exists. - Until PGN export exists, the button can be visually present but log that export is not implemented. - Render rows using temporary/static strings or currently available move placeholders. - Establish layout constants: - panel left/right/top/bottom - row height - turn-number column x - white column x - black column x - max visible rows ### Chess-layer needs None for placeholder rendering. ## Stage 4: Record/display move notation from played games ### UI work - Replace direct `current_state.move(...)` calls with `game.makeMove(...)` once `Game` is wired into `main.zig`. - Read `game.moves` and draw move rows. - Update the move list after every played move. ### Chess-layer needs The `Game` move history should expose display notation for each ply. Suggested shape: ```zig pub const MoveRecord = struct { from: bitboard.Square, to: bitboard.Square, promotion: ?piece.PieceType, san: []const u8, // or fixed buffer / owned slice time_remaining: u32, }; ``` If avoiding heap allocation in each move record, possible alternatives: ```zig san: [16]u8, san_len: u8, ``` The chess layer should generate SAN before mutating the board, because SAN depends on the pre-move position. Needed API could be: ```zig pub fn makeMove(self: *Game, allocator: std.mem.Allocator, from: Square, to: Square, promotion: ?PieceType) !void ``` or: ```zig pub fn formatSanMove(allocator: std.mem.Allocator, state_before: BoardState, from: Square, to: Square, promotion: ?PieceType) ![]u8 ``` ## Stage 5: Playback analysis bar ### UI work - Add a vertical analysis bar between the board and the move list panel. - The analysis bar should be visible only in playback mode. - The bar is divided into a White section and a Black section. - When analysis is equal, the White/Black divide should be centered. - When White is winning, the White portion grows and the divide moves toward Black's side. - When Black is winning, the Black portion grows and the divide moves toward White's side. - The bar is purely display; it should not affect board state or move legality. - Initial placeholder behavior can render the bar at 50/50 until analysis data exists. - Later, map engine evaluation scores to a display fraction. Suggested visual mapping: ```text score = 0.00 pawns -> 50% White / 50% Black score > 0, White better -> White section larger score < 0, Black better -> Black section larger ``` Use a bounded/nonlinear mapping so very large evaluations do not immediately consume the whole bar. For example, later UI code could map centipawns to a normalized value with a clamp or sigmoid-like curve. Exact mapping can be tuned visually. ### Chess/engine-layer needs No chess-layer changes are needed for placeholder rendering. Eventually, playback analysis needs external UCI analysis data: ```text score cp score mate pv ``` The UI only needs a normalized analysis value or raw score from the UCI analysis client. ## Stage 6: Scrolling move list ### UI work - Track `move_list_scroll_row` or similar state in `main.zig`. - Add mouse wheel handling when cursor is over the move panel. - Clamp scroll offset between `0` and `max(0, total_rows - visible_rows)`. - Optional later: visual scrollbar. ### Chess-layer needs None. ## Stage 7: Playback cursor and stepping ### UI work - Add `playback_ply_index` app state. - In playback mode: - Right arrow: advance one ply - Left arrow: go back one ply - Home: first position - End: final position - Render the board for the current playback ply, not necessarily the final game state. - Highlight or mark the current row/ply in the move list. ### Chess-layer needs Need a way to reconstruct board state at an arbitrary ply. Possible API: ```zig pub fn stateAtPly(self: *const Game, ply_index: usize) board.BoardState ``` or, if allocation/errors are involved: ```zig pub fn replayToPly(self: *const Game, ply_index: usize) !board.BoardState ``` This can replay from `initial_state` through the first `ply_index` moves. ## Stage 8: PGN paste/import and PGN copy/export ### UI work - In playback mode, `Ctrl+V` gets clipboard text and passes it to chess PGN parser. - Keep `Ctrl+C` as FEN copy in every mode. - Add click handling for the move-list PGN copy button. - When the PGN copy button is clicked, copy exported PGN to the clipboard. - On success: - replace active game with parsed game - set mode to playback or remain playback - set playback cursor to final ply or start; choose behavior intentionally - reset move-list scroll - On failure: - log a warning - keep current game unchanged ### Chess-layer needs PGN parser that returns a `Game` with move history and final state, plus PGN export for the current game. Suggested API: ```zig pub fn initFromPgn(allocator: std.mem.Allocator, pgn_text: []const u8, time_control: TimeControl) !Game ``` or: ```zig pub fn parsePgn(allocator: std.mem.Allocator, pgn_text: []const u8) !Game ``` Suggested export API: ```zig pub fn formatPgn(allocator: std.mem.Allocator, game: Game) ![]u8 ``` or as a method: ```zig pub fn formatPgn(self: *const Game, allocator: std.mem.Allocator) ![]u8 ``` Parser should understand at least: - move numbers: `1.`, `1...` - SAN moves - castling: `O-O`, `O-O-O` - captures: `x` - promotion: `=Q`, `=R`, `=B`, `=N` - check/checkmate suffixes: `+`, `#` - game termination markers: `1-0`, `0-1`, `1/2-1/2`, `*` - comments and tags can be added later if desired Reference: Steven J. Edwards, *Portable Game Notation Specification and Implementation Guide*, especially movetext/SAN sections. ## Stage 9: Optional polish - Auto-scroll move list to latest move in play mode. - Click a move row in playback mode to jump to that ply. - Show current move highlight. - Add scrollbar. - Show numeric eval next to the analysis bar. - Show best line/PV next to or below the move list. - Add richer PGN copy/export options, such as including/excluding tag pairs. - Display game result in the move panel. - Show time remaining per move later if clocks are implemented. ## Immediate next action Start with Stage 0, then Stage 1 and Stage 2 in UI-only files: - `src/board_input.zig` - `src/text_render.zig` - `src/main.zig` Do not modify `src/chess/` for these stages.