M2: бот и headless-прогон матчей

- PlayerView: проекция состояния, в которой боту видно только то,
  что видно живому игроку (своя рука, стол, размеры чужих рук, счётчики)
- BasicBot: политика из GAMEPLAY.md, полностью детерминированная
- Match: обёртка с состоянием и подпиской на события (её же возьмёт UI)
- runMatch: прогон матча без движка и без таймеров
- soak-тесты: 3500 полных матчей (боты и случайные легальные ходы,
  2-4 игрока), инварианты состояния проверяются после каждого действия
- npm run stats:bot — распределение исходов самоигры

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ftc
2026-09-13 10:43:06 +03:00
co-authored by Claude Opus 5
parent fddab60527
commit c60fbcbb94
19 changed files with 927 additions and 13 deletions
+5 -5
View File
@@ -44,11 +44,11 @@ progress. The Cocos layer only renders state and turns taps into actions.
- [x] End conditions and loser ("fool") determination
- [x] Unit tests covering every rule clause, including the add-card limits
### M2 — Bot and headless match runner `[ ]`
- [ ] `PlayerView` projection — a bot sees only what a player legitimately sees
- [ ] `BasicBot` exactly per GAMEPLAY.md "Basic bot"
- [ ] `MatchRunner` driving a full headless match
- [ ] Soak test: thousands of seeded self-play matches finish with no illegal state
### M2 — Bot and headless match runner `[x]`
- [x] `PlayerView` projection — a bot sees only what a player legitimately sees
- [x] `BasicBot` exactly per GAMEPLAY.md "Basic bot"
- [x] `MatchRunner` driving a full headless match
- [x] Soak test: thousands of seeded self-play matches finish with no illegal state
### M3 — Savegame module `[ ]`
- [ ] `ISaveSection` contract, `SaveManager` registry, JSON keyed by section name
+27 -7
View File
@@ -45,8 +45,9 @@ assets/
core/ PURE TS — no engine imports
cards/ Suit, Rank, Card, Deck
rules/ GameState, actions, events, the reducer
bot/ PlayerView projection, BasicBot
util/ seeded RNG, shuffle, assertions
bot/ PlayerView projection, Bot contract, BasicBot
match/ Match (stateful shell), runMatch (headless driver)
util/ seeded RNG, shuffle
modules/
save/ SaveManager, ISaveSection, StatsSection, storage
app/ Bootstrap, ServiceLocator, SceneRouter
@@ -54,7 +55,10 @@ assets/
lobby/ lobby screen controller
game/ match screen: hand, table, deck, animations
common/ reusable widgets, safe-area root
tests/ vitest specs against assets/scripts/core
tests/
core/ vitest specs against assets/scripts/core
support/ position fixtures and the state invariant checker
tools/ measuring tools, not part of the suite
docs/ specs, this file, plan, decisions
local/ scratch art storage; contents get consumed
extensions/cocos-code-mode/ editor tool server
@@ -83,9 +87,16 @@ apply(state: GameState, action: Action): { state: GameState; events: GameEvent[]
## Bots
A bot is a function `(view: PlayerView) => Action`. `PlayerView` is a projection
of `GameState` containing only legitimately visible information. `BasicBot`
implements the policy in [GAMEPLAY.md](GAMEPLAY.md) verbatim.
A bot is `decide(view: PlayerView) => Action`. `PlayerView` is a projection of
`GameState` containing only legitimately visible information — own hand, table,
trump, opponents' hand *sizes*, deck and discard counts — plus the list of legal
actions. `BasicBot` implements the policy in [GAMEPLAY.md](GAMEPLAY.md).
`Match` wraps the reducer with the current state and an event subscription; the
game screen and the headless `runMatch` driver both hold one. As a reference
point, 5000 `BasicBot` self-play matches split roughly 43/43/14 between the two
seats and draws, with the opening attacker losing about 37% of the time —
`npm run stats:bot` reprints these.
## Savegame
@@ -114,7 +125,16 @@ Keys are explicit string constants, not class names — release builds minify.
npm test
```
Runs vitest over `tests/` against `assets/scripts/core`. No editor required.
Runs vitest over `tests/core` against `assets/scripts/core`. No editor required.
The soak specs play several thousand complete matches and re-check every state
invariant after every single action, so a rule regression surfaces at the move
that caused it.
```bash
npm run typecheck # strict tsc over core/ and the tests
npm run check:core-purity # fails if core/ ever imports the engine
npm run stats:bot # BasicBot self-play distribution, not a test
```
### Driving the editor