Files
basegame-vcko/docs/game.md

122 lines
5.1 KiB
Markdown

# Game engine (`game.py`)
## Key types
`game.py` defines the core runtime objects used by the server:
- `Game`: contains the current game board state and implements actions/phases
- `Player`: per-player scores and owned cards
- `LobbyMember` / `GameMember`: lightweight records used by the server for lobby/game membership
It also provides:
- `load_game_data(...)`: builds an initial `game_state` dict by pulling data from MariaDB and dealing stacks
- `SummaryEncoder` and `GameObjectEncoder`: JSON encoders used by the server to serialize game state
## Game lifecycle
At runtime (via the server):
- `server.py` calls `load_game_data(game_id, preset, game_gamers)` to create a starting `game_state`
- The server wraps that dict in a `Game` object: `Game(game_state)`
- The server exposes the game via `/api/game/{game_id}/state` and `/api/game/{game_id}/action`
The `Game` object tracks:
- `player_list`
- `monster_grid`, `citizen_grid`, `domain_grid`
- dice: `die_one`, `die_two`, `die_sum`
- `effects`
- `action_required` (used to block on per-player, sequential “choose …” actions)
- `concurrent_action` (used to block on non-ordered, multi-player prompts; see “Concurrent actions” below)
- `last_active_time` (used by the server for cleanup)
## DB-backed bootstrap (`load_game_data`)
`load_game_data` is responsible for:
- Fetching cards using stored procedures:
- citizens/monsters depend on the `preset` (e.g. `"base1"` / `"base2"`)
- domains and dukes are randomized via procedures
- starters are selected via a direct `SELECT * FROM starters`
- Creating `Player` instances from the lobby/game membership list
- Randomizing player order and dealing initial cards
- Dealing stacks onto the board:
- monsters grouped by area, then 5 areas selected
- citizens grouped by roll match, placed into 10 stacks (special-cased for roll 11)
- domains dealt into 5 stacks of 3, with the top visible/accessible
This function currently assumes local DB connectivity via `127.0.0.1:3306` (typically via SSH port forward).
See `docs/database.md` for DB setup and stored procedure installation.
## Actions & phases
The server routes map `action_type` strings to methods on `Game`.
Common paths:
- `roll_phase()` rolls two dice and computes a sum
- `harvest_phase()` pays out from owned starters/citizens for all players based on the roll
- `hire_citizen(...)`, `buy_domain(...)`, `slay_monster(...)` mutate board stacks and player resources
### “choose …” actions
Some special payouts set `action_required` and start a background thread that waits until `act_on_required_action` updates `action_required` with a choice.
This is a dev-oriented approach; it allows the REST API to supply a follow-up choice via `act_on_required_action` while the game engine waits.
## Concurrent actions (non-ordered prompts)
Some prompts are not turn-based: every participating player should be able to
respond at the same time, in any order, and the game must wait until **all**
of them have submitted before progressing. The starting duke selection is the
first example — every player simultaneously discards down to one of the dukes
they were dealt.
These are modeled with `Game.concurrent_action`, which is independent from
`action_required`:
```
concurrent_action = {
"kind": "choose_duke", # routes to a handler in CONCURRENT_HANDLERS
"pending": ["pid1", "pid3"],
"completed": ["pid2"],
"responses": { "pid2": <opaque payload> },
"data": { ... } # handler-specific extras (often empty)
}
```
Engine semantics:
- While `concurrent_action.pending` is non-empty, `advance_tick()` returns
`False`. No phase transitions happen, no harvest progresses, and no
per-player turn actions are accepted. `is_blocked_on_concurrent_action()`
exposes the same predicate.
- Players submit via `Game.submit_concurrent_action(player_id, response, kind=...)`.
The handler's `apply()` validates and applies that player's response
immediately (so per-player effects don't have to wait for the others).
- When the last pending player submits, the handler's `finalize()` runs
(for any cross-player resolution), `concurrent_action` is cleared, and
if the engine was sitting in setup it advances forward.
### Adding a new concurrent action kind
1. Implement a handler class with:
- `apply(self, game, player_id, response)` — validate + apply per-player
side effects. Raise `ValueError` to reject a submission (the player
stays in `pending`).
- `finalize(self, game)` — optional; runs once after every participant
has submitted.
2. Register it in `CONCURRENT_HANDLERS` keyed by `kind`.
3. Build the prompt with `_new_concurrent_action(kind, participant_ids, data=...)`
and assign it to `game.concurrent_action` at the point in the engine
where the gate should appear.
4. On the client, register a renderer in the `CONCURRENT_RENDERERS` map in
the dev client (server.py HTML) keyed on the same `kind`.
Because the engine itself only knows "block while pending is non-empty",
the concurrent gate is fully reusable — no engine changes are required to
add new kinds (mulligan, simultaneous discard, voting, etc.).