aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-01 18:36:36 -0400
committerhistoria <[not public]>2026-10-01 18:36:36 -0400
commit8a94f21bd47c0a00bc415395232dba22fa958837 (patch)
tree3dc9492913315ac98b6eddaec4605e4344cde7d6
parent4e2a95503e4ea4c830510f5aea23fac7948ddc41 (diff)
downloadthehouseoficarus-8a94f21bd47c0a00bc415395232dba22fa958837.tar.gz
remove: slop docs
-rw-r--r--AGENTS.md37
-rw-r--r--README.md3
-rw-r--r--building_guide/README.md27
-rw-r--r--building_guide/admin.md3
-rw-r--r--building_guide/bank.md3
-rw-r--r--building_guide/hacking.md (renamed from dev_guide/hacking.md)0
-rw-r--r--building_guide/pai_gow.md149
-rw-r--r--building_guide/rooms.md347
-rw-r--r--building_guide/video_poker.md134
-rw-r--r--cmd/thoi/main.go3
-rw-r--r--dev_guide/README.md20
-rw-r--r--dev_guide/bank.md26
-rw-r--r--dev_guide/concurrency.md232
-rw-r--r--dev_guide/pai_gow.md110
-rw-r--r--dev_guide/roulette.md164
-rw-r--r--dev_guide/tick_order.md143
-rw-r--r--dev_guide/video_poker.md45
-rw-r--r--internal/casino/core.go4
-rw-r--r--internal/casino/video_poker_config.go2
-rw-r--r--internal/casino/video_poker_rules.go3
-rw-r--r--internal/game/act_burn.go2
-rw-r--r--internal/game/hacking/liars_dice_test.go1
-rw-r--r--internal/game/hardening_tick_lifecycle_test.go3
-rw-r--r--internal/game/money.go5
-rw-r--r--internal/game/sys_triggers.go3
-rw-r--r--internal/game/tick_systems.go6
-rw-r--r--internal/game/tick_systems_test.go11
-rw-r--r--internal/game/wallet_test.go3
28 files changed, 209 insertions, 1280 deletions
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index d81deca..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# AGENTS.md
-
-Guidance for AI coding agents working in this repository.
-
-## Documentation layout
-
-- `building_guide/` is for **human world builders only**: how to author the
- game's YAML content (rooms, objects, mobs, items, drops, triggers, quests,
- casino tables, and so on). See its README for the index.
-- `dev_guide/` is for **engine internals**: architecture, concurrency,
- durability contracts, presentation internals, and per-game implementation
- notes. Code comments that cite documentation must point here.
-- `README.md` is the project front page; keep it short and player/operator
- oriented.
-
-When writing documentation, pick the right directory by audience, not by
-topic:
-
-1. A builder placing a Pai Gow table reads `building_guide/pai_gow.md`.
-2. Someone changing hand evaluation reads `dev_guide/pai_gow.md`.
-3. If an article mixes both, split it across the two guides and cross-link.
-
-**Do not** use `building_guide/` as a scratch space: no TODOs, roadmaps,
-progress logs, implementation assessments, or "future work" sections. If
-you find yourself writing one, put it in `dev_guide/`, a code comment, or
-delete it. Conversely, don't bury builder-relevant configuration examples
-in `dev_guide/` — builders read only `building_guide/`.
-
-## House rules
-
-- Go: keep tests deterministic (seeded RNG); money flows through
- `internal/casino/credits.go` helpers; run `go test ./...` before handing
- work back.
-- YAML data files are validated at startup; run the server or the validate
- tests after editing anything under `data/`.
-- If a test or code comment names a documentation file, that file must
- exist at the cited path; update both together.
diff --git a/README.md b/README.md
index 5fb7d4d..c7ace85 100644
--- a/README.md
+++ b/README.md
@@ -49,8 +49,7 @@ There is not much here yet.
# Building / Playing
Docs: `building_guide/` is for human world builders (rooms, objects, mobs,
-items, casino tables); `dev_guide/` documents engine internals for
-contributors. `AGENTS.md` explains the split for AI coding agents.
+items, casino tables, and the rest of the world YAML).
Install Go then
diff --git a/building_guide/README.md b/building_guide/README.md
index de70acd..ab08dea 100644
--- a/building_guide/README.md
+++ b/building_guide/README.md
@@ -4,29 +4,4 @@ These guides are for **human world builders** working on the game's content:
rooms, objects, mobs, items, drops, triggers, quests, hazards, casino table
configuration, and similar data-driven world YAML.
-**Do not put engine documentation here.** Anything about Go internals,
-architecture, concurrency, testing, TODOs, roadmaps, or implementation
-assessments belongs in [`../dev_guide/`](../dev_guide/README.md) or in code
-comments next to the code it describes. Keeping the split clean matters:
-builders should be able to read this directory top to bottom without
-knowing the source layout.
-
-| Guide | Covers |
-|---|---|
-| [admin.md](admin.md) | In-game admin commands and the web admin panel |
-| [bank.md](bank.md) | Bank booths and player banking commands |
-| [behaviors.md](behaviors.md) | Gather, talk and shop behaviors |
-| [construction.md](construction.md) | Player-owned housing and furniture |
-| [courses.md](courses.md) | Agility courses |
-| [drops.md](drops.md) | Shared drop tables |
-| [hazards.md](hazards.md) | Environmental hazards |
-| [items.md](items.md) | Items, equipment, food, combinations |
-| [mobs.md](mobs.md) | Mobs, combat stats, shops, task mobs |
-| [objects.md](objects.md) | Objects, interactions, safespots |
-| [pai_gow.md](pai_gow.md) | Pai Gow Poker table configuration |
-| [quests.md](quests.md) | Quest walkthrough example |
-| [recipes.md](recipes.md) | Crafting and recipes |
-| [rooms.md](rooms.md) | Rooms, exits, casino tables, triggers |
-| [tips.md](tips.md) | Building tips |
-| [triggers.md](triggers.md) | Trigger scenarios |
-| [video_poker.md](video_poker.md) | Video poker cabinet configuration |
+LLMs should NOT add new documentation here.
diff --git a/building_guide/admin.md b/building_guide/admin.md
index b29ed00..a99535a 100644
--- a/building_guide/admin.md
+++ b/building_guide/admin.md
@@ -277,8 +277,7 @@ Editor API requests (including undo/redo) execute as one game-owner task:
file writes and their runtime callbacks/reloads cannot interleave with a tick
or player command. Large edits briefly pause gameplay; slow browser uploads
and downloads do not. Requests are limited to 8 MiB and rejected during game
-shutdown. See [Concurrency & durability](../dev_guide/concurrency.md#admin-editor-isolation)
-for the endpoint inventory and developer rules.
+shutdown.
This does not make every edit automatically hot-reloadable. Existing reload
requirements still apply to definition editors and raw Files edits. Use the
diff --git a/building_guide/bank.md b/building_guide/bank.md
index a6eb97f..71c05f0 100644
--- a/building_guide/bank.md
+++ b/building_guide/bank.md
@@ -47,6 +47,3 @@ objects:
| `data/help/bank.yaml` | Help topic for the bank system |
Default bank booth locations: Town Square (room 1), Forge (room 12).
-
-Engine internals (session state, storage layout, command dispatch) are
-documented in [../dev_guide/bank.md](../dev_guide/bank.md).
diff --git a/dev_guide/hacking.md b/building_guide/hacking.md
index dc4372b..dc4372b 100644
--- a/dev_guide/hacking.md
+++ b/building_guide/hacking.md
diff --git a/building_guide/pai_gow.md b/building_guide/pai_gow.md
index 93e9594..6aea61d 100644
--- a/building_guide/pai_gow.md
+++ b/building_guide/pai_gow.md
@@ -1,126 +1,35 @@
# Pai Gow Poker tables
-Configure a `casino_tables` entry:
+Add a `casino_tables` entry to a room with `game: paigow`:
```yaml
-- id: fortune
- game: paigow
- max_players: 6
- min_bet: 5
- max_bet: 1000
- betting_ticks: 30
- pai_gow:
- variant: classic # classic, ez, no_push, face_up
- envy: true
- autoset: false # default; true locks every legal arrangement without "set"
- a2345_second_highest: true # default; false makes A2345 the lowest straight
- action_ticks: 60
+casino_tables:
+ - id: fortune
+ game: paigow
+ max_players: 6
+ min_bet: 5
+ max_bet: 1000
+ betting_ticks: 30
+ pai_gow:
+ variant: classic
+ envy: true
+ autoset: false
+ a2345_second_highest: true
+ action_ticks: 60
```
-There are six player seats and one dealer. Each round deals seven packets
-from a fresh 53-card deck, including unused seats. No player banking.
-Side bets come first: `bet 5 fortune` (and, on a Face Up table,
-`bet <amount> pushace`) is placed before the main wager. `bet 20` is the
-main wager that commits the round; when every active player has one, the
-cards are dealt immediately. A lone bettor is dealt as soon as the main
-wager lands, with no timed window. Otherwise the betting window opens and
-expires on `betting_ticks`; anyone who has not bet sits out. `unbet main`,
-`unbet fortune`, `unbet pushace` or `unbet all` returns an undealt wager,
-and `stop` before the deal returns every wager and frees the seat. Both
-wagers use the table's limits; each is also bounded to 500,000,000 credits
-to keep combined payouts below the economy's credit cap even on unlimited
-tables.
-
-`a2345_second_highest` defaults to true and controls whether A-2-3-4-5 is
-the second-highest straight (below TJQKA) or the lowest. When enabled the
-rule is printed on the felt.
-
-## Player hand setting
-
-The seven dealt cards are shown with their position numbers underneath.
-`24` or `set 24` arranges positions 2 and 4 into the two-card low hand;
-`13456` arranges those five positions into the high hand. Spaces and order
-are immaterial; duplicates, out-of-range positions and other counts are
-rejected. Fouls are rejected with a clear warning and leave the hand
-unarranged.
-
-An arrangement is only a preview: the board redraws the five-card high
-hand, a gap, then the two-card low hand (with `HIGH` and `LOW` labels and
-position numbers) and waits. `set` locks the hand and reveals the dealer.
-`house` (`house way`, `set house`) arranges the house way and still needs
-`set`. `autoset on` locks any legal arrangement the moment it is made, so
-the preview and `set` steps are skipped; `autoset off` restores them, and
-`autoset` alone toggles. The table's `autoset` setting only seeds a new
-seat. Timeout or disconnect sets an unfinished hand the house way; a
-committed round cannot be abandoned.
-
-When every seat has set, the dealer reveals and the showdown compares the
-high hands first (your high, then the dealer's high) and then the low hands
-(your low, then the dealer's low). Each hand prints its strength in
-parentheses, bold yellow when it wins its comparison and bold white when it
-loses. The summary names the winner of each hand and then the round.
-
-## Variants
-
-* Classic: win both hands for 0.95:1 profit; lose both to lose; otherwise push.
- Whole-credit profit rounds down, with a minimum of one credit on a win,
- consistent with the existing casino economy. This affects small-bet odds.
-* EZ: the **partially-wild Joker edition**. No commission. Dealer's best
- five-card hand exactly queen-high pushes the main bets, regardless of split.
-* No Push: no commission. When one player hand beats the dealer and the other
- does not, compare the two extra cards dealt after the seven packets. The
- first is the shared player tiebreaker, the second the dealer's. Joker is an
- ace; equal ranks lose to the dealer. Both-win/both-loss results are unchanged.
-* Face Up: no commission; the dealer's seven cards are revealed before
- players set theirs. If the dealer's best five-card hand is exactly
- ace-high the round is an automatic push for every main bet. Requires
- `push_ace_high: 5_15_40` or `5_12_40`, which enables the optional Push
- Ace High side bet (`bet <amount> pushace`, capped at half the main
- wager): dealer ace-high without a Joker 5:1, with a Joker 15:1, both
- sides ace-high 40:1, else lost.
-
-Fortune and Envy are independent of all main-game results, including
-variant pushes. `push_ace_high` on a non-Face-Up table (or a Face Up table
-without it) is rejected by validation.
-
-## Fortune / Envy
-
-Source: [Wizard of Odds Fortune tables](https://wizardofodds.com/games/pai-gow-poker/side-bets/fortune/).
-Of the eight published fixed tables compared there, **table 8** has the
-lowest house edge: 4.4292% before Envy, about 0.08% with six players each
-betting five. These are published theoretical side-bet figures, not a
-simulation of this server. Envy value depends on occupancy and wager size.
-
-| Seven-card holding | Profit odds | Fixed Envy credits |
-|---|---:|---:|
-| Natural seven-card straight flush | 1000:1 | 500 |
-| Royal flush + royal match | 750:1 | 250 |
-| Wild seven-card straight flush | 500:1 | 150 |
-| Five aces | 250:1 | 100 |
-| Royal flush | 150:1 | 50 |
-| Straight flush | 50:1 | 20 |
-| Four of a kind | 25:1 | 5 |
-| Full house | 5:1 | 0 |
-| Flush | 4:1 | 0 |
-| Three of a kind | 3:1 | 0 |
-| Straight | 2:1 | 0 |
-| Three pair | 1:1 | 0 |
-
-Only the highest listed category pays, using all seven cards regardless
-of the actual split. Royal match means suited KQ in the two remaining
-cards. Fortune winning wagers also return their stake. A Fortune wager
-of at least five qualifies for fixed Envy prizes on **other active players**,
-not the bettor, dealer, unused packets or spectators. Multiple qualifying
-hands add. The other player need not have a Fortune bet. A losing Fortune
-stake is still lost when Envy pays.
-
-With `envy: false` (default), quads improve to **30:1**, a custom local
-schedule. The felt displays the effective table.
-
-Variant sources:
-* https://wizardofodds.com/games/ez-pai-gow-poker/partially-wild-joker/
-* https://wizardofodds.com/games/pai-gow-poker/appendix/4/
-* https://wizardofodds.com/games/face-up-pai-gow-poker/
-
-House-way algorithm, settlement internals and Joker art notes are in
-[../dev_guide/pai_gow.md](../dev_guide/pai_gow.md).
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `variant` | string | `classic` | `classic`, `ez`, `no_push`, or `face_up`. Each is a separate house ruleset. |
+| `envy` | bool | false | Enables Envy prizes and selects the Fortune/Envy pay schedule. |
+| `push_ace_high` | string | — | `face_up` only, and required there. `5_15_40` or `5_12_40`; selects the Push Ace High payout schedule. |
+| `autoset` | bool | false | Seeds the seat default. When true a legal arrangement locks immediately, without a `set` confirmation. Players may toggle it per seat in game. |
+| `action_ticks` | int | 60 | 1–600. |
+| `a2345_second_highest` | bool | true | When true, A-2-3-4-5 is the second-highest straight; when false it is the lowest. |
+
+`max_players` is capped at 6, and `max_bet` at 500,000,000. Each wager is
+bounded by the table's `min_bet`/`max_bet`.
+
+The `id` is internal; players join with `play paigow`. Shared table fields
+(`betting_ticks`, `min_bet`, `max_bet`, and ID rules) are documented under
+[Rooms — Casino Tables](rooms.md#casino-tables).
diff --git a/building_guide/rooms.md b/building_guide/rooms.md
index 635c43c..9cb93b1 100644
--- a/building_guide/rooms.md
+++ b/building_guide/rooms.md
@@ -234,25 +234,37 @@ Mobs stop wandering while in combat. Dead mobs respawn at their home room.
### Casino Tables
-Rooms may host one or more multiplayer casino tables. Players join by typing
-`play <game>`; the table ID is an internal room-local identifier and does not
-need to be typed. Blackjack and baccarat are the multiplayer table games;
-slots run as single-player machines (see below), and older rooms that declare
-slots under `casino_tables` are migrated to machines automatically.
+Rooms host multiplayer tables under `casino_tables` and single-player slot
+machines under `casino_machines`. Players join a table with `play <game>`, and
+a machine by its `variant` (or by its bare game name when it is the room's
+only machine of that game). Because a table is addressed by its game name, one
+room may host at most one table per game; video poker cabinets are addressed
+by their `id`.
-`max_players`, `betting_ticks`, `min_bet`, and `max_bet` use defaults when omitted.
-Game-specific rule configuration is nested alongside the table definition.
+Multiplayer table fields (`casino_tables`):
-Blackjack tables support sequential player turns and configurable house rules:
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `id` | string | — | Required. Nonempty, no surrounding whitespace, unique among the room's tables and machines. |
+| `game` | string | — | Required. `blackjack`, `baccarat`, `roulette`, `craps`, `paigow`, or `video_poker`. |
+| `max_players` | int | 8 | 0–64. Video poker is forced to 1; Pai Gow caps at 6. |
+| `betting_ticks` | int | 20 | 0–3600. |
+| `min_bet` | int | 1 | Must not exceed `max_bet`. |
+| `max_bet` | int | 1000000 | Must be at least `min_bet`. The sentinel `9223372036854775807` means no cap. |
+
+Game rules are nested in a block named after the game. `id` and `game` are the
+only required keys; omitted numeric fields keep their defaults.
+
+#### Blackjack
```yaml
casino_tables:
- id: blackjack
game: blackjack
max_players: 6
- betting_ticks: 20
min_bet: 1
max_bet: 1000
+ betting_ticks: 20
blackjack:
decks: 8
shuffle_at: 0.25
@@ -268,166 +280,112 @@ casino_tables:
split_unlike_ten_values: true
```
-Blackjack supports `hit`, `stand`, `double`, `split`, `insurance`, and
-`surrender`. A player blackjack against a dealer blackjack pushes. When the
-dealer's up card is an ace, an insurance window opens before anyone plays:
-each bettor may type `insurance` to buy cover for half their base bet, and
-any other action declines. Once every bettor has decided (or the window times
-out), a dealer blackjack settles the round immediately with insurance paying
-its configured profit multiplier; otherwise play begins. Split hands can be
-configured independently for re-splitting aces and doubling, and split aces
-receive one card each with no further actions beyond a configured re-split.
-
-Two blackjack knobs are player-favourable and on by default, so set them
-explicitly when authoring a standard-rules table:
-
-- `split_unlike_ten_values: true` lets any two 10-valued cards split
- (K+Q, 10+J). Casinos vary: some permit any ten-valued pair, others
- require identical ranks.
-- `double_split_blackjack: true` pays a 3+ card hand that reaches 21 after a
- split and double as a blackjack (3:2). Standard play pays 1:1 for any 21 on
- a split hand, doubled or not. Both raise the table's actual RTP above the
- nominal blackjack payout.
-
-Baccarat's `banker_payout` (0.95 = a 5% commission) rounds the commission UP:
-a winning banker bet too small to carry a whole credit of commission returns
-only the stake rather than paying a commission-free 1:1. Payout multipliers
-are capped (`blackjack_payout`/`insurance_payout` ≤ 5, `tie_payout` ≤ 10,
-commission-table `banker_payout` ≤ 0.99) so a typo cannot silently create an
-absurd table. No-commission baccarat variants require `banker_payout: 1`
-(or omit it); their compensating exceptions are enforced by the engine.
-
-### Blackjack rule cards and test tables
-
-`look blackjack`, `look blackjack table`, or `look <table-id>` displays the
-fixture's complete effective rule card (including omitted/default settings,
-fixed engine rules, limits, and timers). Joining with `play blackjack` also
-shows it privately. No scenery object or room-description text is required:
-placing a `casino_tables` entry in any room brings the rule card with it.
-Currently only one table of each game is allowed per room; use separate rooms
-for different blackjack tables.
-
-The casino has these test profiles:
-
-| Room | Table | Decks | Blackjack | Soft 17 | DAS | Maximum hands | Re-split aces | Surrender |
-| --- | --- | --- | --- | --- | --- | --- | --- | --- |
-| 6110 | Classic | 6 | 3:2 | Stand | Yes | 4 | No | No |
-| 6112 | Mercy | 6 | 3:2 | Stand | Yes | 4 | Yes | Late |
-| 6114 | Single Deck | 1 | 6:5 | Hit | No | 2 | No | No |
-| 6116 | Midnight | 8 | 3:2 | Hit | Yes | 4 | No | No |
-
-All four disable the nonstandard split-and-double blackjack bonus. Use
-`blackjack_payout: 1.5` for 3:2 or `1.2` for 6:5. `max_split_hands: 1`
-disables splitting; 2 allows one split, and larger values allow re-splitting
-within the cap (aces additionally require `resplit_aces`).
-
-Common casino variations not currently configurable are restricted double-down
-totals (e.g. 9–11 or 10–11 only), hitting split aces, disabling insurance,
-and a separate even-money action for a natural against an ace. Doubling is
-always for the full original stake, not “double for less.” Late surrender is
-supported; early surrender is not. Side bets are not implemented.
-
-Casino-fidelity caveats: fractional credits round down. Shoe coverage may
-trigger an early shuffle, and exhaustion rebuilds the shoe mid-round rather
-than guaranteeing a casino-style finite shoe for the whole round. A connected
-solo player's action timer also has special grace behavior. None of these
-mechanics were changed by adding rule cards.
-
-At a blackjack table, `bet` uses the configured minimum wager; `bet min`,
-`bet max`, and `bet <amount>` select the wager explicitly.
-
-Card art is loaded from `data/cards.yaml` and is shared by every card game
-(blackjack, baccarat). Each of the 52 rank/suit
-keys may define Unicode art for sizes 5, 7, and 9. Missing art uses the
-generated renderer. Players can choose `option card_size 1`, `5`, `7`, or `9`;
-size 1 uses Unicode playing-card symbols. ASCII mode always uses generated
-5x5 cards. `option card_style light` adds a white card background; `dark` is
-the default style. The same art renders baccarat hands. `option casino_display`
-only controls menu verbosity (`detailed` is the default; `compact` shortens
-menus and action hints) and never changes card size.
-
-Baccarat tables play punto banco: every participant bets on the player, the
-banker, or a tie, then the dealer plays out both hands by fixed drawing rules
-— there are no mid-round player decisions:
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `decks` | int | 8 | 0–64. |
+| `shuffle_at` | float | 0.25 | Shoe fraction; must be greater than 0 and less than 1. |
+| `blackjack_payout` | float | 1.5 | Profit multiplier; at most 5. |
+| `dealer_hit_soft_17` | bool | false | |
+| `max_split_hands` | int | 4 | `1` disables splitting, `2` allows one split, and larger values allow re-splitting up to the cap. |
+| `resplit_aces` | bool | false | |
+| `double_after_split` | bool | true | |
+| `double_split_blackjack` | bool | true | Pays a split and doubled 3+ card 21 as a blackjack. |
+| `insurance_payout` | float | 2 | Profit multiplier; at most 5. |
+| `surrender` | bool | false | Requires `min_bet` of at least 2. |
+| `action_ticks` | int | 20 | 0–3600. |
+| `split_unlike_ten_values` | bool | true | Allows any two 10-valued cards to split. |
+
+#### Baccarat
```yaml
casino_tables:
- id: baccarat
game: baccarat
max_players: 6
- betting_ticks: 20
min_bet: 1
max_bet: 1000
+ betting_ticks: 20
baccarat:
- variant: commission # commission (default), super_6, ez_baccarat
+ variant: commission
decks: 8
shuffle_at: 0.25
- tie_payout: 8 # 8:1 or 9:1 are common
- banker_payout: 0.95 # omit or use 1 for no-commission variants
- pair_bets: false # player_pair and banker_pair, 11:1
- dragon_7: false # winning three-card Banker 7, 40:1
- panda_8: false # winning three-card Player 8, 25:1
- super_6_bet: false # winning Banker 6, 12:1
-```
-
-At a baccarat table, `bet <amount> on player`, `bet <amount> on banker`, and
-`bet <amount> on tie` place wagers (the words and amounts may come in any
-order, and `min`/`max` work as usual). A bare `bet` repeats the previous
-amount and target. Winning player bets pay 1:1; on commission tables winning
-banker bets pay `banker_payout`:1 (0.95 is the classic 5% commission); winning tie bets pay
-`tie_payout`:1. On a tie, player and banker bets push. When the betting
-window closes, both hands are dealt by the standard tableau: naturals (8 or
-9) stand immediately, the player hand draws on 0-5, and the banker hand
-follows the usual third-card rules.
-
-#### Baccarat variants, rule cards, and test tables
-
-- **Commission punto banco:** default 5% commission on Banker winnings;
- reduced commission can be configured with `banker_payout` (e.g. 0.98).
-- **Super 6 (`super_6`):** no commission; Banker wins pay 1:1 except a
- winning Banker 6, with either two or three cards, pays 1:2.
-- **EZ Baccarat (`ez_baccarat`):** no commission; Banker wins pay 1:1 except
- a winning **three-card** Banker 7 pushes the Banker wager. Player and Tie
- wagers still lose on that outcome. A two-card Banker 7 pays normally.
-
-All variants use the same automatic punto banco drawing tableau. Standard,
-mini-, and midi-baccarat mainly differ in seating, limits, dealing pace, and
-card handling, not this tableau; they do not need separate settlement engines.
-Deck count, cut threshold, limits, seats, and Tie payout remain table-specific.
-
-Optional wagers default off and may be enabled independently on any variant.
-Use `bet 10 on player_pair`, `banker_pair`, `dragon_7`, `panda_8`, or `super_6`
-where offered. Pairs mean matching **ranks** of the first two cards (not merely
-matching baccarat values); they settle independently of which hand wins and
-can win on a tie. Dragon 7, Panda 8, and Super 6 require an outright hand win,
-not a tie. Their fixed profit odds are listed in the configuration example.
-The separate Super 6 optional bet is not the main Banker's half-pay rule.
-
-**Current wagering limitation:** one wager per player per round. Optional
-bets are selectable wagers, not additions to a simultaneous Player/Banker bet.
-All use the table's bet limits. Fractional winnings round down; for exact
-5% commissions use multiples of 20 credits, and for half-pay wins use even
-stakes. Commission is deducted immediately, not collected later on a ledger.
-
-`look baccarat`, `look baccarat table`, and `look <table-id>` display the full
-rule card, as does joining with `play baccarat`. It lists every supported
-setting, including disabled optional bets, automatic drawing rules, rounding,
-and operational limits. It belongs to the table and works in any room without
-a plaque or special room text. One baccarat table is allowed per room.
-
-| Room | Table | Variant | Decks | Tie pays | Optional wagers |
-| --- | --- | --- | --- | --- | --- |
-| 6127 | Harbor | 5% commission | 8 | 8:1 | Player/Banker pair |
-| 6128 | Charter | Super 6 | 6 | 9:1 | Pairs, Super 6 |
-| 6129 | Crown | EZ Baccarat | 8 | 8:1 | Dragon 7, Panda 8 |
-
-Not modeled: player squeeze/shoe-passing ceremonies, burn-card rituals,
-deferred commission, simultaneous wagers, and other casino-specific side-bet
-paytables (such as Dragon Bonus or progressive jackpots). These are not
-implied by selecting an implemented variant.
-
-Single-player machines use `casino_machines` and are separate from multiplayer
-tables:
+ tie_payout: 8
+ banker_payout: 0.95
+ pair_bets: false
+ dragon_7: false
+ panda_8: false
+ super_6_bet: false
+```
+
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `variant` | string | `commission` | `commission`, `super_6`, or `ez_baccarat`. |
+| `decks` | int | 8 | 0–64. |
+| `shuffle_at` | float | 0.25 | Must be greater than 0 and less than 1. |
+| `tie_payout` | float | 8 | Profit multiplier; at most 10. |
+| `banker_payout` | float | 0.95 | Commission tables only; at most 0.99. The `super_6` and `ez_baccarat` variants require 1 (or omit it). |
+| `pair_bets` | bool | false | Enables the player and banker pair wagers. |
+| `dragon_7` | bool | false | Enables the Dragon 7 wager. |
+| `panda_8` | bool | false | Enables the Panda 8 wager. |
+| `super_6_bet` | bool | false | Enables the optional Super 6 wager; separate from the `super_6` variant. |
+
+#### Roulette
+
+```yaml
+casino_tables:
+ - id: roulette
+ game: roulette
+ max_players: 8
+ min_bet: 1
+ max_bet: 10000
+ betting_ticks: 15
+ roulette:
+ double_zero: false
+ racetrack: false
+ spin_ticks: 4
+```
+
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `double_zero` | bool | false | false selects the European 37-pocket wheel; true selects the American 38-pocket wheel. |
+| `racetrack` | bool | false | Adds the French call bets; single-zero tables only. |
+| `spin_ticks` | int | 3 | 0–3600. |
+
+#### Craps
+
+```yaml
+casino_tables:
+ - id: craps
+ game: craps
+ max_players: 12
+ min_bet: 1
+ max_bet: 10000
+ craps:
+ variant: bank
+ odds: 0
+ field_12: 2
+```
+
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `variant` | string | `bank` | `bank`, `simplified`, `high_point`, `crapless`, `new_york`, `die_rich`, or `diceless`. Each is a separate house edition. |
+| `odds` | int | 0 | `0` selects 3-4-5x odds; `1`–`1000000` selects a uniform multiple. Editions without free odds require `0`. |
+| `field_12` | int | 2 | `2` or `3`: the field's payout on a 12. |
+
+#### Pai Gow Poker
+
+Pai Gow tables are configured under `pai_gow`; see
+[Pai Gow Poker tables](pai_gow.md) for every option.
+
+#### Video poker
+
+Video poker cabinets are single-seat `casino_tables` entries; see
+[Video poker cabinets](video_poker.md) for every option.
+
+#### Slot machines
+
+Slots are single-player machines and must be declared under `casino_machines`,
+not `casino_tables`:
```yaml
casino_machines:
@@ -440,57 +398,32 @@ casino_machines:
payout: 0.97
```
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `id` | string | — | Required; same rules as table IDs. |
+| `game` | string | — | Required; only `slots`. |
+| `variant` | string | `huff_and_even_more_puff` | Registered in Go (`slotVariants` in `internal/casino/registry.go`); an unregistered name is rejected. |
+| `min_bet` | int | 1 | |
+| `max_bet` | int | 1000000 | Must be at least `min_bet`. |
+| `spin_ticks` | int | 2 | 0–3600. |
+| `payout` | float | 0.97 | Target return-to-player fraction; greater than 0 and at most 1. |
+
#### Casino configuration validation
-Room create/update requests validate casino definitions before writing; startup
-validation reports invalid definitions, and game lookup refuses invalid room
-casino configurations. Raw file editing is not a validated save endpoint.
-
-- IDs must be nonempty, have no surrounding whitespace, and be unique across
- the room's tables and machines.
-- Supported table games are `blackjack`, `baccarat`, `roulette`, `craps`, and
- `paigow` ([Pai Gow configuration and variants](pai_gow.md)). Single-seat
- `video_poker` cabinets also use the table adapter ([cabinet guide](video_poker.md)).
- Roulette configuration basics: `roulette.double_zero` selects the American
- 38-pocket wheel (otherwise the European 37-pocket wheel), `roulette.spin_ticks`
- defaults to 3, and `roulette.racetrack: true` adds the French call bets on
- single-zero tables only. Craps rules and house editions are listed in-game via
- `help craps` and `table rules`; slots are machines only
- and must be declared under `casino_machines`. A `slots` entry under
- `casino_tables` is rejected. Machines use `slots`
- with the optional variant `huff_and_even_more_puff`. A machine variant is
- registered in Go (`slotVariants` in `internal/casino/registry.go`); naming an
- unregistered variant is a validation error.
-- A multiplayer table is addressed by its game name (`play blackjack`), so a
- room may host only one table per game.
-- A machine is addressed by its `variant` when one is declared, so a room may
- host several variants of one game, each reached by its own variant name. A
- bare game name addresses a machine only when it is the sole machine of that
- game (or declares no variant). Two variant-less machines of one game, or two
- machines sharing a variant, are rejected as unreachable duplicates.
-- Omitted/zero numeric values keep their existing defaults. Negative bets,
- timers, seat counts, split counts, decks, and payouts are rejected.
-- An explicit `max_bet` must be at least `min_bet`. An omitted or zero
- `max_bet` uses the default cap (1,000,000); the explicit sentinel
- `9223372036854775807` means no cap.
-- A blackjack table that offers `surrender` must set `min_bet` to at least 2:
- a surrendered hand returns half the bet, which rounds down to zero below
- that (the omitted default of 1 is rejected too).
-- Shoe coverage is best-effort, not a hard floor. The shoe reshuffles at the
- start of a round when it is smaller than the heuristic round size (eleven
- cards per hand--an all-ace hand can exceed that), and `draw()` rebuilds an
- exhausted shoe in place. A small `decks` value therefore never invalidates
- a table or darkens the room's other casino games.
-- `decks` is capped at 64 to bound shoe allocation. An explicit `shuffle_at`
- must be greater than zero and less than one.
-- Payouts and shuffle fractions must be finite (no NaN/infinity). Slots `payout`
- is an RTP fraction greater than zero and at most one; zero defaults to 0.97.
-- Unknown casino fields are rejected by the room create/update API.
-
-Editing a room hot-reconfigures an **idle** table or machine on the next
-lookup; an occupied game keeps its live rules until it becomes idle, so a
-committed wager is never discarded. Use explicit, modest wager limits:
-validation does not yet make currency arithmetic overflow-safe.
+Room create/update requests validate casino definitions before writing;
+startup validation reports invalid definitions, and game lookup refuses
+invalid room casino configurations. Unknown fields are rejected.
+
+- IDs share one namespace across the room's tables and machines.
+- One table per game per room. Machines address by `variant`, so several
+ variants may share a room; two variant-less machines of one game, or two
+ machines sharing a variant, are rejected as unreachable.
+- Omitted or zero numeric fields keep their defaults; negative bets, timers,
+ seat counts, split counts, decks, and payouts are rejected.
+- Payouts and shuffle fractions must be finite (no NaN or infinity).
+- Editing a room hot-reconfigures an **idle** table or machine on the next
+ lookup; an occupied game keeps its live rules until it becomes idle, so a
+ committed wager is never discarded.
### Objects — interactive fixtures
diff --git a/building_guide/video_poker.md b/building_guide/video_poker.md
index 497f64e..349797d 100644
--- a/building_guide/video_poker.md
+++ b/building_guide/video_poker.md
@@ -1,125 +1,27 @@
# Video poker cabinets
-The video poker hall east of Penny Reels (6118) is three rooms. The parlor
-(6192) runs low pay schedules, the high-limit room (6193) normal schedules, and
-the VIP room (6194) full pay schedules. Each holds the same five single-seat
-cabinets: Classic, Ultimate X Single Line, Ultimate X Multi-Hand, Super Times
-Pay, and Hot Roll. Choose a game **inside** a cabinet; schedules are not games.
-The shared table lifecycle adapter provides room routing, wallet settlement,
-disconnect and eviction handling, so author cabinets under `casino_tables`:
+Video poker cabinets are single-seat `casino_tables` entries:
```yaml
casino_tables:
-- id: uxm
- game: video_poker
- max_players: 1
- video_poker:
- cabinet: ultimate_x_multi
- payout: full
+ - id: uxm
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: ultimate_x_multi
+ payout: full
```
-`cabinet` accepts `classic`, `ultimate_x_single`, `ultimate_x_multi`,
-`super_times_pay`, `hot_roll`. `payout` accepts `low`, `normal`, `full` and is
-the house's setting: players cannot change it. Defaults are classic/full. Do
-not author variant/pay_table/feature/single_line; strict YAML validation
-rejects those obsolete fields. Do not use table wager limits to set coin
-denominations. Each cabinet has one occupant, but multiple cabinets may share a
-room and are addressed by ID. Short room ids (`vp`, `ux`, `uxm`, `stp`, `hr`)
-and cabinet aliases (`poker`, `ultimate_x`, `super_times`, `hot_roll`) keep
-`play` concise.
+| Field | Type | Default | Notes |
+|---|---|---|---|
+| `cabinet` | string | `classic` | `classic`, `ultimate_x_single`, `ultimate_x_multi`, `super_times_pay`, or `hot_roll`. |
+| `payout` | string | `full` | `low`, `normal`, or `full`. The cabinet's house schedule tier, applied to every game selectable on it. Players cannot change it. |
-## Games and quality tiers
+`max_players` is forced to 1. Cabinets are addressed by their `id` (plus a few
+built-in aliases such as `vp`, `ux`, `uxm`, `stp`, and `hr`), so several
+cabinets may share a room. Do not author `variant`, `pay_table`, `feature`, or
+`single_line`; strict YAML validation rejects those obsolete fields. Table
+wager limits do not set coin denominations.
-Each row below lists **low / normal / full** schedules. The authored tier
-applies to all games on that cabinet; selecting a different game preserves it.
-These are published schedules, not interpolated payouts. The tier names rank
-the offered schedules within a game/cabinet; they do not imply equal RTP across
-games. Some classic full schedules exceed 100% with optimal play.
-
-| Game | Classic | UX Single | UX Multi | STP | Hot Roll |
-|---|---|---|---|---|---|
-| Jacks or Better | 7/5, 8/5, 9/6 | 8/5, 8/6, 9/6 | 7/5, 8/5, 8/6 | 7/5, 8/5, 9/6 | 8/5, 9/5, 9/6 |
-| Bonus Poker | 6/5, 7/5, 8/5 | 6/5, 7/5, 8/5 | omitted | 6/5, 7/5, 8/5 | omitted |
-| Bonus Poker Deluxe | 8/5, 9/5, 9/6 | 8/5, 8/6, 9/6 | 7/5, 8/5, 8/6 | omitted | omitted |
-| Double Bonus | 9/6/4, 9/7, 10/7 | 9/6/4, 9/6/5, 9/7 | 9/5/4, 9/6/4, 9/6/5 | 9/6/4, 9/6/5, 9/7 | omitted |
-| Double Double Bonus | 8/5, 9/6, 10/6 | 8/5, 9/5, 9/6 | 8/5, 9/5, 9/6 | 8/5, 8/6, 9/6 | 8/5, 9/5, 9/6 |
-| Deuces Wild | C | U | U | C | omitted |
-| Joker Poker (Kings or Better) | J | J | J | omitted | omitted |
-
-- C: 20/12/10/4/4/3, 25/15/9/4/4/3, 25/15/9/5/3/2.
-- U: 25/16/13/4/3/2, 20/12/10/4/4/3, 25/15/9/4/4/3.
-- J: 50/15/7/5/3, 50/16/7/5/3, 50/17/7/5/3.
-- Double Bonus 9/7 and 10/7 pay five for straights; three-part names explicitly
- identify full-house/flush/straight rates.
-- Bonus Poker has only two sourced UX Multi schedules; it is omitted rather
- than inventing a third tier. Other omissions are deliberately conservative,
- not claims that no physical edition exists.
-
-| Edition | Supported hands |
-|---|---|
-| Classic Jacks or Better | 1, 3, 5, 10, 25, 50, 100 |
-| Other classic games | 1 |
-| Ultimate X Single | 1 |
-| Ultimate X Multi | 3, 5, 10 |
-| Super Times Pay | 1, 3, 5, 10 |
-| Hot Roll | 3, 5, 10 |
-
-## Player controls
-
-- `play <id>` seats you; `game` lists games; `game jacks` (or a full game name)
- selects one. The house sets the payout tier when the cabinet is authored in
- YAML; players cannot change it.
-- While seated at any casino game, casino verbs may be shortened to an
- unambiguous prefix: `b 1000` is `bet 1000`, and `be 1000` also works.
- `stop` and `rules` must be typed in full.
-- `denom 1|10|50` / `denomination` sets credits per coin.
-- `hands` lists counts; `hands <count>` selects one.
-- `bet 1-4` **adds** coins, capped at five per hand. `bet max` loads five.
- Bare `bet` / `rebet` repeats the previous selection, initially five.
-- Loading neither charges nor deals. Explicit bets show the pay table; bare
- bet does not. `deal` charges the full wager and reveals five cards.
-- `531`, `1 3 5`, or `hold 531` **toggles** holds. Repeating the command releases
- those cards; duplicate digits within one command toggle only once.
- `hold none` is not supported. `draw` or seated `d` finishes the hand.
- Away from video poker, `d` retains down movement.
-- `feature on|off` toggles the cabinet's bonus between rounds.
-- `paytable`, `look`, `table rules` redisplay; `rules` opens the cabinet guide.
- `list` retains available-wagers semantics and is not a pay-table command.
-- `stop` leaves between rounds. Disconnect completes the current holds.
-
-## Bonuses
-
-Feature ON requires five base coins per hand. UX and Hot Roll add five coins
-(10 total); STP adds one (6 total). Fees never increase the base payout.
-
-- **Ultimate X:** each line's current multiplier applies to its payout. Enabled
- wins earn next-hand multipliers using the published single/3/5/10-play award
- schedules. Awards never compound. Feature OFF collects existing multipliers
- without earning new ones. `NEXT X` shows awards for the selected hand count.
-- **Super Times Pay:** a pre-deal 1/15 trigger awards a shared multiplier.
- Conditional 2x/3x/4x/5x/8x/10x probabilities are 17/33/16/24/6/4 percent.
-- **Hot Roll:** a pre-deal 1/6 trigger rolls two fair dice; their sum multiplies
- every line. STP and Hot Roll have no next-hand bank.
-
-Ultimate X multiplier banks belong to the **machine**, not the player: another
-occupant inherits them, and they persist on disk under
-`data/casino/video_poker_banks/`.
-
-## Sources
-
-Published schedules, award tables and feature rules:
-
-- https://wizardofodds.com/games/video-poker/tables/ultimate-x-single-line/
-- https://wizardofodds.com/games/video-poker/tables/ultimate-x/
-- https://wizardofodds.com/games/video-poker/tables/super-times-pay/
-- https://wizardofodds.com/games/video-poker/tables/hot-roll/
-- https://wizardofodds.com/games/video-poker/tables/jacks-or-better/
-- https://wizardofodds.com/games/video-poker/tables/bonus-poker/
-- https://wizardofodds.com/games/video-poker/tables/bonus-poker-deluxe/
-- https://wizardofodds.com/games/video-poker/tables/deuces-wild/
-- https://wizardofodds.com/games/video-poker/tables/double-bonus/
-- https://wizardofodds.com/games/video-poker/tables/double-double-bonus/
-- https://www.videopoker.com/games/ (single/multi/hundred-play editions)
-
-Presentation internals and durable-bank transaction details are in
-[../dev_guide/video_poker.md](../dev_guide/video_poker.md).
+Shared table fields and ID rules are documented under
+[Rooms — Casino Tables](rooms.md#casino-tables).
diff --git a/cmd/thoi/main.go b/cmd/thoi/main.go
index 4a8353a..5b27439 100644
--- a/cmd/thoi/main.go
+++ b/cmd/thoi/main.go
@@ -48,8 +48,7 @@ func main() {
// the engine fires it before any dynamically-registered combat/aggro/respawn
// callbacks (engine.processTick sorts subscribers by registration ID). Its
// body — the canonical per-tick system sequence — is enforced and tested in
- // internal/game/tick_systems.go and documented in
- // dev_guide/tick_order.md. Reordering it is a breaking change.
+ // internal/game/tick_systems.go. Reordering it is a breaking change.
// SubscribeTicks executes the body on the game's owner goroutine so the
// whole tick sequence serializes against session input and admin tasks.
g.SubscribeTicks(1, func() bool {
diff --git a/dev_guide/README.md b/dev_guide/README.md
deleted file mode 100644
index 996ce8a..0000000
--- a/dev_guide/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# About this directory
-
-These guides document **engine internals** for developers and code-assisting
-agents: architecture decisions, durability contracts, presentation internals,
-and per-game implementation notes.
-
-World-builder documentation (YAML content, room/object/mob/item guides,
-casino table configuration) lives in [`../building_guide/`](../building_guide/README.md).
-Do not add builder-facing content here, and do not add implementation notes,
-TODOs or assessments to `building_guide/`.
-
-| Guide | Covers |
-|---|---|
-| [bank.md](bank.md) | Bank system session state and dispatch |
-| [concurrency.md](concurrency.md) | Owner loop, durable money, lifecycle transactions, shutdown |
-| [hacking.md](hacking.md) | Terminal objects and the hacking minigame interface |
-| [pai_gow.md](pai_gow.md) | Pai Gow Poker hand evaluation, house way, settlement, Joker art |
-| [roulette.md](roulette.md) | Roulette presentation internals and extension notes |
-| [tick_order.md](tick_order.md) | The canonical per-tick system order (kept in sync with tests) |
-| [video_poker.md](video_poker.md) | Video poker presentation and durable multiplier banks |
diff --git a/dev_guide/bank.md b/dev_guide/bank.md
deleted file mode 100644
index ed890a7..0000000
--- a/dev_guide/bank.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Bank system internals
-
-Builder-facing bank documentation (booth object, room placement, player
-commands) lives in [../building_guide/bank.md](../building_guide/bank.md).
-This page covers the engine side.
-
-## Session state
-
-`StateBank` (defined in `internal/net/server.go`) is the session state for
-the bank interface. Input is routed through `handleBankInput` in
-`internal/game/cmd_bank.go`.
-
-## Player storage
-
-Bank items are stored in the `Player.Bank` field (`map[int]*InventorySlot`),
-saved in character YAML. Bank slots are sequential and reuse freed slots.
-There is no slot limit.
-
-## Command dispatch
-
-- `bank` is classified as `ClassActive` in `classifyCommand()`
-- `bank` case in `executeCommand()` calls `doBank(sess)`
-- `doUse()` checks for `"bank"` or `"bank booth"` targets and routes to
- `doBank()`
-- `doBank()` checks for a `bank_booth` object in the current room
-- `handleBankInput()` dispatches deposit/withdraw/browse/leave commands
diff --git a/dev_guide/concurrency.md b/dev_guide/concurrency.md
deleted file mode 100644
index 187b942..0000000
--- a/dev_guide/concurrency.md
+++ /dev/null
@@ -1,232 +0,0 @@
-# Concurrency & durability
-
-This guide documents the runtime architecture decisions that keep live game
-state safe: the single-owner loop, single-file durable money, the account
-lifecycle transactions, and the graceful-shutdown sequence.
-
-## Single-owner game state
-
-One goroutine — the **owner loop** (`internal/game/owner.go`) — executes all
-mutation of live game state: session input handlers, connect/disconnect
-transitions, every tick callback (the master per-tick system sequence and all
-dynamically registered callbacks), and admin mutations. Network and admin
-goroutines submit tasks; they never touch live players, sessions, Hub
-membership, or game runtime maps directly.
-
-Rules:
-
-- `Game.RunOnOwner(fn)` (and `Game.HandleSessionOnOwner` for session input)
- block the caller until the closure has executed on the owner. Connection
- goroutines submit one task per input line, giving per-client backpressure
- without ever blocking another client.
-- A FULL task queue applies backpressure: submitters block until the owner
- drains a task, and a full queue never bypasses the owner — running a task
- on the caller's goroutine while the owner keeps executing would break the
- single-owner invariant. A STOPPED owner (graceful shutdown, after
- admissions have closed) refuses the task and drops it rather than running
- it on the caller's goroutine: `RunOnOwner` returns without executing the
- closure, because running it there would mutate live state concurrently
- with the shutdown sequence and after the final-save barrier. (Synthetic
- tests that construct a Game without an owner run tasks inline on the
- caller.)
-- Code already running on the owner must NOT call `Game.RunOnOwner` — it
- would deadlock waiting for itself. Owner-executed code calls functions
- directly.
-- Task execution is FIFO, so tick callbacks serialize against session input
- and admin mutations in submission order. Tick callbacks still run in
- engine registration-ID order (see tick_order.md); the owner wrapper only
- moves their execution onto the owner goroutine.
-- Slow clients cannot stall the world: `Session` writes are queued
- (`net.Session.StartWriter` puts a transport session into queued mode) and a
- dedicated per-session writer goroutine performs the network I/O with
- per-message write deadlines. A full queue disconnects only that client.
-- Saves run on the owner and serialize a value snapshot; concurrent
- read-modify-write cycles of the same files cannot happen.
-- Password work is the one deliberate exception: Argon2id verification and
- hashing (~64 MiB, tens of milliseconds) run on the CALLER's connection
- goroutine in `HandleSessionOnOwner` (`StatePassword` and
- `StateNewAccount` are intercepted before any owner task is submitted).
- A burst of logins therefore costs memory and CPU on connection
- goroutines, never a stall of the owner's command/tick queue. The owner
- task re-checks the stored hash against the one that was verified so a
- password change mid-login cannot authorize a stale attempt, and
- `player.hashLimiter` caps concurrent Argon2id computations at 4.
- All resulting state transitions (echo, session state, account saves)
- still happen on the owner.
-
-Unit tests construct Games without an owner; such Games run submitted tasks
-inline on the caller's goroutine (legacy behavior), which is fine for
-synthetic tests but production Games always have the owner (started by
-`game.New`).
-
-## Admin editor isolation
-
-All `/api/` routes run through one boundary in `internal/admin/owner.go`:
-
-1. Authenticate, read the request body (maximum 8 MiB) on the HTTP goroutine.
-2. Submit one synchronous `Game.RunAdminOnOwner` task. The **whole handler**
- runs there: validation against current files, plan construction, file
- commits, runtime migration/eviction, store reloads, and undo history.
-3. Write the buffered response on the HTTP goroutine after the task finishes.
-
-Reads use the same boundary so editor API reads cannot observe half an edit.
-Neither slow uploads nor slow downloads hold the owner. File I/O does: large
-bulk edits can briefly pause gameplay. This deliberately avoids a second
-transaction scheduler, fine-grained locks, or optimistic plan retries. Use a
-maintenance window for large world reorganizations.
-
-`RunAdminOnOwner` rejects requests during shutdown with HTTP 503, including
-requests queued before shutdown but not yet started. It never executes on the
-HTTP goroutine after the owner stops. A disconnected request is skipped if it
-has not started; once started, its edit completes even if the browser leaves.
-A failed/lost HTTP response is not proof that the edit did not commit.
-
-### Endpoint inventory and callback contract
-
-| API family (all behind the same boundary) | Mutations / live dependencies |
-| --- | --- |
-| `/rooms`, `/rooms/{id}`, `/rooms/new-empty` | Room create/update/delete, room index and runtime room state |
-| `/rooms/link`, `/rooms/insert`, `/rooms/remove`, `/rooms/move`, `/rooms/rename`, `/rooms/swap` | Multi-file room plans, course changes, room-ID migration or eviction where applicable |
-| `/rooms/bulk-edit`, `/rooms/bulk-delete`, `/rooms/resolve-duplicate` | Bulk room changes, indexes and runtime cleanup |
-| `/room-dirs/create`, `/room-dirs/rename` | Room file organization and indexes |
-| `/items`, `/mobs`, `/drops`, `/hazards` and their ID routes | Definitions plus existing cache/index invalidation; items also rebuild crafting |
-| `/objects`, `/techs`, `/modules`, `/triggers` and their ID routes | Definition files; existing manual-reload semantics remain unchanged |
-| Entity `/rename`, `/move`, `/dirs` routes | Definition file organization, undo history, existing cache invalidation |
-| `/courses`, `/courses/{id}` | Course files, exit reconciliation, course store reload |
-| `/files`, `/files/rename` | Raw file data and history; NOT a semantic room-ID migration or automatic cache reload |
-| `/colors`, `/colors/reset` | Persisted config and live color maps |
-| `/undo/undo`, `/undo/redo` | File images, room-ID migration, index/cache rebuilds, history together |
-| Remaining API routes | Read-only queries; serialized to get coherent snapshots |
-
-Register new endpoints on `apiMux`, never outside this boundary. Do not wrap
-individual callbacks in `RunOnOwner`: they are **already on the owner**.
-`cmd/thoi/main.go` wires direct course reload, craft rebuild, casino eviction,
-and `MigrateRoomIDsOnOwner` callbacks. `UndoStack`'s mutex protects history,
-not gameplay: its apply path and the following rebuild must remain in the
-same enclosing API task. Login password rehash saves and history clearing
-also run on the owner, with expensive password hashing outside it.
-
-This guarantees **isolation from gameplay**, not new crash-atomicity or
-rollback guarantees for every editor operation. Existing file transaction
-and error behavior is unchanged. Raw Files edits and external filesystem
-writes still bypass semantic validation/migrations; use structured room
-operations for live renumbering, and stop the game for external bulk edits.
-Some definition editors still need the in-game `reload` command; serialization
-does not add missing cache invalidations or migrate existing spawned entities.
-
-## Durable money (single-file wallet)
-
-Money lives in the character file and **nowhere else**. Every casino money
-mutation is one atomic `AccountStore.SaveCharacter` (`persistence.WriteFile`:
-temp file → fsync → rename → directory fsync) that carries both the balance
-and the `Player.PendingWagers` ledger. There is no second durable store, so
-there is no cross-file crash window to reconcile.
-
-- **Wager** — debits chips then credits and appends a `PendingWager` (its
- wallet handle plus the exact chips/credits debited) in the SAME save. The
- entry stays until the round resolves.
-- **Settlement** — `PayCredits(amount, stakeIDs...)` credits the payout and
- removes the listed stakes in the same save. A table passes the hand's stake
- IDs (its payout includes their return); a slot win passes its triggering
- stake (its payout is winnings only). Either way the credit and the stake
- consumption are atomic, so a crash can never pay the win and refund the
- stake.
-- **Loss/forfeit** — `ResolveStake`/`ResolveStakes` consume the pending
- stakes in one save; the round resolved and the stake is gone.
-- **Refund** — `RefundOutstanding` returns every pending stake (chips in
- kind, credits as credits) and clears the ledger in one save. Graceful
- shutdown (`Game.runShutdown` → `Casino.RefundAll`) and eviction use it.
-
-A save failure **before replacement** reverts the in-memory balance and
-ledger together. On `persistence.ErrDurabilityUncertain`, every wallet
-operation (wager, payout, stake resolution, refund) instead keeps the applied
-balance and ledger, because the visible file already carries both. The wallet
-logs the storage fault and reports the operation as applied so callers do not
-retry a payout or refund a resolved stake. This does not promise survival of
-a subsequent OS/power crash; it prevents a later save from overwriting the
-visible result with reverted memory.
-
-At startup (`game.New` → `recoverPendingWagers`, `internal/game/money.go`)
-any stake still listed in a character's `PendingWagers` is refunded: a listed
-stake means the process died before its round resolved. That is the entire
-recovery story — no ordering, checkpoints, evidence, or operator queue.
-
-### Two-phase journal recovery is out of scope
-
-This game deliberately does **not** implement a durable money journal or
-two-phase crash recovery. That design was built and then removed: it required
-a second file (`data/economy/journal.jsonl`) plus balance checkpoints, record
-ordering (`Seq`), payout `Covers` linkage, `open_payouts` evidence, and a
-startup recovery pass with per-record ambiguity handling. Its entire purpose
-was to make an ungraceful stop mid-round produce zero credit discrepancies,
-at a cost of ~1,400 lines of engine code and ~2,000 lines of tests.
-
-With the crash-consistency bar deliberately lowered to "a rare ungraceful
-stop may lose or refund one in-flight stake, and must never double-pay", the
-single atomic character save is sufficient: a listed pending stake is
-refunded, and a settlement whose save fails before replacement leaves the
-stake pending and the win unannounced (never paid on top of the refund).
-Post-replacement uncertainty follows the applied-state policy above. Do not reintroduce
-a second money store, per-record ordering, or an operator-inspection queue
-without an explicit decision to restore bank-grade guarantees.
-
-Currency arithmetic is checked (`internal/casino/credits.go`): explicit
-`MaxCredits` cap, bounded `DefaultMaxBet` default (the explicit unlimited
-sentinel is still honored), floor conversion for payout multipliers, and
-overflow rejected instead of wrapping. The cap is shared by the wallet:
-bank withdrawals are rejected when they would push the on-hand balance
-past it, and gameplay credit grants (shop sales, pickups, activity
-payouts) clamp at the cap and log rather than wrap.
-
-## Account lifecycle transactions
-
-Account and character lifecycle operations (create, rename, delete, purge)
-are single journal transactions (`internal/persistence/txn.go`): a journal
-entry with before-images is written durably BEFORE any file changes, the
-operations apply, and the journal is removed at the commit point. Deletes
-sync their parent directory; moves sync both affected directories (once if
-they are the same). Rollback also syncs deletions, including retries where
-the file is already absent, before retiring the journal. An apply failure
-attempts rollback; if rollback fails, the journal stays for recovery. A crash
-mid-commit is rolled back at the next startup by `persistence.Recover`.
-
-Characters carry a stable UUID (`player.Player.ID`) and owner field
-(`player.Player.Account`), assigned at creation and backfilled at first
-startup. Cached membership authorizes by the character file's owner, not by
-its renameable filename, so a stale cached name can never authorize
-connecting, renaming, or deleting a character reassigned to another account.
-
-After any lifecycle mutation, `Game.publishAccountUpdate` republishes the
-account state to every authenticated session of that account — including
-sessions sitting in menus — resetting stale pending selections. The online
-check and the file mutation run as one owner step, closing the
-check-then-rename gap.
-
-## Graceful shutdown
-
-Signals (SIGINT/SIGTERM) and the in-game `shutdown` command converge on
-`Game.RequestShutdown`, which runs the sequence exactly once:
-
-1. **Admissions stop:** session input and disconnect forfeit/save callbacks
- become no-ops (server-initiated shutdown is not a player disconnect).
-2. **Tick stop and join:** `Engine.StopAndWait` stops the ticker and waits
- for the in-flight callback to finish.
-3. **Drain and persist (on the owner):** remaining queued commands execute,
- outstanding wagers are refunded (`Casino.RefundAll`), and every online
- character is saved. Save failures are logged for the operator.
-4. **Owner stop:** the owner loop drains and stops.
-5. **Transports close:** HTTP(S) servers drain, listeners close, and
- sessions flush their outbound queues before their sockets drop.
- `ListenAndServe` returns and main exits normally — no `os.Exit`.
-
-## Room-ID migration
-
-Live room-ID changes (admin rename, swapid) execute as one mutation plan:
-the renamed room's file (id field, filename, self-loop references) plus every
-other room whose references moved, applied as a single journal transaction,
-followed within the same owner task by the full runtime migration
-(`Game.MigrateRoomIDsOnOwner` for the editor): player
-state, Hub room buckets, casino room keys, world room-keyed state (ground
-items, seeded markers, object states), and live mob instances. Undo/redo
-store the whole plan (all before/after images plus the idMap) and apply the
-inverse/forward runtime migration through the same wired callback.
diff --git a/dev_guide/pai_gow.md b/dev_guide/pai_gow.md
deleted file mode 100644
index 3e0bc88..0000000
--- a/dev_guide/pai_gow.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Pai Gow Poker internals
-
-Builder-facing table configuration and the published Fortune pay table
-live in [../building_guide/pai_gow.md](../building_guide/pai_gow.md).
-This page covers the engine side: hand evaluation, the house way,
-settlement and the Joker art.
-
-## Hand evaluation
-
-The Joker is partially wild: an ace, or any missing card completing a
-straight, flush or straight flush. Five aces beats a royal flush. A2345 is
-the second-highest straight (and straight flush), below TJQKA but above
-9TJQK, by default. `PaiGowConfig.A2345High()` reads the optional
-`a2345_second_highest` field (default true); the evaluator is threaded
-through the `...Rules` variants (`pgEvaluateRules`, `pgHouseRules`,
-`pgFortuneRules`, `pgBestRules`, `pgDealerPushRules`,
-`pgMainResultRules`, `pgPushAceClassRules`) so a table can demote the wheel
-to the lowest straight without changing the default wrappers the tests use.
-Low hands recognize only pairs and high cards. Suits do not break ties.
-Copies lose to the dealer. The high hand must strictly outrank the low hand.
-
-## Betting flow
-
-Side bets (`fortune`, and `pushace` on Face Up) are placed first; the main
-wager commits the round. The main bet deals immediately when every active
-(connected) player has one, and a lone bettor is dealt at once, so a single
-seat never waits on a timer. With two or more active players the shared
-betting window (`betting_ticks`) opens and `finishBettingLocked` deals when
-it expires if any main wager is live, refunding side bets on seats that
-never placed one. `unbet main|fortune|pushace|all` returns an undealt stake
-through `PayCredits`, and `leave` before the deal refunds the departing
-seat and re-checks `allBetLocked` so the remaining bettors can be dealt.
-The Push Ace High cap (half the main wager) is enforced when the main wager
-lands, because side bets are already down by then.
-
-## Player setting and showdown
-
-An arrangement made with `24`/`13456`/`house` is stored on the player as
-`pending`; it does not lock the hand or reveal the dealer. `set` moves
-`pending` into `split` and marks the seat ready. When `autoset` is on (the
-table default or a per-seat `autoset on`) a legal arrangement is committed
-straight to `split`. A seat's `autoset` is seeded from
-`PaiGowConfig.Autoset` at join.
-
-When every active seat is ready, `prepareSettlementLocked` evaluates both
-sides, builds a `PaiGowShowdown`, and hands it to `payLocked`. The showdown
-always orders `Hands` player-high, dealer-high, player-low, dealer-low.
-Each `PaiGowHandView` carries the label, the `pgRankName` strength, the
-cards, and whether that hand won its comparison. The presentation layer
-derives the colours (bold yellow for a winning strength, bold white for a
-losing one) from those flags, so the casino package stays colour-free. The
-`PaiGowShowdown` also carries the main result word, Fortune and Push Ace
-outcomes, the Envy amount (only when another active player is seated), the
-total stake, the payout, and the paid/pending state.
-
-The dealt seven cards are only put on the snapshot before a seat has
-arranged anything. A manual seat shows its `pending`/committed split as the
-`Preview` (high then low, gap between, position numbers); an autoset seat
-skips the preview and shows only the set/waiting note. `pgCardNums` maps
-each hand card back to its 1-7 dealt position.
-
-## House way
-
-This is a **local, deterministic house way**, not a claim to reproduce a
-particular casino's proprietary chart or optimal player strategy:
-
-1. Enumerate the 21 possible splits and discard fouls.
-2. Preserve the strongest available high-hand category.
-3. With two pair or a full house available, also allow splits leaving at
- least a pair high and a pair low.
-4. Maximize the low hand among these options, then the high hand.
-
-The dealer and automatic player setting use precisely the same algorithm.
-Published main-game house-edge estimates using other house ways do not
-necessarily apply to this implementation.
-
-## Fortune / Envy mathematics
-
-Of the eight published fixed tables compared on the Wizard of Odds page,
-table 8 (the one configured here) has the lowest house edge: 4.4292%
-before Envy, about 0.08% with six players each betting five. With Envy
-disabled, quads improve from 25:1 to 30:1, a custom local schedule
-lowering the published no-Envy edge by about 0.9974 percentage points to
-3.4318%. These are published theoretical side-bet figures, not a
-simulation of this server; Envy value depends on occupancy and wager size.
-
-## Face Up mechanics
-
-The dealer sets and reveals before players act; the automatic ace-high
-push is checked against the dealer's best five-card hand (`pgDealerPush`),
-so ace pairs and ace-headed flushes do not trigger it. Push Ace High
-classes are resolved by `pgPushAceClass`: a Joker in the dealer's seven
-cards pays the Joker rate (a natural ace plus the Joker makes a pair of
-aces, which is NOT ace-high); otherwise 40:1 when the player is also
-ace-high, else the plain rate.
-
-## Settlement
-
-Main, Fortune, Envy and Push Ace High settle in a single wallet
-transaction per player. Failed settlements freeze the result and block
-the next deal; `settle` or the table tick retries payment, never redraws.
-Successful recipients are not paid twice. Shutdown uses the existing
-durable stake-refund mechanism.
-
-## Joker art
-
-The shared Joker is rank `JK`, suit `joker`. `data/cards.yaml` supplies
-`JK` artwork at sizes 1/5/7/9; `internal/game/render_joker.go` also has
-ASCII and no-store fallbacks, including dark/light styles. Existing Joker
-Poker video poker uses the same art.
diff --git a/dev_guide/roulette.md b/dev_guide/roulette.md
deleted file mode 100644
index e3794e2..0000000
--- a/dev_guide/roulette.md
+++ /dev/null
@@ -1,164 +0,0 @@
-# Roulette and casino extension readiness
-
-The casino has a usable extension seam: `tableGame`, generic `tableCore`,
-registered constructors/config validation in `registry.go`, structured snapshots,
-shared participation/control routing, and durable per-stake wallet handles.
-Roulette uses those seams rather than adding another command dispatcher.
-
-Example table (one game of each type per room remains the manager's rule):
-
-```yaml
-casino_tables:
- - id: roulette_single
- game: roulette
- min_bet: 1
- max_bet: 10000
- betting_ticks: 15
- roulette:
- double_zero: false
- spin_ticks: 4
-```
-
-`double_zero: true` selects the American 38-pocket wheel; otherwise the
-European 37-pocket wheel is used. Spin ticks default to 3; betting ticks use
-the shared default. Ticks follow the server's configured tick length.
-The wheel order is authentic and the outcome is selected uniformly only when
-spinning finishes.
-
-## Presentation
-
-Art is rendered per recipient in `internal/game/render_roulette.go`, so color
-mode and Unicode preference stay per player (the engine snapshot never embeds
-ANSI). The views and betting tracker share one snapshot renderer:
-
-- **Tracker**: a five-line, 74-column strip above the betting board, rendered
- by `render_roulette_tracker.go`. The betting snapshot leads with a blank line
- so the tracker is separated from the prose or menu that precedes it. It shows
- the latest eight hits (newest first), three hot/cold pockets with hit counts,
- and red/black/zero percentages.
- All stats use a table-local rolling window of 50 completed spins. Zeros are
- included in the denominator; ties use numeric order (0, 00, 1, ...).
- History is copied into snapshots, never sampled before settlement, and
- survives round resets but not table replacement or server restart.
- History is copied into snapshots, never sampled before settlement, and
- survives round resets but not table replacement or server restart.
-- **Snapshot cadence**: the board and tracker are drawn once per round by the
- betting menu (on join and after settlement) and by an
- explicit `table`/`table rules`. Placing a wager only confirms the wager; it
- does not re-send the board snapshot.
-- **Betting board**: numbered grid with zeros in the left lane, red/black
- pockets, and outside bets. The whole five-row zero lane (three numbered rows
- plus the two separators between them) carries the green background, and its
- outer corners are rounded in Unicode mode. A racetrack table draws
- `renderRouletteRacetrack` above the board: a numbered oval in physical
- clockwise wheel order, with 26 at the right tip. Interior dividers meet
- each call bet's pockets, left to right: Tiers du Cylindre, Orphelins,
- Voisins du Zero, Zero. Labels are centered on the middle row; a label too
- wide to leave a blank cell against its dividers wraps onto the rows above
- and below it (e.g. "Tiers du" / "Cylindre"), and only the glyphs are
- written so the centering padding never paints over the dividers. Connected
- box-drawing steps separate Tiers from Orphelins; Zero has a curved boundary
- within Voisins. Neighbors is not drawn on the board, but remains in the
- payout reference and help.
-- **Menu ordering**: the roulette betting menu prints its balance and
- heading, then the tracker and board, and only then the wager reminder
- (`bet <amount> on <target>`, `bet / rebet`, `table / sitout / stop`) so the
- instructions follow the table they describe. Other games keep their menu
- above the snapshot.
-- **Spinning**: a stylised 17x9 wheel (wider than tall so it reads as a circle
- in a terminal cell grid). Only the rim is colored: solid red pockets alternate
- with a very dark blue that stands in for black (`rouletteWheelBlack`), because
- pure black vanishes on dark terminals. Pockets are two cells wide except the
- top and bottom centres, which stay one cell. One pocket is green (plus its
- diametrically opposite pocket on American wheels). A simple plus marks the
- croupier's spindle. No pocket numbers are drawn; with color off the pockets
- fall back to R/B/G so the wheel still reads.
-- **Result**: the winning pocket plus two physical wheel neighbours on each
- side (five cards) at playing-card size. The pocket number sits on the top row,
- a rule separates it from the ball (one row above centre), and the color name
- is spelled out on the bottom row only when color is off. There is no
- surrounding prose. The smallest (five-row) cards drop the rule so the ball
- can sit directly under the number. The wheel and result graphics each get a
- blank line before and after so they do not crowd the prose.
-
-The colored payout tables in `internal/game/render_casino_help.go` use the
-shared ANSI-aware `ui.Table` renderer, respecting ASCII/Unicode and wrap width.
-They show wheel-specific win chances and net profit odds, including call-bet
-exceptions. They appear only with `table` and `table rules`, not ambient art.
-`table help` opens the current game's guide; `help casino` indexes the brief
-casino guides in `data/help/`. `help racetrack` explains chip placements and
-rounding, including `zero`/`jeu zero` versus the straight-number target `0`.
-Each table's one-line
-overview is kept short (`Label | Limits 1-10000 | n/m seated | state`) so it
-fits beside a look minimap; `writeLookSideBySide` also wraps any content line
-that would otherwise run into the map column.
-
-Initial scope is **one wager per player per spin**. A wager covers straight-up
-numbers, outside bets (colors, parity, high/low, 1to12/13to24/25to36, rows),
-and the combination bets: splits, streets, corners, six-lines, trios, and the
-basket. Combination targets are a compact number list (`split 1 2`,
-`street 1-2-3`, `corner 1 2 4 5`, `line 1 2 3 4 5 6`, `trio 0 00 2`,
-`basket`) joined from the tokens after `on`; the leading name is optional
-because the geometry of the covered set identifies the bet. Non-adjacent or
-wrong-shaped sets are rejected rather than silently accepted. Multiple
-simultaneous wagers remain future work: `Participant.Wager` still holds a
-single stake.
-
-Single-zero tables may set `roulette.racetrack: true`. That adds the four
-French call bets (Voisins du Zéro, Tiers du Cylindre, Orphelins, Jeu Zero)
-and the Neighbors bet (a chosen number plus four on each side in wheel order).
-The racetrack is a single-zero layout only: the validator rejects
-`racetrack: true` together with `double_zero: true`. A call bet is **one
-wager** whose amount is the total stake spread evenly over the bet's chips
-(Voisins 9, Tiers 6, Orphelins 5, Jeu Zero 4, Neighbors 9). Because the
-components are ordinary inside bets, each returns `36/covered` when its
-number hits, so a winning pocket can pay a different multiple of the stake
-than another (for example 0 on Voisins pays 24/9, while 4 pays 18/9). The
-engine therefore resolves a wager to exact per-pocket `num/den` shares
-(`roulette_racetrack.go`) instead of a single multiplier; `bet` validates
-every possible return before committing and `settle` floors
-`stake*num/den`. The rack panel is drawn by `renderRouletteRacetrack` above
-the board and lists each bet's wheel-order series.
-
-All returns are checked before debiting a wager. Both winning and losing stakes
-settle through `PayCredits(return, stakeID)`, including a zero return for losses.
-Failures leave the stake pending and do not claim that payment succeeded.
-Shutdown and eviction refund outstanding stakes and reset the round.
-Disconnect refunds are safe because no partial outcome is sampled or exposed.
-
-Settlements are rendered by `settlementText` in `internal/casino/table_ux.go`:
-a colored win/loss/push headline gives the round's net once (the old
-"Net ... | ... returned (stake included)" split is gone), then each named bet
-— a blackjack hand, insurance, or the table's spot — is listed with its own
-net. Wins are green, losses red, pushes white, and a natural keeps the jackpot
-gradient. The engine emits inline color tags (`{casino_result_win}WIN{/}`),
-which `Game.casinoProse` expands per recipient against the session palette, so
-the engine still never embeds ANSI. Casino prose defaults to white
-(`casino_action`), leaving color for emphasized outcomes.
-
-## Assessment and boundaries
-
-This is a sound foundation for more testing-stage table games, not a blanket
-production-readiness certification. New games must test payout math, overflow,
-phase boundaries, disconnects, queued departure, refunds, persistence failure,
-and spectator output. Shared card-game loss settlement still uses the void
-`ResolveStakesByIDs` API: it preserves failed stakes for recovery but cannot
-report that failure to its caller. Roulette avoids that limitation with the
-existing error-returning payment API. Uniform per-stake error-returning
-settlement would be a worthwhile follow-up for older games.
-
-Multiple concurrent wagers need their own explicit ledger/limit model instead
-of reusing the single `Participant.Wager`. Multiple same-game tables in one room
-would also require changing current addressing/validation assumptions.
-
-## Testing shortcut
-
-`data/objects/casino_test_portal.yaml` is placed in starting room 1001.
-`use casino` gives 1000 chips and teleports to vestibule 6100. It is intentionally
-repeatable and uses ordinary trigger/drop/teleport machinery. Keep an inventory
-slot free (ordinary trigger drops fall to the ground when inventory is full).
-Remove the room's `casino_test_portal` reference before production: this grants
-unlimited test funds and bypasses travel/progression. No account roles are needed.
-
-Single-zero room 6182 and double-zero room 6183 are north of ground-floor
-landing 6102. `help roulette` describes player syntax and supported bets.
diff --git a/dev_guide/tick_order.md b/dev_guide/tick_order.md
deleted file mode 100644
index b1246dd..0000000
--- a/dev_guide/tick_order.md
+++ /dev/null
@@ -1,143 +0,0 @@
-## Per-Tick System Order
-
-The server runs on a fixed game tick (600ms by default; configurable via
-`tick_length` in `config.yaml`). Each tick, every online player's pending
-commands resolve and a fixed sequence of systems advance world state. The
-**canonical order** in which these per-tick systems fire is a load-bearing
-contract: combat resolution, deplete/restock timers, regen, respawns, and
-sequence-driven triggers all depend on it. Reordering it is a breaking change.
-
-### Source of truth
-
-The authoritative list lives in code, not in this doc:
-
-- `internal/game/tick_systems.go` — the `tickSystemOrder` slice and
- `(*Game).RunTickSystems()`, the method that fires them in order.
-- `cmd/thoi/main.go` — the master per-tick subscriber, registered with the
- engine as **subscription ID 1**, whose body is `g.RunTickSystems()`. The
- engine (`internal/engine/tick.go` `processTick`) sorts subscribers by
- registration ID, so the master subscriber fires first each tick, before
- any dynamically-registered combat / aggro / respawn callbacks (which get
- higher IDs as players act).
-- `internal/game/tick_systems_test.go` — `TestTickSystemOrder` fails if the
- table drifts from the list below. `TestRunTickSystemsEmptyGameIsSafe`
- guards the "safe to tick with no players connected" contract every system
- must uphold (see "Per-system contracts" below).
-
-If you change the order, add a system, or remove one: update `tickSystemOrder`
-in `tick_systems.go`, the `expectedTickSystemOrder` list in
-`tick_systems_test.go`, and the table in this file. All three are intentional
-duplicates so any one of them drifting fails the test loudly.
-
-### Canonical sequence
-
-| # | System | What it does |
-| --- | ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
-| 1 | `ProcessQueuedCommands` | Drains each player's free-queued commands and the globally timestamp-sorted active queue; advances walk sequences. |
-| 2 | `CasinoTick` | Advances casino betting deadlines and resolves completed casino rounds. |
-| 3 | `MoveTick` | Counts down `p.MoveTicks`; on reach-zero, completes a pending run/walk and re-prompts. |
-| 4 | `SequenceTick` | Advances every active trigger `sequence` (per-player and global) one step. |
-| 5 | `TransientMobTick` | Despawns owned / `DespawnOnLeave` mobs whose owner has left their room. |
-| 6 | `World.Tick` | World-internal: ground-item respawn/despawn/reserve timers and shared-object deplete timers. |
-| 7 | `MobStore.Tick` | Mob shop restock and mob HP regenerate. Order: by `InstanceID` so processing is stable across ticks. |
-| 8 | `RegenTick` | Player HP regeneration, accelerated by tech HP-regen multipliers. |
-| 9 | `EnergyRegenTick` | Run-energy regeneration for every online player. |
-| 10 | `DisconnectTick` | Counts down pending disconnects and hard-removes finished sessions (skipping players still in combat). |
-| 11 | `WanderTick` | Mob wandering (exit-based) and object wandering (fishing spots), with enter/leave messages to observers. |
-| 12 | `SharedDepletionTick` | Ticks shared-object deplete timers up (in use) or down (recovering). |
-| 13 | `FireTick` | Decays fire objects; on burn-out, spawns the removal item and cancels stokers. |
-| 14 | `AdvanceActions` | Advances every active player action by one tick; fires completion handlers per action type. |
-| 15 | `TechTick` | Drains battery for active tech, applies equipment / PreserveDrain reductions, and deactivates all on empty. |
-| 16 | `ConsumeTick` | Counts down `ConsumeCooldown` and `HomeTransportCooldown`. |
-| 17 | `BroadcastRespawns` | Flushes respawned world objects and broadcasts their `respawn_broadcast` lines to players in those rooms. |
-| 18 | `VisualTick` | Optional per-player "visual tick" line/counter for AFK feedback. |
-| 19 | `SneakTick` | Advances guard watch timers and sneaking players' tick counters. |
-| 20 | `FarmTick` | Crops grow, drain water, and yield on harvest (fires every `FarmTickInterval` ticks). |
-| 21 | `BuffTick` | Potion buff duration countdown; expires and removes effects. |
-| 22 | `SafespotTick` | Safespot unsafe-chance and decay-chance rolls per player. |
-| 23 | `HazardTick` | Room hazard damage to non-GodMode players. |
-
-### Determinism contract
-
-Three layers of ordering decide what happens on a given tick, in priority:
-
-1. **Subscriber order** — deterministic. `engine.processTick` sorts
- subscribers by registration ID. The master subscriber (ID 1, set in
- `cmd/thoi/main.go`) always fires first; every system in
- `RunTickSystems` then runs in the table order above. Dynamic combat /
- aggro / respawn subscribers registered later get higher IDs and fire
- after the canonical 23 systems. This is asserted by
- `internal/engine/tick_test.go::TestProcessTickFiresInSubscriptionOrder`.
-
-2. **Per-system iteration order** — deterministic, as of the deterministic
- per-tick subscriber work. Systems that iterate over players or mobs use
- stable ordering:
- - `Hub.AllSessions()` and `Hub.PlayersInRoom` (`internal/net/server.go`)
- sort sessions by `Session.seq`, a monotonic counter assigned in
- `Hub.Add` in connection order.
- - `MobStore.AllInstances()`, `MobStore.MobsInRoom()`, and `MobStore.Tick()`
- (`internal/world/mob.go`) sort by `MobInstance.InstanceID`.
-
- This means, for example, that cosmetic "X enters." / "Y leaves."
- interleavings in `WanderTick`, shop-restock order in `MobStore.Tick`,
- and per-player processing order in `RegenTick` / `HazardTick` /
- `SneakTick` etc. are reproducible across runs.
-
-3. **Random-number generation** — **not deterministic across server
- restarts.** The `math/rand` top-level functions used across the engine
- (`rand.Float64`, `rand.Intn`, `rand.Int`, `rand.Shuffle`) are
- auto-seeded at program startup (Go 1.20+; calling `rand.Seed` is
- deprecated and is never done here). So combat hit rolls, drop rolls,
- wandering exit picks, and minigame randomness differ between server
- runs. Two ticks from the same seeded run are reproducible *relative* to
- each other (the global source is deterministic), but a fresh server
- start produces a different stream.
-
- Only `rollAggroRand` (`internal/game/sys_combat.go`) is replaceable for
- tests; the rest are global-function calls. Reproducible full-tick
- traces (e.g. for record/replay debugging) would require threading a
- seeded `*math/rand.Rand`/`math/rand/v2.ChaCha8` through the engine; not
- currently done.
-
-### Per-system contracts (the empty-game invariant)
-
-Every system in the table must be safe to invoke on a `*Game` that has its
-stores initialized (`World`, `MobStore`, `Hub`, `queue`, etc. via
-`game.New` + `SetHub`) but **no players connected**. Concretely:
-
-- Systems that touch the hub: guard with `if g.Hub == nil { return }` at the
- top, *or* guard each `Hub.*` call with `if g.Hub != nil`. Either is fine.
- (`main.go` now calls `SetHub` *before* `Ticks.Start`, so the production
- server never ticks with a nil hub; the guards are belt-and-braces for a
- `*Game` that tests construct without `SetHub`, such as
- `newRunTickSystemsTestGame`.)
-- Systems that iterate over maps/slices held on `*Game` (`sequences`,
- `restTimers`, `guardWatchTimers`, `hackingStates`, …) must tolerate empty
- / freshly-initialized maps. `game.New` initializes all of them; tests that
- build a stub `*Game` must too (see `newRunTickSystemsTestGame`).
-
-Violating either of these is caught by
-`TestRunTickSystemsEmptyGameIsSafe` in `internal/game/tick_systems_test.go`.
-If you add a new system that needs a new field initialized, update that
-helper as well.
-
-### Notes for builders
-
-When writing content that depends on per-tick ordering (a trigger that
-should fire *after* regen, a sequence whose step messages should appear
-*before* the move completes, etc.), consult the table above and remember:
-
-- `MoveTick` (table #3) runs before `SequenceTick` (table #4) — the table
- fires top-to-bottom, so a move that completes this tick will have its
- enter-room knock-on triggers fire (inside `ProcessQueuedCommands`, #1, or
- `MoveTick`, #3) before trigger `sequences` advance in #4.
-- `on_enter` / `on_exit` room triggers fire from command execution, not
- from a per-tick system; they run inside `ProcessQueuedCommands` (#1)
- or `MoveTick` (#3) when the move actually completes.
-- Shop restock (`MobStore.Tick`, #7) happens *before* `RegenTick` (#8) and
- *before* `AdvanceActions` (#14); if a gathering action completes this tick
- it does so in #14, by which point shop state for this tick has
- already advanced.
-
-When in doubt, the test in `tick_systems_test.go` is the executable
-specification.
diff --git a/dev_guide/video_poker.md b/dev_guide/video_poker.md
deleted file mode 100644
index 46d900e..0000000
--- a/dev_guide/video_poker.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# Video poker internals
-
-Builder-facing cabinet configuration, payout tiers and player controls
-live in [../building_guide/video_poker.md](../building_guide/video_poker.md).
-This page covers presentation and money-safety internals.
-
-## Presentation
-
-Pay tables use yellow Unicode borders and dotted hand-name leaders. Blue
-`#000269`, red `#fe0000` (selected coin column), and yellow `#f7e70e` are mapped
-to nearest xterm colors. HELD and wins are bold yellow; no-win text is bold
-white. Results are below cards. Bonus indicators and multipliers sit beside
-cards. Multi-hand art shrinks; hundred-play uses compact Unicode ranks/suits.
-
-Payouts are total coins including stake, not profit. Multiply by denomination
-for credits. Natural royals pay 250 per coin below five coins, 4000 at five.
-Other categories scale linearly. Only the highest-paying category pays.
-All games use fresh 52-card decks except Joker Poker's 53-card deck. Each line
-shares the deal/holds but draws from its own shuffled remaining deck, excluding
-**all five** original cards, including discards.
-
-## Shared banks and money safety
-
-Ultimate X multipliers belong to the **machine**, not the player. Another
-occupant inherits them. Files live at
-`data/casino/video_poker_banks/<room>_<cabinet>.yaml`. Within each file banks
-are keyed by game/tier/denomination/hand-count, preventing denomination or
-edition switching from amplifying an award. Fully consumed banks are removed.
-Missing files are new machines; corrupt/unreadable files block deals.
-
-`casino.PokerWallet` loads banks and atomically settles character credits,
-pending stake removal, and the cabinet file in a multi-file undo-journal
-transaction. There is no package-global persistence callback. The game owner
-serializes writes. The stake is durable before revealing cards; frozen final
-hands and awards survive payment retries without redraws or rerolls. Failed
-transactions leave the old live bank intact. Commit-point failures are recovered
-and reconciled before any retry; unrecoverable storage faults fail closed.
-Startup journal recovery precedes pending-wager recovery. A process crash before
-commit restores both old records, then refunds the unresolved stake. Committed
-payouts cannot replay an old bank. Shutdown/eviction refunds unresolved stakes
-without consuming previously saved multipliers or retaining uncommitted awards.
-
-Old experimental player-owned `video_poker_multipliers` fields are no longer
-read. They are not merged into shared cabinets (multiple player banks cannot
-unambiguously become one physical bank).
diff --git a/internal/casino/core.go b/internal/casino/core.go
index 8ab44f5..f51dde3 100644
--- a/internal/casino/core.go
+++ b/internal/casino/core.go
@@ -156,8 +156,8 @@ func resolveStakeIDs(w Wallet, ids []string) {
// settleStakeOutcome applies one wager's outcome through the wallet and
// reports whether the settlement could NOT be made durable. It carries the
-// money-critical ordering shared by every table game (documented in
-// dev_guide/concurrency.md): compute the payout, pay it via PayCredits
+// money-critical ordering shared by every table game: compute the payout,
+// pay it via PayCredits
// BEFORE announcing the win — that one save credits the payout and resolves
// the stakes it returns, so a crash can never pay the payout AND refund the
// same stake — and only then stand the hand down. On a persistence failure
diff --git a/internal/casino/video_poker_config.go b/internal/casino/video_poker_config.go
index 0a39913..88fcbc4 100644
--- a/internal/casino/video_poker_config.go
+++ b/internal/casino/video_poker_config.go
@@ -56,7 +56,7 @@ var cabinetGames = map[string][]string{
// Each tuple is low, normal, full. These are named published schedules, not
// interpolated payouts. Full means the highest offered on this cabinet, not
-// a promise of identical RTP across games. Sources: dev_guide/video_poker.md.
+// a promise of identical RTP across games.
var cabinetSchedules = map[string]map[string][3]string{
CabinetClassic: {
"jacks_or_better": {"7/5", "8/5", "9/6"},
diff --git a/internal/casino/video_poker_rules.go b/internal/casino/video_poker_rules.go
index bfff0c4..29ab5c3 100644
--- a/internal/casino/video_poker_rules.go
+++ b/internal/casino/video_poker_rules.go
@@ -14,8 +14,7 @@ type pokerSchedule struct {
var pokerCategories = []string{"Natural royal flush", "Royal flush", "Four deuces", "Five of a kind", "Wild royal flush", "Straight flush", "Four aces with 2-4 kicker", "Four 2s-4s with A-4 kicker", "Four aces", "Four 2s-4s", "Four 5s-Ks", "Four of a kind", "Full house", "Flush", "Straight", "Three of a kind", "Two pair", "Jacks or better", "Kings or better"}
// pokerPayTable resolves the per-coin (five-coin) rates for a resolved
-// game/pay-table pair. Every listed schedule is a published one; see
-// dev_guide/video_poker.md for the sources.
+// game/pay-table pair. Every listed schedule is a published one.
func pokerPayTable(c VideoPokerConfig) (pokerSchedule, bool) {
c = c.Normalize()
if c.Variant == "" {
diff --git a/internal/game/act_burn.go b/internal/game/act_burn.go
index 706d0a7..4b3b437 100644
--- a/internal/game/act_burn.go
+++ b/internal/game/act_burn.go
@@ -424,7 +424,7 @@ func (g *Game) broadcastDrop(p *player.Player, itemID, itemName string) {
func (g *Game) FireTick() {
// Mandatory tick-system guard: the master per-tick subscriber can fire
// before SetHub during startup, and burning out a fire reaches
- // cancelStokers -> g.Hub.PlayersInRoom. See dev_guide/tick_order.md.
+ // cancelStokers -> g.Hub.PlayersInRoom.
if g.Hub == nil {
return
}
diff --git a/internal/game/hacking/liars_dice_test.go b/internal/game/hacking/liars_dice_test.go
index 83bed93..03f2b8d 100644
--- a/internal/game/hacking/liars_dice_test.go
+++ b/internal/game/hacking/liars_dice_test.go
@@ -10,7 +10,6 @@ import (
// "illegal use of non-zero Builder copied by value" on ordinary input — a
// forced challenge resolves on the very first bid. A panic here runs on the
// game owner goroutine, which has no recovery, so it terminated the server.
-// See dev_guide/concurrency.md for the owner's no-recovery guarantee.
func TestLiarsDiceForcedChallengeDoesNotPanic(t *testing.T) {
g := NewLiarsDiceGame()
diff --git a/internal/game/hardening_tick_lifecycle_test.go b/internal/game/hardening_tick_lifecycle_test.go
index d16c2c8..d9a348a 100644
--- a/internal/game/hardening_tick_lifecycle_test.go
+++ b/internal/game/hardening_tick_lifecycle_test.go
@@ -50,8 +50,7 @@ func TestFireTickNilHubIsSafe(t *testing.T) {
// TestSequenceTickFiresInDeterministicOrder guards the sorted iteration over
// g.sequences: map iteration randomly chose which simultaneous sequence
-// advanced first, contradicting the determinism contract in
-// dev_guide/tick_order.md.
+// advanced first, contradicting the deterministic-order contract.
func TestSequenceTickFiresInDeterministicOrder(t *testing.T) {
g := escapeTestGame()
g.sequences = make(map[string]*sequence)
diff --git a/internal/game/money.go b/internal/game/money.go
index 4c3316e..6317df8 100644
--- a/internal/game/money.go
+++ b/internal/game/money.go
@@ -6,8 +6,7 @@ package game
// economy journal and no two-phase recovery: startup simply refunds any stake
// still listed as pending, because a listed stake means the process died
// before the round resolved. Two-phase journal recovery was deliberately
-// removed as out of scope for a game this size; see
-// dev_guide/concurrency.md ("Durable money") before reintroducing it.
+// removed as out of scope for a game this size.
import (
"crypto/rand"
@@ -128,7 +127,7 @@ func addCreditsChecked(a, b int) (int, error) {
// same save that settled them.
//
// This is intentionally the entire recovery story. Two-phase journal recovery
-// is out of scope for this game (see dev_guide/concurrency.md).
+// is out of scope for this game.
func (g *Game) recoverPendingWagers() {
names, err := g.AccountStore.ListCharacterNames()
if err != nil {
diff --git a/internal/game/sys_triggers.go b/internal/game/sys_triggers.go
index d1ce336..790eb12 100644
--- a/internal/game/sys_triggers.go
+++ b/internal/game/sys_triggers.go
@@ -101,8 +101,7 @@ func (g *Game) startSequence(sess *net.Session, p *player.Player, steps []behavi
func (g *Game) SequenceTick() {
g.seqMu.Lock()
// g.sequences is a map: collect its keys and sort them so the per-tick
- // order in which simultaneous sequences advance is reproducible (the
- // determinism contract in dev_guide/tick_order.md).
+ // order in which simultaneous sequences advance is reproducible.
keys := make([]string, 0, len(g.sequences))
for key := range g.sequences {
keys = append(keys, key)
diff --git a/internal/game/tick_systems.go b/internal/game/tick_systems.go
index f85cdfa..8311ccf 100644
--- a/internal/game/tick_systems.go
+++ b/internal/game/tick_systems.go
@@ -1,15 +1,15 @@
package game
// tickSystem is one step of the canonical per-tick system sequence. Name is
-// the human-readable identifier also encoded in dev_guide/tick_order.md;
-// Run invokes the underlying system. The slice tickSystemOrder is the single
+// the human-readable identifier for the step; Run invokes the underlying
+// system. The slice tickSystemOrder is the single
// executable source of truth for the order in which per-tick systems fire.
//
// The order is what callers like the master subscriber in cmd/thoi/main.go
// (registered as engine subscription ID 1, which the engine fires before all
// dynamically-registered combat/aggro/respawn callbacks per processTick's
// registration-ID sort) depend on. Reordering this slice is a breaking change
-// to game behavior; keep dev_guide/tick_order.md in sync.
+// to game behavior.
type tickSystem struct {
Name string
Run func(*Game)
diff --git a/internal/game/tick_systems_test.go b/internal/game/tick_systems_test.go
index 82028d6..a701fd7 100644
--- a/internal/game/tick_systems_test.go
+++ b/internal/game/tick_systems_test.go
@@ -13,8 +13,8 @@ import (
)
// expectedTickSystemOrder is the canonical per-tick system sequence. It MUST
-// stay in sync with tickSystemOrder in tick_systems.go and with
-// dev_guide/tick_order.md. Updating one without the others is a bug.
+// stay in sync with tickSystemOrder in tick_systems.go. Updating one without
+// the other is a bug.
//
// The list is intentionally spelled out here (rather than derived from
// tickSystemOrder) so the test fails loudly when someone reorders, adds, or
@@ -48,17 +48,16 @@ var expectedTickSystemOrder = []string{
// TestTickSystemOrder asserts the canonical per-tick system sequence matches
// the documented order exactly. This is the regression guard referenced in
-// dev_guide/tick_order.md and in the master subscriber doc comment in
-// cmd/thoi/main.go.
+// the master subscriber doc comment in cmd/thoi/main.go.
func TestTickSystemOrder(t *testing.T) {
got := TickSystemOrder()
if len(got) != len(expectedTickSystemOrder) {
- t.Fatalf("tick system count changed: got %d, want %d (update expectedTickSystemOrder and dev_guide/tick_order.md)",
+ t.Fatalf("tick system count changed: got %d, want %d (update expectedTickSystemOrder)",
len(got), len(expectedTickSystemOrder))
}
for i, want := range expectedTickSystemOrder {
if got[i].Name != want {
- t.Errorf("tick system %d: got %q, want %q (reordered? update dev_guide/tick_order.md)",
+ t.Errorf("tick system %d: got %q, want %q (reordered?)",
i, got[i].Name, want)
}
}
diff --git a/internal/game/wallet_test.go b/internal/game/wallet_test.go
index a74ccd5..56789d8 100644
--- a/internal/game/wallet_test.go
+++ b/internal/game/wallet_test.go
@@ -3,8 +3,7 @@ package game
// Single-file money wallet regressions: a wager debits and records a pending
// stake in one atomic character save, a settlement credits and clears the
// stakes it returns in one save, a failed save reverts, and startup refunds
-// anything left in flight. See internal/game/money.go for the design and
-// dev_guide/concurrency.md for why two-phase recovery is out of scope.
+// anything left in flight. See internal/game/money.go for the design.
import (
"os"