expanded basic game features
This commit is contained in:
121
docs/game.md
Normal file
121
docs/game.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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.).
|
||||
|
||||
Reference in New Issue
Block a user