Add game framework tests and planning docs
This commit is contained in:
parent
145bc948dd
commit
a5476ccdd7
448
docs/uci-engine-architecture-plan.md
Normal file
448
docs/uci-engine-architecture-plan.md
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
# UCI Engine Architecture Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Support two families of executables:
|
||||||
|
|
||||||
|
1. **GUI/UI executable**
|
||||||
|
- Renders and tracks games.
|
||||||
|
- Lets humans play/edit/replay games.
|
||||||
|
- Can let people play against bots.
|
||||||
|
- Can spawn bot/engine executables as subprocesses on demand.
|
||||||
|
- Speaks UCI to engine subprocesses.
|
||||||
|
- Can optionally use a user-selected external UCI engine, such as Stockfish, for playback analysis.
|
||||||
|
- Eventually stores games in a database.
|
||||||
|
|
||||||
|
2. **Bot/engine executables**
|
||||||
|
- No GUI.
|
||||||
|
- Implement chess engine logic.
|
||||||
|
- Speak UCI over stdin/stdout.
|
||||||
|
- Can be launched by the GUI.
|
||||||
|
- Can also run as long-lived processes connected to external services such as lichess.org through a service adapter.
|
||||||
|
|
||||||
|
## Important terminology
|
||||||
|
|
||||||
|
In UCI terminology, the **engine** is the side that implements the UCI command loop over stdin/stdout.
|
||||||
|
|
||||||
|
The GUI/app is the **UCI controller/client**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GUI/controller
|
||||||
|
├── starts engine process
|
||||||
|
├── writes UCI commands to engine stdin
|
||||||
|
├── reads UCI responses from engine stdout
|
||||||
|
└── applies bestmove to the current game
|
||||||
|
```
|
||||||
|
|
||||||
|
The engine executable is the **UCI engine/server-like process**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Engine
|
||||||
|
├── reads commands from stdin
|
||||||
|
├── maintains/searches position
|
||||||
|
├── writes info/bestmove responses to stdout
|
||||||
|
└── exits on quit
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid naming the GUI-side wrapper a "server" in code. Prefer names like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UciEngineClient
|
||||||
|
UciController
|
||||||
|
EngineProcess
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desired long-term process architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ zig-chess-gui │
|
||||||
|
│ │
|
||||||
|
│ - Vulkan UI │
|
||||||
|
│ - game tracking │
|
||||||
|
│ - human input │
|
||||||
|
│ - playback │
|
||||||
|
│ - database later │
|
||||||
|
│ - UCI client │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ UCI stdin/stdout
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ zig-chess-engine-* │
|
||||||
|
│ │
|
||||||
|
│ - evaluation │
|
||||||
|
│ - search │
|
||||||
|
│ - move selection │
|
||||||
|
│ - UCI command loop │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Future external-service architecture:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ lichess-adapter │
|
||||||
|
│ │
|
||||||
|
│ - lichess API │
|
||||||
|
│ - account/game I/O │
|
||||||
|
│ - UCI client │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ UCI stdin/stdout or socket/process bridge
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ zig-chess-engine-* │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Key design principle:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GUI ───────────────┐
|
||||||
|
├── UCI ── engine executable
|
||||||
|
Lichess adapter ───┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The engine should not know whether it is being used by the GUI, a test harness, or a lichess adapter.
|
||||||
|
|
||||||
|
## Suggested source layout
|
||||||
|
|
||||||
|
Possible future layout:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
├── chess/ // board/game/FEN/PGN/legal moves
|
||||||
|
├── main.zig // current GUI entry point, or later src/gui/main.zig
|
||||||
|
├── uci/
|
||||||
|
│ ├── protocol.zig // shared UCI parse/format types
|
||||||
|
│ ├── client.zig // GUI/service side: controls engine process
|
||||||
|
│ └── server.zig // engine side: stdin/stdout command loop
|
||||||
|
├── engine/
|
||||||
|
│ ├── engine.zig // common engine interface
|
||||||
|
│ ├── eval.zig // static evaluation
|
||||||
|
│ ├── search.zig // search algorithms
|
||||||
|
│ └── perft.zig // move-generator validation tooling
|
||||||
|
└── bots/
|
||||||
|
├── random.zig // random/legal-move bot executable
|
||||||
|
├── material.zig // material-eval bot executable
|
||||||
|
└── search.zig // stronger search bot executable
|
||||||
|
```
|
||||||
|
|
||||||
|
This layout can evolve. The important boundary is that engine executables communicate with outside controllers through UCI.
|
||||||
|
|
||||||
|
## Build targets
|
||||||
|
|
||||||
|
Eventually `build.zig` should produce multiple executables, for example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
zig-chess-gui
|
||||||
|
zig-chess-random-bot
|
||||||
|
zig-chess-material-bot
|
||||||
|
zig-chess-search-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
The GUI target links rendering/UI code.
|
||||||
|
|
||||||
|
The engine targets should avoid linking Vulkan/windowing code.
|
||||||
|
|
||||||
|
## Analysis engine policy
|
||||||
|
|
||||||
|
The GUI should support an optional external UCI engine for real-time analysis during game playback.
|
||||||
|
|
||||||
|
Primary intended use case:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Playback mode
|
||||||
|
├── user steps through game
|
||||||
|
├── GUI sends current position to analysis engine
|
||||||
|
├── analysis engine returns eval/PV/best line
|
||||||
|
└── GUI displays analysis to the user
|
||||||
|
```
|
||||||
|
|
||||||
|
This analysis engine is separate from project bot engines:
|
||||||
|
|
||||||
|
- It is for user-facing analysis only.
|
||||||
|
- It must not be used by project bot engines as their search/evaluation implementation.
|
||||||
|
- Project bots should have their own evaluation/search logic.
|
||||||
|
- The GUI should treat the analysis engine as just another external UCI process.
|
||||||
|
|
||||||
|
### Stockfish licensing policy
|
||||||
|
|
||||||
|
Stockfish is GPL-3.0 licensed. Because this project is intended to be MIT licensed, do **not** bundle Stockfish directly into the executable or source distribution by default.
|
||||||
|
|
||||||
|
Preferred approaches:
|
||||||
|
|
||||||
|
1. **User-provided engine path**
|
||||||
|
- User installs Stockfish or another UCI engine separately.
|
||||||
|
- GUI stores/configures the path.
|
||||||
|
- GUI launches it as an external process.
|
||||||
|
|
||||||
|
2. **Optional download flow**
|
||||||
|
- GUI can offer to download Stockfish or guide the user through downloading it.
|
||||||
|
- The download should be explicit and optional.
|
||||||
|
- The UI should clearly identify the engine and its license before download/use.
|
||||||
|
- Keep downloaded engine files outside the MIT-licensed source tree/release bundle unless licensing obligations are intentionally handled.
|
||||||
|
|
||||||
|
3. **Generic UCI analysis engine support**
|
||||||
|
- Do not hard-code Stockfish as the only option.
|
||||||
|
- Any compatible UCI engine path should work.
|
||||||
|
|
||||||
|
### Analysis engine UI/config needs
|
||||||
|
|
||||||
|
Eventually the GUI should provide:
|
||||||
|
|
||||||
|
- analysis engine executable path
|
||||||
|
- enable/disable analysis
|
||||||
|
- analysis depth or movetime
|
||||||
|
- optional thread/hash settings via UCI options
|
||||||
|
- current eval display
|
||||||
|
- principal variation display
|
||||||
|
- best move display
|
||||||
|
- start/stop analysis when stepping through playback
|
||||||
|
|
||||||
|
Suggested app-side abstraction:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AnalysisEngine
|
||||||
|
├── engine_process / UciEngineClient
|
||||||
|
├── executable_path
|
||||||
|
├── enabled
|
||||||
|
├── depth or movetime limit
|
||||||
|
├── latest_score
|
||||||
|
├── latest_pv
|
||||||
|
└── latest_bestmove
|
||||||
|
```
|
||||||
|
|
||||||
|
## UCI protocol responsibilities
|
||||||
|
|
||||||
|
### GUI/client side
|
||||||
|
|
||||||
|
The GUI should be able to:
|
||||||
|
|
||||||
|
- Spawn an engine executable.
|
||||||
|
- Send UCI commands.
|
||||||
|
- Read engine output asynchronously or incrementally.
|
||||||
|
- Track engine readiness.
|
||||||
|
- Track available engine options.
|
||||||
|
- Send current game position.
|
||||||
|
- Request a move/search.
|
||||||
|
- Stop a search.
|
||||||
|
- Shut down the engine process cleanly.
|
||||||
|
|
||||||
|
Common commands sent by GUI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
uci
|
||||||
|
isready
|
||||||
|
ucinewgame
|
||||||
|
position startpos
|
||||||
|
position startpos moves e2e4 e7e5
|
||||||
|
position fen <fen fields> moves ...
|
||||||
|
go depth 5
|
||||||
|
go movetime 1000
|
||||||
|
stop
|
||||||
|
quit
|
||||||
|
```
|
||||||
|
|
||||||
|
Common responses parsed by GUI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
id name <name>
|
||||||
|
id author <author>
|
||||||
|
option name <name> type <type> ...
|
||||||
|
uciok
|
||||||
|
readyok
|
||||||
|
info depth 3 score cp 20 nodes 1234 time 10 pv e2e4 e7e5
|
||||||
|
bestmove e2e4
|
||||||
|
bestmove e2e4 ponder e7e5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Engine/server side
|
||||||
|
|
||||||
|
Each engine executable should:
|
||||||
|
|
||||||
|
- Read one line at a time from stdin.
|
||||||
|
- Parse UCI commands.
|
||||||
|
- Maintain an internal position.
|
||||||
|
- Search when given `go`.
|
||||||
|
- Print `bestmove ...` when done.
|
||||||
|
- Print `info ...` optionally during search.
|
||||||
|
- Respond to `stop` promptly.
|
||||||
|
- Exit on `quit`.
|
||||||
|
|
||||||
|
## First milestone: minimal UCI engine executable
|
||||||
|
|
||||||
|
Implement an engine executable that supports only:
|
||||||
|
|
||||||
|
Input:
|
||||||
|
|
||||||
|
```text
|
||||||
|
uci
|
||||||
|
isready
|
||||||
|
quit
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
id name ZigChess Random Bot
|
||||||
|
id author WayfinderAK
|
||||||
|
uciok
|
||||||
|
readyok
|
||||||
|
```
|
||||||
|
|
||||||
|
No search or move generation required yet.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Prove executable separation.
|
||||||
|
- Prove stdin/stdout loop.
|
||||||
|
- Prove GUI/test harness can launch and communicate with engine.
|
||||||
|
|
||||||
|
## Second milestone: GUI-side engine process wrapper
|
||||||
|
|
||||||
|
Implement a UI/application-side wrapper that can:
|
||||||
|
|
||||||
|
- Start an engine executable by path.
|
||||||
|
- Send `uci`.
|
||||||
|
- Wait for `uciok`.
|
||||||
|
- Send `isready`.
|
||||||
|
- Wait for `readyok`.
|
||||||
|
- Send `quit` on shutdown.
|
||||||
|
|
||||||
|
Suggested name:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
UciEngineClient
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
EngineProcess
|
||||||
|
```
|
||||||
|
|
||||||
|
This code belongs on the GUI/controller side, not inside the engine bot.
|
||||||
|
|
||||||
|
## Third milestone: position and bestmove
|
||||||
|
|
||||||
|
Add UCI support for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
position startpos
|
||||||
|
position startpos moves e2e4 e7e5
|
||||||
|
position fen <fen> moves ...
|
||||||
|
go depth 1
|
||||||
|
bestmove <move>
|
||||||
|
```
|
||||||
|
|
||||||
|
For the first engine, `go depth 1` can simply pick the first legal move or a random legal move.
|
||||||
|
|
||||||
|
Move format is UCI long algebraic coordinate format:
|
||||||
|
|
||||||
|
```text
|
||||||
|
e2e4
|
||||||
|
e7e8q
|
||||||
|
e1g1
|
||||||
|
```
|
||||||
|
|
||||||
|
This is different from SAN/PGN notation.
|
||||||
|
|
||||||
|
## Fourth milestone: reusable protocol module
|
||||||
|
|
||||||
|
Create shared parse/format helpers for UCI text:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/uci/protocol.zig
|
||||||
|
```
|
||||||
|
|
||||||
|
Potential responsibilities:
|
||||||
|
|
||||||
|
- Parse GUI-to-engine commands.
|
||||||
|
- Parse engine-to-GUI responses.
|
||||||
|
- Format moves as UCI coordinate moves.
|
||||||
|
- Parse UCI coordinate moves into from/to/promotion.
|
||||||
|
- Format `position ...` commands from a `Game` move list.
|
||||||
|
|
||||||
|
Keep protocol parsing separate from subprocess management and separate from search/evaluation.
|
||||||
|
|
||||||
|
## Fifth milestone: stronger engines
|
||||||
|
|
||||||
|
Once the protocol boundary works:
|
||||||
|
|
||||||
|
- random legal move engine
|
||||||
|
- material evaluation engine
|
||||||
|
- shallow search engine
|
||||||
|
- alpha-beta search engine
|
||||||
|
- improved move ordering
|
||||||
|
- transposition table
|
||||||
|
- time management
|
||||||
|
|
||||||
|
Each stronger engine can be its own executable or selected by options/config.
|
||||||
|
|
||||||
|
## Future lichess adapter
|
||||||
|
|
||||||
|
Do not put lichess-specific code inside the engine.
|
||||||
|
|
||||||
|
Instead, later create a separate adapter/service that:
|
||||||
|
|
||||||
|
- talks to lichess.org APIs
|
||||||
|
- manages authentication/account events
|
||||||
|
- receives games/moves from lichess
|
||||||
|
- launches or connects to an engine executable
|
||||||
|
- sends positions/search commands via UCI
|
||||||
|
- sends engine moves back to lichess
|
||||||
|
|
||||||
|
This keeps engines reusable by:
|
||||||
|
|
||||||
|
- GUI
|
||||||
|
- lichess adapter
|
||||||
|
- local match runner
|
||||||
|
- tournament harness
|
||||||
|
- tests/benchmarks
|
||||||
|
|
||||||
|
## Future playback analysis milestone
|
||||||
|
|
||||||
|
After the generic UCI client exists, add analysis support to playback mode:
|
||||||
|
|
||||||
|
1. Let the user configure an external UCI engine path.
|
||||||
|
2. Start that engine as an analysis process.
|
||||||
|
3. Send `uci` / `isready` handshake.
|
||||||
|
4. When playback cursor changes, send:
|
||||||
|
|
||||||
|
```text
|
||||||
|
position fen <current playback FEN>
|
||||||
|
go depth <n>
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```text
|
||||||
|
position fen <current playback FEN>
|
||||||
|
go movetime <ms>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Parse `info` lines for score and PV.
|
||||||
|
6. Display the latest analysis in the UI.
|
||||||
|
7. Send `stop` when the user moves to another ply or disables analysis.
|
||||||
|
8. Send `quit` when closing the app or changing engine path.
|
||||||
|
|
||||||
|
Keep this separate from bot-vs-human play. The analysis engine is an advisor for the user, not the implementation of project bots.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Primary UCI reference:
|
||||||
|
|
||||||
|
- Stefan Meyer-Kahlen, **Universal Chess Interface (UCI)** protocol, April 2006. Commonly distributed as `uci.txt` and mirrored by chess-programming resources. Covers `uci`, `isready`, `position`, `go`, `stop`, `quit`, `info`, `bestmove`, and engine options.
|
||||||
|
|
||||||
|
Stockfish licensing/reference:
|
||||||
|
|
||||||
|
- Official Stockfish repository: https://github.com/official-stockfish/Stockfish
|
||||||
|
- Stockfish `Copying.txt`: GNU General Public License v3.0, https://github.com/official-stockfish/Stockfish/blob/master/Copying.txt
|
||||||
|
- Because the project goal is MIT licensing, Stockfish should be user-provided or optionally downloaded rather than bundled by default.
|
||||||
|
|
||||||
|
Useful related notation distinction:
|
||||||
|
|
||||||
|
- UCI moves are coordinate moves such as `e2e4` or `e7e8q`.
|
||||||
|
- PGN/SAN moves are display/game-record notation such as `e4`, `Nf3`, `O-O`, `Qxf7#`.
|
||||||
|
- The GUI move list should display PGN/SAN.
|
||||||
|
- Engine communication should use UCI coordinate moves.
|
||||||
382
docs/ui-move-list-playback-plan.md
Normal file
382
docs/ui-move-list-playback-plan.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
# 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 <centipawns>
|
||||||
|
score mate <moves>
|
||||||
|
pv <principal variation moves>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
@ -66,6 +66,37 @@ pub const BoardState = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn initStartingPosition() BoardState {
|
||||||
|
return .{
|
||||||
|
.turn = piece.Color.white,
|
||||||
|
.castle_rights = 0b1111,
|
||||||
|
.en_passant = 0,
|
||||||
|
.halfmove = 0,
|
||||||
|
.fullmove = 1,
|
||||||
|
.bitboards = [_]u64{
|
||||||
|
0xFFFF00000000FFFF, // 0: all occupancy
|
||||||
|
|
||||||
|
0x00FF000000000000, // 1: black pawns
|
||||||
|
0x4200000000000000, // 2: black knights
|
||||||
|
0x2400000000000000, // 3: black bishops
|
||||||
|
0x8100000000000000, // 4: black rooks
|
||||||
|
0x0800000000000000, // 5: black queen
|
||||||
|
0x1000000000000000, // 6: black king
|
||||||
|
0xFFFF000000000000, // 7: black occupancy
|
||||||
|
|
||||||
|
0x0000000000000000, // 8: empty
|
||||||
|
|
||||||
|
0x000000000000FF00, // 9: white pawns
|
||||||
|
0x0000000000000042, // 10: white knights
|
||||||
|
0x0000000000000024, // 11: white bishops
|
||||||
|
0x0000000000000081, // 12: white rooks
|
||||||
|
0x0000000000000008, // 13: white queen
|
||||||
|
0x0000000000000010, // 14: white king
|
||||||
|
0x000000000000FFFF, // 15: white occupancy
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getSquare(self: BoardState, square: bitboard.Square) u4 {
|
pub fn getSquare(self: BoardState, square: bitboard.Square) u4 {
|
||||||
const mask = bitboard.bit(square);
|
const mask = bitboard.bit(square);
|
||||||
for (self.bitboards[1..7], 1..) |board, i| {
|
for (self.bitboards[1..7], 1..) |board, i| {
|
||||||
@ -120,7 +151,7 @@ pub const BoardState = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move(self: *BoardState, from_square: bitboard.Square, to_square: bitboard.Square, promotion_type: ?piece.PieceType) !void {
|
pub fn move(self: *BoardState, from_square: bitboard.Square, to_square: bitboard.Square, promotion_type: ?piece.PieceType) void {
|
||||||
const from_file: u3 = @intCast(from_square % 8);
|
const from_file: u3 = @intCast(from_square % 8);
|
||||||
const from_rank: u3 = @intCast(from_square / 8);
|
const from_rank: u3 = @intCast(from_square / 8);
|
||||||
const to_file: u3 = @intCast(to_square % 8);
|
const to_file: u3 = @intCast(to_square % 8);
|
||||||
@ -224,22 +255,22 @@ pub const BoardState = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getValidMoves(self: *BoardState, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalMoves(self: *BoardState, square: bitboard.Square) bitboard.Bitboard {
|
||||||
const p = self.getSquare(square);
|
const p = self.getSquare(square);
|
||||||
return switch (piece.typeOf(p)) {
|
return switch (piece.typeOf(p)) {
|
||||||
piece.PieceType.pawn => self.getValidPawnMoves(p, square),
|
piece.PieceType.pawn => self.getPseudoLegalPawnMoves(p, square),
|
||||||
piece.PieceType.knight => self.getValidKnightMoves(p, square),
|
piece.PieceType.knight => self.getPseudoLegalKnightMoves(p, square),
|
||||||
piece.PieceType.bishop => self.getValidBishopMoves(p, square),
|
piece.PieceType.bishop => self.getPseudoLegalBishopMoves(p, square),
|
||||||
piece.PieceType.rook => self.getValidRookMoves(p, square),
|
piece.PieceType.rook => self.getPseudoLegalRookMoves(p, square),
|
||||||
piece.PieceType.queen => self.getValidQueenMoves(p, square),
|
piece.PieceType.queen => self.getPseudoLegalQueenMoves(p, square),
|
||||||
piece.PieceType.king => self.getValidKingMoves(p, square),
|
piece.PieceType.king => self.getPseudoLegalKingMoves(p, square),
|
||||||
else => 0,
|
else => 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getLegalMoves(self: *BoardState, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getLegalMoves(self: *BoardState, square: bitboard.Square) bitboard.Bitboard {
|
||||||
var legal_moves: bitboard.Bitboard = 0;
|
var legal_moves: bitboard.Bitboard = 0;
|
||||||
var pseudo_moves = self.getValidMoves(square);
|
var pseudo_moves = self.getPseudoLegalMoves(square);
|
||||||
|
|
||||||
const p = self.getSquare(square);
|
const p = self.getSquare(square);
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
@ -306,7 +337,7 @@ pub const BoardState = struct {
|
|||||||
return !self.hasAnyLegalMove(color) and !self.isInCheck(color);
|
return !self.hasAnyLegalMove(color) and !self.isInCheck(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getValidPawnMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalPawnMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
||||||
var valid_moves: bitboard.Bitboard = 0;
|
var valid_moves: bitboard.Bitboard = 0;
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
switch (color) {
|
switch (color) {
|
||||||
@ -347,7 +378,7 @@ pub const BoardState = struct {
|
|||||||
}
|
}
|
||||||
return valid_moves;
|
return valid_moves;
|
||||||
}
|
}
|
||||||
pub fn getValidKnightMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalKnightMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
const friendlies = switch (color) {
|
const friendlies = switch (color) {
|
||||||
.white => self.bitboards[15],
|
.white => self.bitboards[15],
|
||||||
@ -357,7 +388,7 @@ pub const BoardState = struct {
|
|||||||
return attacks;
|
return attacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getValidBishopMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalBishopMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
const friendlies = switch (color) {
|
const friendlies = switch (color) {
|
||||||
.white => self.bitboards[15],
|
.white => self.bitboards[15],
|
||||||
@ -370,7 +401,7 @@ pub const BoardState = struct {
|
|||||||
return legal;
|
return legal;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getValidRookMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalRookMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
const friendlies = switch (color) {
|
const friendlies = switch (color) {
|
||||||
.white => self.bitboards[15],
|
.white => self.bitboards[15],
|
||||||
@ -383,7 +414,7 @@ pub const BoardState = struct {
|
|||||||
return legal;
|
return legal;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getValidQueenMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalQueenMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
const friendlies = switch (color) {
|
const friendlies = switch (color) {
|
||||||
.white => self.bitboards[15],
|
.white => self.bitboards[15],
|
||||||
@ -396,7 +427,7 @@ pub const BoardState = struct {
|
|||||||
return legal;
|
return legal;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getValidKingMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
pub fn getPseudoLegalKingMoves(self: *BoardState, p: u4, square: bitboard.Square) bitboard.Bitboard {
|
||||||
const color = piece.colorOf(p) orelse return 0;
|
const color = piece.colorOf(p) orelse return 0;
|
||||||
const friendlies = if (color == .white)
|
const friendlies = if (color == .white)
|
||||||
self.bitboards[15]
|
self.bitboards[15]
|
||||||
@ -665,11 +696,11 @@ test "move toggles turn and increments fullmove after black move" {
|
|||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
||||||
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(1)), piece.encode(.black, .knight));
|
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(1)), piece.encode(.black, .knight));
|
||||||
|
|
||||||
try state.move(1, 18, null);
|
state.move(1, 18, null);
|
||||||
try std.testing.expectEqual(piece.Color.black, state.turn);
|
try std.testing.expectEqual(piece.Color.black, state.turn);
|
||||||
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
|
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
|
||||||
|
|
||||||
try state.move(57, 42, null);
|
state.move(57, 42, null);
|
||||||
try std.testing.expectEqual(piece.Color.white, state.turn);
|
try std.testing.expectEqual(piece.Color.white, state.turn);
|
||||||
try std.testing.expectEqual(@as(u32, 2), state.fullmove);
|
try std.testing.expectEqual(@as(u32, 2), state.fullmove);
|
||||||
}
|
}
|
||||||
@ -679,7 +710,7 @@ test "quiet non-pawn move increments halfmove clock" {
|
|||||||
state.halfmove = 4;
|
state.halfmove = 4;
|
||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
||||||
|
|
||||||
try state.move(1, 18, null);
|
state.move(1, 18, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u8, 5), state.halfmove);
|
try std.testing.expectEqual(@as(u8, 5), state.halfmove);
|
||||||
}
|
}
|
||||||
@ -689,7 +720,7 @@ test "pawn move resets halfmove clock" {
|
|||||||
state.halfmove = 4;
|
state.halfmove = 4;
|
||||||
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
||||||
|
|
||||||
try state.move(12, 20, null);
|
state.move(12, 20, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
||||||
}
|
}
|
||||||
@ -700,7 +731,7 @@ test "capture resets halfmove clock" {
|
|||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
||||||
state.setSquare((@as(u6, @intCast(2)) * 8) + @as(u6, @intCast(2)), piece.encode(.black, .bishop));
|
state.setSquare((@as(u6, @intCast(2)) * 8) + @as(u6, @intCast(2)), piece.encode(.black, .bishop));
|
||||||
|
|
||||||
try state.move(1, 18, null);
|
state.move(1, 18, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
||||||
}
|
}
|
||||||
@ -709,7 +740,7 @@ test "white double pawn push sets en passant target to passed square" {
|
|||||||
var state = BoardState.empty();
|
var state = BoardState.empty();
|
||||||
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
||||||
|
|
||||||
try state.move(12, 28, null);
|
state.move(12, 28, null);
|
||||||
|
|
||||||
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 20), state.en_passant);
|
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 20), state.en_passant);
|
||||||
}
|
}
|
||||||
@ -719,7 +750,7 @@ test "black double pawn push sets en passant target to passed square" {
|
|||||||
state.turn = .black;
|
state.turn = .black;
|
||||||
state.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
state.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
||||||
|
|
||||||
try state.move(51, 35, null);
|
state.move(51, 35, null);
|
||||||
|
|
||||||
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 43), state.en_passant);
|
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 43), state.en_passant);
|
||||||
}
|
}
|
||||||
@ -729,7 +760,7 @@ test "single pawn move clears en passant target" {
|
|||||||
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
||||||
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
||||||
|
|
||||||
try state.move(12, 20, null);
|
state.move(12, 20, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
||||||
}
|
}
|
||||||
@ -739,7 +770,7 @@ test "quiet non-pawn move clears en passant target" {
|
|||||||
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
||||||
|
|
||||||
try state.move(1, 18, null);
|
state.move(1, 18, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
||||||
}
|
}
|
||||||
@ -753,7 +784,7 @@ test "non-king non-rook quiet move preserves castling rights while updating othe
|
|||||||
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .knight));
|
||||||
|
|
||||||
try state.move(1, 18, null);
|
state.move(1, 18, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
||||||
try std.testing.expectEqual(@as(u8, 4), state.halfmove);
|
try std.testing.expectEqual(@as(u8, 4), state.halfmove);
|
||||||
@ -767,7 +798,7 @@ test "white king move clears white castling rights only" {
|
|||||||
state.castle_rights = 0b1111;
|
state.castle_rights = 0b1111;
|
||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
|
||||||
|
|
||||||
try state.move(4, 12, null);
|
state.move(4, 12, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u4, 0b0011), state.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b0011), state.castle_rights);
|
||||||
}
|
}
|
||||||
@ -778,7 +809,7 @@ test "black king move clears black castling rights only" {
|
|||||||
state.castle_rights = 0b1111;
|
state.castle_rights = 0b1111;
|
||||||
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(4)), piece.encode(.black, .king));
|
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(4)), piece.encode(.black, .king));
|
||||||
|
|
||||||
try state.move(60, 52, null);
|
state.move(60, 52, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u4, 0b1100), state.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1100), state.castle_rights);
|
||||||
}
|
}
|
||||||
@ -787,27 +818,27 @@ test "rook moves from original squares clear matching castling rights" {
|
|||||||
var white_king_side = BoardState.empty();
|
var white_king_side = BoardState.empty();
|
||||||
white_king_side.castle_rights = 0b1111;
|
white_king_side.castle_rights = 0b1111;
|
||||||
white_king_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
white_king_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
||||||
try white_king_side.move(7, 15, null);
|
white_king_side.move(7, 15, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b0111), white_king_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b0111), white_king_side.castle_rights);
|
||||||
|
|
||||||
var white_queen_side = BoardState.empty();
|
var white_queen_side = BoardState.empty();
|
||||||
white_queen_side.castle_rights = 0b1111;
|
white_queen_side.castle_rights = 0b1111;
|
||||||
white_queen_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
white_queen_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
||||||
try white_queen_side.move(0, 8, null);
|
white_queen_side.move(0, 8, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1011), white_queen_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1011), white_queen_side.castle_rights);
|
||||||
|
|
||||||
var black_king_side = BoardState.empty();
|
var black_king_side = BoardState.empty();
|
||||||
black_king_side.turn = .black;
|
black_king_side.turn = .black;
|
||||||
black_king_side.castle_rights = 0b1111;
|
black_king_side.castle_rights = 0b1111;
|
||||||
black_king_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
black_king_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
||||||
try black_king_side.move(63, 55, null);
|
black_king_side.move(63, 55, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1101), black_king_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1101), black_king_side.castle_rights);
|
||||||
|
|
||||||
var black_queen_side = BoardState.empty();
|
var black_queen_side = BoardState.empty();
|
||||||
black_queen_side.turn = .black;
|
black_queen_side.turn = .black;
|
||||||
black_queen_side.castle_rights = 0b1111;
|
black_queen_side.castle_rights = 0b1111;
|
||||||
black_queen_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(0)), piece.encode(.black, .rook));
|
black_queen_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(0)), piece.encode(.black, .rook));
|
||||||
try black_queen_side.move(56, 48, null);
|
black_queen_side.move(56, 48, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1110), black_queen_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1110), black_queen_side.castle_rights);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -816,7 +847,7 @@ test "rook move from non-original square preserves castling rights" {
|
|||||||
state.castle_rights = 0b1111;
|
state.castle_rights = 0b1111;
|
||||||
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(3)), piece.encode(.white, .rook));
|
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(3)), piece.encode(.white, .rook));
|
||||||
|
|
||||||
try state.move(27, 35, null);
|
state.move(27, 35, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
||||||
}
|
}
|
||||||
@ -826,28 +857,28 @@ test "capturing rooks on original squares clears matching castling rights" {
|
|||||||
white_king_side.castle_rights = 0b1111;
|
white_king_side.castle_rights = 0b1111;
|
||||||
white_king_side.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
white_king_side.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
||||||
white_king_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
white_king_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
||||||
try white_king_side.move(15, 7, null);
|
white_king_side.move(15, 7, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b0111), white_king_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b0111), white_king_side.castle_rights);
|
||||||
|
|
||||||
var white_queen_side = BoardState.empty();
|
var white_queen_side = BoardState.empty();
|
||||||
white_queen_side.castle_rights = 0b1111;
|
white_queen_side.castle_rights = 0b1111;
|
||||||
white_queen_side.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(0)), piece.encode(.black, .rook));
|
white_queen_side.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(0)), piece.encode(.black, .rook));
|
||||||
white_queen_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
white_queen_side.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
||||||
try white_queen_side.move(8, 0, null);
|
white_queen_side.move(8, 0, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1011), white_queen_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1011), white_queen_side.castle_rights);
|
||||||
|
|
||||||
var black_king_side = BoardState.empty();
|
var black_king_side = BoardState.empty();
|
||||||
black_king_side.castle_rights = 0b1111;
|
black_king_side.castle_rights = 0b1111;
|
||||||
black_king_side.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
black_king_side.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
||||||
black_king_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
black_king_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
||||||
try black_king_side.move(55, 63, null);
|
black_king_side.move(55, 63, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1101), black_king_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1101), black_king_side.castle_rights);
|
||||||
|
|
||||||
var black_queen_side = BoardState.empty();
|
var black_queen_side = BoardState.empty();
|
||||||
black_queen_side.castle_rights = 0b1111;
|
black_queen_side.castle_rights = 0b1111;
|
||||||
black_queen_side.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
black_queen_side.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
||||||
black_queen_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(0)), piece.encode(.black, .rook));
|
black_queen_side.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(0)), piece.encode(.black, .rook));
|
||||||
try black_queen_side.move(48, 56, null);
|
black_queen_side.move(48, 56, null);
|
||||||
try std.testing.expectEqual(@as(u4, 0b1110), black_queen_side.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1110), black_queen_side.castle_rights);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -857,7 +888,7 @@ test "capturing non-rook on original rook square preserves castling rights" {
|
|||||||
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .knight));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .knight));
|
||||||
|
|
||||||
try state.move(15, 7, null);
|
state.move(15, 7, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
||||||
}
|
}
|
||||||
@ -870,7 +901,7 @@ test "white double pawn push sets en passant and updates turn halfmove fullmove"
|
|||||||
state.castle_rights = 0b1111;
|
state.castle_rights = 0b1111;
|
||||||
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
state.setSquare((@as(u6, @intCast(1)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
||||||
|
|
||||||
try state.move(12, 28, null);
|
state.move(12, 28, null);
|
||||||
|
|
||||||
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 20), state.en_passant);
|
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 20), state.en_passant);
|
||||||
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
||||||
@ -887,7 +918,7 @@ test "black double pawn push sets en passant and increments fullmove" {
|
|||||||
state.castle_rights = 0b1111;
|
state.castle_rights = 0b1111;
|
||||||
state.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
state.setSquare((@as(u6, @intCast(6)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
||||||
|
|
||||||
try state.move(51, 35, null);
|
state.move(51, 35, null);
|
||||||
|
|
||||||
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 43), state.en_passant);
|
try std.testing.expectEqual((@as(u7, 1) << 6) | @as(u7, 43), state.en_passant);
|
||||||
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
||||||
@ -921,7 +952,7 @@ test "white en passant capture removes captured pawn behind target" {
|
|||||||
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
||||||
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 43);
|
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 43);
|
||||||
|
|
||||||
try state.move(36, 43, null);
|
state.move(36, 43, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(43));
|
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(43));
|
||||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(36));
|
try std.testing.expectEqual(@as(u4, 0), state.getSquare(36));
|
||||||
@ -938,7 +969,7 @@ test "black en passant capture removes captured pawn behind target" {
|
|||||||
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .pawn));
|
||||||
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
state.en_passant = (@as(u7, 1) << 6) | @as(u7, 20);
|
||||||
|
|
||||||
try state.move(27, 20, null);
|
state.move(27, 20, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(20));
|
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(20));
|
||||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(27));
|
try std.testing.expectEqual(@as(u4, 0), state.getSquare(27));
|
||||||
@ -954,7 +985,7 @@ test "white pawn promotes to each legal promotion piece" {
|
|||||||
var state = BoardState.empty();
|
var state = BoardState.empty();
|
||||||
state.setSquare(52, piece.encode(.white, .pawn)); // e7
|
state.setSquare(52, piece.encode(.white, .pawn)); // e7
|
||||||
|
|
||||||
try state.move(52, 60, promotion_type); // e8
|
state.move(52, 60, promotion_type); // e8
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.white, promotion_type), state.getSquare(60));
|
try std.testing.expectEqual(piece.encode(.white, promotion_type), state.getSquare(60));
|
||||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(52));
|
try std.testing.expectEqual(@as(u4, 0), state.getSquare(52));
|
||||||
@ -972,7 +1003,7 @@ test "black pawn promotes to each legal promotion piece" {
|
|||||||
state.fullmove = 7;
|
state.fullmove = 7;
|
||||||
state.setSquare(11, piece.encode(.black, .pawn)); // d2
|
state.setSquare(11, piece.encode(.black, .pawn)); // d2
|
||||||
|
|
||||||
try state.move(11, 3, promotion_type); // d1
|
state.move(11, 3, promotion_type); // d1
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.black, promotion_type), state.getSquare(3));
|
try std.testing.expectEqual(piece.encode(.black, promotion_type), state.getSquare(3));
|
||||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(11));
|
try std.testing.expectEqual(@as(u4, 0), state.getSquare(11));
|
||||||
@ -987,7 +1018,7 @@ test "promotion capture replaces captured piece with promoted piece" {
|
|||||||
white_state.setSquare(52, piece.encode(.white, .pawn)); // e7
|
white_state.setSquare(52, piece.encode(.white, .pawn)); // e7
|
||||||
white_state.setSquare(59, piece.encode(.black, .rook)); // d8
|
white_state.setSquare(59, piece.encode(.black, .rook)); // d8
|
||||||
|
|
||||||
try white_state.move(52, 59, .knight); // exd8=N
|
white_state.move(52, 59, .knight); // exd8=N
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.white, .knight), white_state.getSquare(59));
|
try std.testing.expectEqual(piece.encode(.white, .knight), white_state.getSquare(59));
|
||||||
try std.testing.expectEqual(@as(u4, 0), white_state.getSquare(52));
|
try std.testing.expectEqual(@as(u4, 0), white_state.getSquare(52));
|
||||||
@ -998,7 +1029,7 @@ test "promotion capture replaces captured piece with promoted piece" {
|
|||||||
black_state.setSquare(11, piece.encode(.black, .pawn)); // d2
|
black_state.setSquare(11, piece.encode(.black, .pawn)); // d2
|
||||||
black_state.setSquare(4, piece.encode(.white, .rook)); // e1
|
black_state.setSquare(4, piece.encode(.white, .rook)); // e1
|
||||||
|
|
||||||
try black_state.move(11, 4, .bishop); // dxe1=B
|
black_state.move(11, 4, .bishop); // dxe1=B
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.black, .bishop), black_state.getSquare(4));
|
try std.testing.expectEqual(piece.encode(.black, .bishop), black_state.getSquare(4));
|
||||||
try std.testing.expectEqual(@as(u4, 0), black_state.getSquare(11));
|
try std.testing.expectEqual(@as(u4, 0), black_state.getSquare(11));
|
||||||
@ -1009,7 +1040,7 @@ test "pawn reaching last rank without promotion type promotes to queen by defaul
|
|||||||
var white_state = BoardState.empty();
|
var white_state = BoardState.empty();
|
||||||
white_state.setSquare(52, piece.encode(.white, .pawn));
|
white_state.setSquare(52, piece.encode(.white, .pawn));
|
||||||
|
|
||||||
try white_state.move(52, 60, null);
|
white_state.move(52, 60, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.white, .queen), white_state.getSquare(60));
|
try std.testing.expectEqual(piece.encode(.white, .queen), white_state.getSquare(60));
|
||||||
|
|
||||||
@ -1017,7 +1048,7 @@ test "pawn reaching last rank without promotion type promotes to queen by defaul
|
|||||||
black_state.turn = .black;
|
black_state.turn = .black;
|
||||||
black_state.setSquare(11, piece.encode(.black, .pawn));
|
black_state.setSquare(11, piece.encode(.black, .pawn));
|
||||||
|
|
||||||
try black_state.move(11, 3, null);
|
black_state.move(11, 3, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.black, .queen), black_state.getSquare(3));
|
try std.testing.expectEqual(piece.encode(.black, .queen), black_state.getSquare(3));
|
||||||
}
|
}
|
||||||
@ -1026,7 +1057,7 @@ test "promotion type is ignored for non-promotion pawn move" {
|
|||||||
var state = BoardState.empty();
|
var state = BoardState.empty();
|
||||||
state.setSquare(12, piece.encode(.white, .pawn)); // e2
|
state.setSquare(12, piece.encode(.white, .pawn)); // e2
|
||||||
|
|
||||||
try state.move(12, 20, .queen); // e3, not a promotion square
|
state.move(12, 20, .queen); // e3, not a promotion square
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(20));
|
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(20));
|
||||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(12));
|
try std.testing.expectEqual(@as(u4, 0), state.getSquare(12));
|
||||||
@ -1078,7 +1109,7 @@ fn squaresMask(squares: []const bitboard.Square) bitboard.Bitboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn expectPawnMoves(state: *BoardState, from: bitboard.Square, expected: []const bitboard.Square) !void {
|
fn expectPawnMoves(state: *BoardState, from: bitboard.Square, expected: []const bitboard.Square) !void {
|
||||||
try std.testing.expectEqual(squaresMask(expected), state.getValidMoves(from));
|
try std.testing.expectEqual(squaresMask(expected), state.getPseudoLegalMoves(from));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expectLegalMoves(state: *BoardState, from: bitboard.Square, expected: []const bitboard.Square) !void {
|
fn expectLegalMoves(state: *BoardState, from: bitboard.Square, expected: []const bitboard.Square) !void {
|
||||||
@ -1591,7 +1622,7 @@ test "castling move execution moves the rook and clears castling rights" {
|
|||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
|
||||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(7)), piece.encode(.white, .rook));
|
||||||
|
|
||||||
try state.move(4, 6, null);
|
state.move(4, 6, null);
|
||||||
|
|
||||||
try std.testing.expectEqual(piece.encode(.white, .king), state.getSquare(6));
|
try std.testing.expectEqual(piece.encode(.white, .king), state.getSquare(6));
|
||||||
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(5));
|
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(5));
|
||||||
@ -1600,6 +1631,142 @@ test "castling move execution moves the rook and clears castling rights" {
|
|||||||
try std.testing.expectEqual(@as(u4, 0b0000), state.castle_rights & 0b1100);
|
try std.testing.expectEqual(@as(u4, 0b0000), state.castle_rights & 0b1100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setUpStartingPosition(state: *BoardState) void {
|
||||||
|
state.* = BoardState.empty();
|
||||||
|
state.turn = .white;
|
||||||
|
state.castle_rights = 0b1111;
|
||||||
|
state.fullmove = 1;
|
||||||
|
|
||||||
|
state.setSquare(0, piece.encode(.white, .rook));
|
||||||
|
state.setSquare(1, piece.encode(.white, .knight));
|
||||||
|
state.setSquare(2, piece.encode(.white, .bishop));
|
||||||
|
state.setSquare(3, piece.encode(.white, .queen));
|
||||||
|
state.setSquare(4, piece.encode(.white, .king));
|
||||||
|
state.setSquare(5, piece.encode(.white, .bishop));
|
||||||
|
state.setSquare(6, piece.encode(.white, .knight));
|
||||||
|
state.setSquare(7, piece.encode(.white, .rook));
|
||||||
|
for (8..16) |square| {
|
||||||
|
state.setSquare(@intCast(square), piece.encode(.white, .pawn));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.setSquare(56, piece.encode(.black, .rook));
|
||||||
|
state.setSquare(57, piece.encode(.black, .knight));
|
||||||
|
state.setSquare(58, piece.encode(.black, .bishop));
|
||||||
|
state.setSquare(59, piece.encode(.black, .queen));
|
||||||
|
state.setSquare(60, piece.encode(.black, .king));
|
||||||
|
state.setSquare(61, piece.encode(.black, .bishop));
|
||||||
|
state.setSquare(62, piece.encode(.black, .knight));
|
||||||
|
state.setSquare(63, piece.encode(.black, .rook));
|
||||||
|
for (48..56) |square| {
|
||||||
|
state.setSquare(@intCast(square), piece.encode(.black, .pawn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "isCheckmate detects Fool's mate" {
|
||||||
|
var state = BoardState.empty();
|
||||||
|
setUpStartingPosition(&state);
|
||||||
|
|
||||||
|
state.move(13, 21, null); // 1. f3
|
||||||
|
state.move(52, 36, null); // ... e5
|
||||||
|
state.move(14, 30, null); // 2. g4
|
||||||
|
state.move(59, 31, null); // ... Qh4#
|
||||||
|
|
||||||
|
try std.testing.expect(state.isInCheck(.white));
|
||||||
|
try std.testing.expect(state.isCheckmate(.white));
|
||||||
|
try std.testing.expect(!state.isStalemate(.white));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "isCheckmate detects Scholar's mate" {
|
||||||
|
var state = BoardState.empty();
|
||||||
|
setUpStartingPosition(&state);
|
||||||
|
|
||||||
|
state.move(12, 28, null); // 1. e4
|
||||||
|
state.move(52, 36, null); // ... e5
|
||||||
|
state.move(5, 26, null); // 2. Bc4
|
||||||
|
state.move(57, 42, null); // ... Nc6
|
||||||
|
state.move(3, 31, null); // 3. Qh5
|
||||||
|
state.move(62, 45, null); // ... Nf6
|
||||||
|
state.move(31, 53, null); // 4. Qxf7#
|
||||||
|
|
||||||
|
try std.testing.expect(state.isInCheck(.black));
|
||||||
|
try std.testing.expect(state.isCheckmate(.black));
|
||||||
|
try std.testing.expect(!state.isStalemate(.black));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "isStalemate detects known queen and king stalemate FEN position" {
|
||||||
|
var state = BoardState.empty();
|
||||||
|
state.turn = .black;
|
||||||
|
state.fullmove = 1;
|
||||||
|
state.setSquare(63, piece.encode(.black, .king)); // h8
|
||||||
|
state.setSquare(53, piece.encode(.white, .queen)); // f7
|
||||||
|
state.setSquare(46, piece.encode(.white, .king)); // g6
|
||||||
|
|
||||||
|
try std.testing.expect(!state.isInCheck(.black));
|
||||||
|
try std.testing.expect(!state.isCheckmate(.black));
|
||||||
|
try std.testing.expect(state.isStalemate(.black));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "promotion gives immediate check" {
|
||||||
|
var state = BoardState.empty();
|
||||||
|
state.setSquare(0, piece.encode(.white, .king));
|
||||||
|
state.setSquare(4, piece.encode(.black, .king)); // e1
|
||||||
|
state.setSquare(52, piece.encode(.white, .pawn)); // e7
|
||||||
|
|
||||||
|
state.move(52, 60, .queen); // e8=Q+
|
||||||
|
|
||||||
|
try std.testing.expectEqual(piece.encode(.white, .queen), state.getSquare(60));
|
||||||
|
try std.testing.expect(state.isInCheck(.black));
|
||||||
|
try std.testing.expect(!state.isCheckmate(.black));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "promotion choice can decide checkmate" {
|
||||||
|
var queen_state = BoardState.empty();
|
||||||
|
queen_state.setSquare(46, piece.encode(.white, .king)); // g6
|
||||||
|
queen_state.setSquare(53, piece.encode(.white, .pawn)); // f7
|
||||||
|
queen_state.setSquare(63, piece.encode(.black, .king)); // h8
|
||||||
|
|
||||||
|
queen_state.move(53, 61, .queen); // f8=Q#
|
||||||
|
|
||||||
|
try std.testing.expectEqual(piece.encode(.white, .queen), queen_state.getSquare(61));
|
||||||
|
try std.testing.expect(queen_state.isInCheck(.black));
|
||||||
|
try std.testing.expect(queen_state.isCheckmate(.black));
|
||||||
|
|
||||||
|
var knight_state = BoardState.empty();
|
||||||
|
knight_state.setSquare(46, piece.encode(.white, .king)); // g6
|
||||||
|
knight_state.setSquare(53, piece.encode(.white, .pawn)); // f7
|
||||||
|
knight_state.setSquare(63, piece.encode(.black, .king)); // h8
|
||||||
|
|
||||||
|
knight_state.move(53, 61, .knight); // f8=N, not checkmate
|
||||||
|
|
||||||
|
try std.testing.expectEqual(piece.encode(.white, .knight), knight_state.getSquare(61));
|
||||||
|
try std.testing.expect(!knight_state.isInCheck(.black));
|
||||||
|
try std.testing.expect(!knight_state.isCheckmate(.black));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "promotion capture piece attacks as selected promotion type" {
|
||||||
|
var knight_state = BoardState.empty();
|
||||||
|
knight_state.setSquare(0, piece.encode(.white, .king));
|
||||||
|
knight_state.setSquare(52, piece.encode(.black, .king)); // e7
|
||||||
|
knight_state.setSquare(54, piece.encode(.white, .pawn)); // g7
|
||||||
|
knight_state.setSquare(62, piece.encode(.black, .rook)); // g8
|
||||||
|
|
||||||
|
knight_state.move(54, 62, .knight); // gxg8=N+
|
||||||
|
|
||||||
|
try std.testing.expectEqual(piece.encode(.white, .knight), knight_state.getSquare(62));
|
||||||
|
try std.testing.expect(knight_state.isInCheck(.black));
|
||||||
|
|
||||||
|
var bishop_state = BoardState.empty();
|
||||||
|
bishop_state.setSquare(0, piece.encode(.white, .king));
|
||||||
|
bishop_state.setSquare(35, piece.encode(.black, .king)); // d5
|
||||||
|
bishop_state.setSquare(49, piece.encode(.white, .pawn)); // b7
|
||||||
|
bishop_state.setSquare(56, piece.encode(.black, .rook)); // a8
|
||||||
|
|
||||||
|
bishop_state.move(49, 56, .bishop); // bxa8=B+
|
||||||
|
|
||||||
|
try std.testing.expectEqual(piece.encode(.white, .bishop), bishop_state.getSquare(56));
|
||||||
|
try std.testing.expect(bishop_state.isInCheck(.black));
|
||||||
|
}
|
||||||
|
|
||||||
test "isCheckmate detects corner queen and king mate" {
|
test "isCheckmate detects corner queen and king mate" {
|
||||||
var state = BoardState.empty();
|
var state = BoardState.empty();
|
||||||
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .king));
|
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .king));
|
||||||
|
|||||||
@ -575,7 +575,7 @@ test "formatFen includes promoted white pieces" {
|
|||||||
state.fullmove = 1;
|
state.fullmove = 1;
|
||||||
state.setSquare(52, piece.encode(.white, .pawn));
|
state.setSquare(52, piece.encode(.white, .pawn));
|
||||||
|
|
||||||
try state.move(52, 60, case.promotion_type);
|
state.move(52, 60, case.promotion_type);
|
||||||
|
|
||||||
const actual = try formatFen(std.testing.allocator, state);
|
const actual = try formatFen(std.testing.allocator, state);
|
||||||
defer std.testing.allocator.free(actual);
|
defer std.testing.allocator.free(actual);
|
||||||
@ -601,7 +601,7 @@ test "formatFen includes promoted black pieces" {
|
|||||||
state.fullmove = 7;
|
state.fullmove = 7;
|
||||||
state.setSquare(11, piece.encode(.black, .pawn));
|
state.setSquare(11, piece.encode(.black, .pawn));
|
||||||
|
|
||||||
try state.move(11, 3, case.promotion_type);
|
state.move(11, 3, case.promotion_type);
|
||||||
|
|
||||||
const actual = try formatFen(std.testing.allocator, state);
|
const actual = try formatFen(std.testing.allocator, state);
|
||||||
defer std.testing.allocator.free(actual);
|
defer std.testing.allocator.free(actual);
|
||||||
|
|||||||
134
src/chess/game.zig
Normal file
134
src/chess/game.zig
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const board = @import("board.zig");
|
||||||
|
const piece = @import("piece.zig");
|
||||||
|
const bitboard = @import("bitboard.zig");
|
||||||
|
const fen = @import("fen.zig");
|
||||||
|
|
||||||
|
const Game = struct {
|
||||||
|
initial_state: board.BoardState,
|
||||||
|
state: board.BoardState,
|
||||||
|
moves: std.ArrayList(MoveRecord),
|
||||||
|
time_control: TimeControl,
|
||||||
|
clocks: [2]u32,
|
||||||
|
status: GameStatus,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Game, allocator: std.mem.Allocator) void {
|
||||||
|
self.moves.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn makeMove(self: *Game, from: bitboard.Square, to: bitboard.Square, promotion: ?piece.PieceType) void {
|
||||||
|
self.state.move(from, to, promotion);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legalMoves(self: *Game, square: bitboard.Square) bitboard.Bitboard {
|
||||||
|
if (!self.state.isCheckmate(self.state.turn) and !self.state.isStalemate(self.state.turn)) {
|
||||||
|
return self.state.getLegalMoves(square);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MoveRecord = struct {
|
||||||
|
from: u6,
|
||||||
|
to: u6,
|
||||||
|
promotion: u4,
|
||||||
|
time_remaining: u32,
|
||||||
|
captured_piece: u4,
|
||||||
|
previous_castling_rights: u4,
|
||||||
|
previous_en_passant: u7,
|
||||||
|
previous_halfmove: u8,
|
||||||
|
previous_fullmove: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeControl = enum {
|
||||||
|
tentwo,
|
||||||
|
fiveone,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GameStatus = enum {
|
||||||
|
not_started,
|
||||||
|
playing,
|
||||||
|
check,
|
||||||
|
stalemate,
|
||||||
|
checkmate_black,
|
||||||
|
checkmate_white,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn initStartingPosition(time_control: TimeControl) Game {
|
||||||
|
const state = board.BoardState.initStartingPosition();
|
||||||
|
return .{
|
||||||
|
.initial_state = state,
|
||||||
|
.state = state,
|
||||||
|
.moves = .empty,
|
||||||
|
.time_control = time_control,
|
||||||
|
.status = .not_started,
|
||||||
|
.clocks = [_]u32{0} ** 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFromFen(fen_string: []const u8, time_control: TimeControl) !Game {
|
||||||
|
const state = try fen.parseFen(fen_string);
|
||||||
|
return .{
|
||||||
|
.initial_state = state,
|
||||||
|
.state = state,
|
||||||
|
.moves = .empty,
|
||||||
|
.time_control = time_control,
|
||||||
|
.status = .not_started,
|
||||||
|
.clocks = [_]u32{0} ** 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "initStartingPosition creates independent current and initial states" {
|
||||||
|
var game = initStartingPosition(.tentwo);
|
||||||
|
defer game.deinit(std.testing.allocator);
|
||||||
|
|
||||||
|
const expected = board.BoardState.initStartingPosition();
|
||||||
|
try std.testing.expectEqualDeep(expected, game.initial_state);
|
||||||
|
try std.testing.expectEqualDeep(expected, game.state);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), game.moves.items.len);
|
||||||
|
try std.testing.expectEqual(TimeControl.tentwo, game.time_control);
|
||||||
|
try std.testing.expectEqual(GameStatus.not_started, game.status);
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), game.clocks[0]);
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), game.clocks[1]);
|
||||||
|
|
||||||
|
game.makeMove(12, 28, null); // e2-e4
|
||||||
|
|
||||||
|
try std.testing.expectEqualDeep(expected, game.initial_state);
|
||||||
|
try std.testing.expectEqual(piece.encode(.white, .pawn), game.state.getSquare(28));
|
||||||
|
try std.testing.expectEqual(@as(u4, 0), game.state.getSquare(12));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "initFromFen stores parsed position as initial and current state" {
|
||||||
|
const fen_string = "7k/5Q2/6K1/8/8/8/8/8 b - - 0 1";
|
||||||
|
var game = try initFromFen(fen_string, .fiveone);
|
||||||
|
defer game.deinit(std.testing.allocator);
|
||||||
|
|
||||||
|
const expected = try fen.parseFen(fen_string);
|
||||||
|
try std.testing.expectEqualDeep(expected, game.initial_state);
|
||||||
|
try std.testing.expectEqualDeep(expected, game.state);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), game.moves.items.len);
|
||||||
|
try std.testing.expectEqual(TimeControl.fiveone, game.time_control);
|
||||||
|
try std.testing.expectEqual(GameStatus.not_started, game.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legalMoves returns legal destinations while game is active" {
|
||||||
|
var game = initStartingPosition(.tentwo);
|
||||||
|
defer game.deinit(std.testing.allocator);
|
||||||
|
|
||||||
|
const e2: bitboard.Square = 12;
|
||||||
|
const e3: bitboard.Square = 20;
|
||||||
|
const e4: bitboard.Square = 28;
|
||||||
|
|
||||||
|
try std.testing.expectEqual(bitboard.bit(e3) | bitboard.bit(e4), game.legalMoves(e2));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "legalMoves returns no destinations after checkmate or stalemate" {
|
||||||
|
var mate_game = try initFromFen("rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3", .tentwo);
|
||||||
|
defer mate_game.deinit(std.testing.allocator);
|
||||||
|
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), mate_game.legalMoves(4)); // white king e1
|
||||||
|
|
||||||
|
var stalemate_game = try initFromFen("7k/5Q2/6K1/8/8/8/8/8 b - - 0 1", .tentwo);
|
||||||
|
defer stalemate_game.deinit(std.testing.allocator);
|
||||||
|
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), stalemate_game.legalMoves(63)); // black king h8
|
||||||
|
}
|
||||||
25
src/main.zig
25
src/main.zig
@ -114,6 +114,17 @@ fn promotionColorForMove(state: chess_board.BoardState, move: board_input.MoveRe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn isGameOverForTurn(state: chess_board.BoardState) bool {
|
||||||
|
var state_copy = state;
|
||||||
|
return state_copy.isCheckmate(state_copy.turn) or state_copy.isStalemate(state_copy.turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legalMovesIfGameActive(state: chess_board.BoardState, square: bitboard.Square) bitboard.Bitboard {
|
||||||
|
if (isGameOverForTurn(state)) return 0;
|
||||||
|
var state_copy = state;
|
||||||
|
return state_copy.getLegalMoves(square);
|
||||||
|
}
|
||||||
|
|
||||||
fn gameStatusForTurn(state: chess_board.BoardState) GameStatus {
|
fn gameStatusForTurn(state: chess_board.BoardState) GameStatus {
|
||||||
var state_copy = state;
|
var state_copy = state;
|
||||||
const side_to_move = state_copy.turn;
|
const side_to_move = state_copy.turn;
|
||||||
@ -681,7 +692,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
.none => {},
|
.none => {},
|
||||||
.drag_started => |held_square| {
|
.drag_started => |held_square| {
|
||||||
valid_moves = 0;
|
valid_moves = 0;
|
||||||
valid_moves = current_state.getLegalMoves(held_square);
|
valid_moves = legalMovesIfGameActive(current_state, held_square);
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -771,7 +782,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
board,
|
board,
|
||||||
promotion.move.to,
|
promotion.move.to,
|
||||||
)) |promotion_type| {
|
)) |promotion_type| {
|
||||||
try current_state.move(promotion.move.from, promotion.move.to, promotion_type);
|
current_state.move(promotion.move.from, promotion.move.to, promotion_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pending_promotion = null;
|
pending_promotion = null;
|
||||||
@ -1006,7 +1017,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
log.debug("selected square file={} rank={}", .{ selected_coord.file, selected_coord.rank });
|
log.debug("selected square file={} rank={}", .{ selected_coord.file, selected_coord.rank });
|
||||||
hovered_square = selected_coord;
|
hovered_square = selected_coord;
|
||||||
valid_moves = 0;
|
valid_moves = 0;
|
||||||
valid_moves = current_state.getLegalMoves(selected_square);
|
valid_moves = legalMovesIfGameActive(current_state, selected_square);
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -1042,7 +1053,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
if (promotionColorForMove(current_state, move)) |promotion_color| {
|
if (promotionColorForMove(current_state, move)) |promotion_color| {
|
||||||
pending_promotion = .{ .move = move, .color = promotion_color };
|
pending_promotion = .{ .move = move, .color = promotion_color };
|
||||||
} else {
|
} else {
|
||||||
try current_state.move(move.from, move.to, null);
|
current_state.move(move.from, move.to, null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug("invalid click move requested; clearing selection", .{});
|
log.debug("invalid click move requested; clearing selection", .{});
|
||||||
@ -1087,7 +1098,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
hovered_square = selected_coord_after_release;
|
hovered_square = selected_coord_after_release;
|
||||||
valid_moves = 0;
|
valid_moves = 0;
|
||||||
if (selected_after_release) |selected_square| {
|
if (selected_after_release) |selected_square| {
|
||||||
valid_moves = current_state.getLegalMoves(selected_square);
|
valid_moves = legalMovesIfGameActive(current_state, selected_square);
|
||||||
}
|
}
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
@ -1119,7 +1130,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
const selected_coord = board_input.squareToCoord(selected_square);
|
const selected_coord = board_input.squareToCoord(selected_square);
|
||||||
hovered_square = selected_coord;
|
hovered_square = selected_coord;
|
||||||
valid_moves = 0;
|
valid_moves = 0;
|
||||||
valid_moves = current_state.getLegalMoves(selected_square);
|
valid_moves = legalMovesIfGameActive(current_state, selected_square);
|
||||||
try rebuildBoardAndPieceRenderResources(
|
try rebuildBoardAndPieceRenderResources(
|
||||||
vc,
|
vc,
|
||||||
ldc,
|
ldc,
|
||||||
@ -1180,7 +1191,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
if (promotionColorForMove(current_state, move)) |promotion_color| {
|
if (promotionColorForMove(current_state, move)) |promotion_color| {
|
||||||
pending_promotion = .{ .move = move, .color = promotion_color };
|
pending_promotion = .{ .move = move, .color = promotion_color };
|
||||||
} else {
|
} else {
|
||||||
try current_state.move(move.from, move.to, null);
|
current_state.move(move.from, move.to, null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug("invalid drag move requested; clearing selection", .{});
|
log.debug("invalid drag move requested; clearing selection", .{});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user