At a glance

Crate
crates/engine-modes (legaia_engine_modes), depending on engine-system (pad layout) and expansion-format (pack schema).
Contract
session::ModeSession: tick(&[PadWord]) -> Vec<Event>, view(), state_hash(), is_finished(), results().
Math
fixed: 20.12 fixed point, 12-bit angles, table sine, integer atan2 and square root.
RNG
session::ModeRng (SplitMix64), one per session, seeded from the config.
Modes
poker, cards + cards::duel (with the economy in cards::economy, cards::screen, cards::trade), golf, racing, prop_hunt.
Pack schema
[[poker]], [[card_duels]], [[golf]], [[races]], [[prop_hunt]] in modes.toml; [[cards]], starter, [[boosters]], [[dealers]], [[rewards]] in cards.toml (format page).
Example pack
packs/sample/modes.toml, packs/sample/cards.toml.
Built-in set
crates/engine-modes/builtin/: one practice instance per mode, offered with no pack.
Host seat
legaia_engine_expansion::modes::PackModeSeat on World::expansion, under SceneMode::Expansion.
Screens
legaia_engine_ui::expansion_modes, projected by legaia_engine_screens::expansion_modes.
Tests
Per-module rule tests; crates/engine-modes/tests/ determinism, random-pad soak and the sample pack conversion (below).

The session contract

A mode session is a pure function of three things: its config (including the seed), the order of its ticks, and the pad words fed to each tick. tick takes one raw pad word per seat (PadButton layout); a short slice reads as released pads. A seat is a human or an AI of a tier (Novice, Regular, Expert); an AI seat ignores its pad word and decides from the session's own RNG, so a missing player becomes an AI seat without changing the input shape.

Config + seeda pack instance Pad wordsper seat, per frame ModeSessiontick(&[PadWord]) view()snapshot the host draws state_hash()FNV-1a, peers compare results()places, rewards is_finished()
The whole surface a host sees. Nothing outside the config, the tick order and the pad words reaches the rules, which is what lets the same session run under lockstep netplay.

A host draws only from view(), an owned snapshot, and changes the session only through tick. Each mode also exposes its rule entry points (such as PokerSession::act or GolfSession::strike), which the pad path itself ends in; tests and alternative hosts use them directly. When is_finished() turns true, results() gives placements (1-based, ties sharing a place) and the rewards each seat takes away: a currency change, a booster pack or a card, all by pack id. Granting them to a save is the host's job.

Why lockstep holds

Lockstep netplay sends only pad words between peers; every peer runs the full session and must arrive at the same state after every tick. The kernels hold to four rules:

  • No floats in state. Positions, velocities and speeds are 20.12 fixed point (fixed::Fx). f64::sin and f64::sqrt can round differently across libm builds, CPUs and wasm, so the trig is a literal quarter-wave sine table with linear interpolation, atan2 is a binary search over it, and square roots are integer Newton. Products that could overflow widen to i64 / i128.
  • No ambient randomness. Each session owns a ModeRng; AI decisions, shuffles, Monte Carlo equity samples and booster rolls all draw from it. No clock, no global generator, no host state.
  • No unordered iteration. State is Vec and BTreeMap, never a hash map.
  • A checkable digest. state_hash() is FNV-1a 64 over every field the rules read, written field by field (not std's DefaultHasher, whose algorithm is not stable across Rust releases). Peers compare it to detect a desync; the determinism tests compare it frame by frame.
Lockstep golf, racing, prop hunt Peer Afull session Peer Bfull session pad words, every frame checkpoint hashes every peer holds the whole state a desync is caught at a checkpoint a dropped seat goes to the AI Host-authoritative view stream poker, Grid Duel Hostruns the session Guestlabels only guest: pad changes host: StateBlob per seat a guest sees only its own seat poker hole cards hidden to showdown ends if the host leaves
Which model a mode runs under online. The real-time modes hide nothing and run in lockstep; the turn-based tables run on the host so hidden cards stay hidden (netplay in the hosts).

Hidden information (poker hole cards) would be part of every peer's state under lockstep, so netplay does not run the turn-based modes that way: poker and Grid Duel run on the host alone, which sends each guest only the view its seat may see. The real-time modes - golf, racing, prop hunt - hide nothing and run in lockstep.

Prop hunt is geometry-agnostic: its built-in Arena (bounds and rectangle blockers) is enough to play and test the rules, and a host with real scene collision feeds positions through PropHuntSession::tick_with_positions, which makes those positions part of the lockstep input.

Poker

No-limit Texas Hold'em, 2 to 6 seats. Every seat buys in for the table's chips; a pack's chip_value and currency (gold or casino coins) turn the final stack into the currency reward, (chips - buy_in) * chip_value.

The table rules: blinds, betting, pots, showdown
  • Blinds. The seat after the button posts the small blind and the next the big blind. Heads-up, the button posts the small blind, acts first preflop and last afterwards. blind_double_every doubles both every N hands.
  • Betting. Preflop the current bet is the big blind even when the big blind is all-in for less, and the big blind keeps its option. A raise must add at least the last full raise (the big blind at a street's start). An all-in that raises by less is a short raise: every other seat acts again, but a seat that had already acted on the street may only call or fold.
  • Run-out. When at most one seat can still bet, the rest of the board is dealt straight to showdown.
  • Pots. Each distinct contribution level of a seat still in the hand closes a pot holding what every seat put in between the previous level and this one; seats that reached the level are eligible. Folded chips above the top live level join the last pot, and a bet nobody called returns as a one-seat pot.
  • Showdown. The evaluator scores every 5-card subset of the 7 cards and packs category plus five tie-break ranks into one comparable u32; the wheel (A-2-3-4-5) is a five-high straight. A split pot's odd chips go one at a time to the tied winners nearest the button's left. The evaluator test enumerates all 2,598,960 five-card hands against the textbook category counts.
  • End. One seat holds every chip, every human has busted, or max_hands is reached.

The pad menu: Left / Right choose Fold, Check-Call, Raise or All-in; Up / Down size the raise in big-blind steps; Cross commits. AI seats reduce a spot to a win equity: the novice guesses it from its hand category and calls loosely; the regular and the expert sample it by Monte Carlo against random holdings (60 and 200 deals), compare it with pot odds and their fair share of the pot, and the expert bluffs now and then.

Trading cards

A pack's cards.toml defines the cards. Each has an id, a name and flavour text of the pack's own, a rarity (common, uncommon, rare, legendary), an optional element, four side strengths for the duel, and a disc coordinate for its art (a retail monster by id, an item). cards::CardBook wraps the section:

PieceRule
BoostersA set has a size, an optional pool (default every card), rarity weights and an optional guaranteed rarity for the last slot. Each slot rolls a rarity on the weights (a rarity the pool lacks weighs zero), then a card uniformly within it.
RewardsA rule ties a source (retail monster id, pack formation, pack boss or pack mode id) to a booster or card, a chance and a count; roll_rewards rolls each.
SellingA card's own sell price, else the section's per-rarity table, else a built-in default.
CollectionCopies per id plus a binder order: every owned id once, new ids appended, the last copy leaving the binder. Serde-derived; normalize repairs one read from an older save.
TradesA TradeOffer lists what each side gives; execute validates both collections first, then moves every line. A refused trade changes nothing.

The card economy

The player's cards are pack progress: cards::economy::CardProgress (the collection, unopened boosters by set, the chosen duel deck, the ids already shown, whether the starter cards were handed over, and the card stream's state) sits in ExpansionProgress::cards and rides the save's expansion block as tail record 3, written only once it holds anything - so a save with no card state, and every save made without a pack, keeps its bytes. Like quests, it follows the pack: parked in the dormant block when the pack is deactivated, adopted again when it returns. legaia_engine_expansion::cards is the plumbing around it, and world::expansion_cards the World side.

  • Grants. Quest rewards (quests.card_inbox) and mode wins (pending_card_rewards) are drained by World::collect_card_grants, which the quest settle and the mode settle both call; battle drops are credited where they roll. An id the book does not know is dropped. Each credited grant raises a CardNotice, shown as a toast ("Got card: Mist Wisp (new!)") one at a time for 150 frames each (the rest queue behind it), one row under the quest toast, and kept for a host to take (World::take_card_notices).
  • Starter. The section's starter cards are handed over once, the first time the card screen opens or a pack duel is dealt.
  • Battle drops. World::apply_battle_loot, after the retail spoils and the pack boss reward, makes one call into expansion_card_drops: the sources are each defeated seat's retail monster id, the pack formation's local key and the pack boss whose formation was fought. A no-reward fight rolls nothing.
  • The card stream. Every card roll draws from CardProgress::rng, a SplitMix stream saved with the collection, seeded once from the world's frame counter and RNG word mixed with the pack id. It never touches the game's RNG: a fight's spoils, its item drop and the world's RNG word come out identical with a pack active or not, and a reloaded save opens the same boosters.
  • Shop. buy_booster charges a dealer's priced stock out of the party purse; open_booster consumes one and adds its cards; sell_card pays the dealer's sell_percent of the card's sell value, clamped at 9,999,999.
  • Decks and stakes. A pack Grid Duel deals the player CardProgress::duel_deck: the chosen deck while all five are still owned, else the strongest five owned cards, else - with fewer than five owned - the duel's lent starter (the first five cards of the section). Under stakes the winner's take is real: a won card arrives as a pending reward, and a card the opponent takes leaves the collection (settle_card_stakes; a lent card costs nothing). The built-in practice duel keeps the starter deck.

Trading between players

cards::trade::TradeTable runs one trade on an authority - the netplay host. The proposer puts an offer on the table, which bumps its revision and clears both confirmations; each side confirms that revision; once both have, the host's settle validates the offer against its own collection and the partner's last reported holdings and swaps, every line or none. A failed validation reopens the table with the confirmations cleared. The partner then applies its half with TradeOffer::apply_one_side, validated against its own collection.

The wire form is TradeMsg (holdings, offer, confirm, cancel, settled, refused) with its own byte codec. It travels as the payload of the netplay protocol's existing host-authoritative messages - Action from a client, Event from the host - so trades need no message of their own.

Card screens

cards::screen::CardScreen is one pad-driven state machine over a CardProgress, the party purse and the card book; World seats it as ExpansionState::cards.seat under SceneMode::Expansion, and World::tick_pack_mode hands it the frame before the mode seat.

  1. Card menuBinder, Open boosters, Sell cards, Duel deck · World::open_card_menu
  2. Dealer counterpriced stock as Buy rows, then Sell, Open, Binder · a map's card_dealer trigger
  3. Binder4 x 2 per page; L1 / R1 page, Square sort, Triangle filter
  4. Card detailportrait, sides on a cross, flavour; Left / Right browse
  5. Boosters → Revealopen a set; Cross turns the next card, Triangle all
  6. Sell / Deck buildersell one copy; build the five-card duel deck
StageShows and does
Card menuBinder, Open boosters, Sell cards, Duel deck. Opened by World::open_card_menu.
Dealer counterThe dealer's priced stock as Buy rows, then Sell cards, Open boosters, Binder. Opened by a map's card_dealer trigger (World::open_card_dealer).
BinderThe whole set four by two per page: an owned card's face (portrait, element strip, name, sides, copies, a NEW tag), a missing one as a numbered blank. Square cycles the sort (set order, newest last, rarity, strength, copies), Triangle the filter (all, owned, missing, each rarity).
Card detailThe portrait large, the sides on a cross, rarity, element, copies, sell value and flavour text.
BoostersUnopened sets with their counts and what a pack holds; Cross opens one.
RevealThe pack's cards face down; a card never owned before is marked NEW.
SellOwned cards with what the dealer pays; Cross sells one copy.
Deck builderOwned cards with copies in the deck, and its five slots; Cross adds, Square removes the last, Triangle fills with the strongest, Circle keeps a full deck.

Circle backs out a stage and Start leaves from any; the header shows the purse and the set completion. legaia_engine_ui::expansion_modes::card_screen_prims / card_screen_texts draw every stage, routed through the same pack_mode_prims / pack_mode_stage_texts calls as the mode screens, so both hosts draw them with no code of their own. Toasts ride pack_mode_stage_texts in every mode: over play at the top of the stage, above the footer inside a mode screen; the card screen marks new cards with its own NEW tags and shows none.

Portraits

A card whose art is a retail monster shows that monster. The first time the card screen opens with a pack whose portraits are not built, the scene host renders one 64 x 48 RGBA image per monster off the player's own disc (legaia_actor_formats::monster_portrait): the battle mesh and texture pool staged as battle slot 0 (boss effect page included), posed in the idle action's first frame, drawn by the mesh_raster software rasteriser from a three-quarter view. The images stay in memory on the world (ExpansionState::cards.portraits); a pack carries only the monster id.

The card screens draw an image as flat stage-pixel runs of equal 15-bit colour, scaled nearest to its box, so it reaches both hosts through the ordinary screen-prim pass with no texture upload. Without a disc a card shows its element colour in the art box. A Grid Duel draws its cards from a texture page instead (below), and opening a duel builds the portraits as the card screen does.

Grid Duel

The card battle. Two players, five cards each with both hands face up, a 3x3 grid; a seeded coin flip picks who starts. Players alternate placing a card on an empty cell (the starter places five, the other four).

  • Capture. A placed card takes each orthogonally adjacent opponent card whose facing side is strictly lower than its own.
  • Elemental cells (optional). One to three cells carry an element; a card of that element plays every side at +1 there, a card of another element at -1 (never below 1).
  • Chain (optional). Each card a placement captured then captures its own weaker opponent neighbours, one step and no further.
  • Score. Owned grid cards plus unplayed hand cards; more wins, equal draws.
  • Stakes (optional). The winner takes one card of the loser's deck: picked with the pad, or the strongest for an AI winner. In a pack the card moves between collections.

The novice AI plays a random legal move; the regular maximises immediate captures while not exposing weak sides; the expert also subtracts the opponent's best reply.

A duel with no table in the world draws as its 2D board: the grid on a gold-rimmed felt, each hand fanned down its side with the selected card slid out, the score plates, the turn line and the selected card's panel - its full name, rarity, the four strengths, and what the cursor's elemental cell does to it.

The card page

Every card a duel shows, on the 2D board and on the table alike, is a textured quad sampling one 15-bit texture page the world writes into VRAM while a duel is seated (cards::table_art, written by World::step_duel_card_page inside the field VRAM step both hosts already run and re-upload after): seat 0's five faces, seat 1's five, and the card back.

  • A face is 52 x 72 texels: a bevelled frame in its rarity's metal (bronze, silver, blue steel, gold), the monster's portrait box-filtered into a window over a vignette in its element's colour, an element gem, the four strengths on a cross, and rarity pips.
  • The owner's colour shows through the face's transparent body from a quad drawn under it, so a captured card changes colour with no second texture.
  • Where it lives. (0, 256), 208 x 216 halfwords, in retail's display-buffer band; the pack-map VRAM allocator keeps the rectangle out of its free list, and the words it covers are put back when the duel closes. No disc bytes are written.

Golf

A course is a list of holes, each a tile grid written as strings in modes.toml, with an optional height-digit grid of the same shape. A tile is 32 world units.

CharTile
.fairway
,rough
ssand
wwater (hazard)
#wall
spaceout of bounds (hazard)
T / Otee / cup (both fairway)
^ v < >fairway sloping downhill that way
  • Ball. A point integrated in four sub-steps per frame: arrow slopes and the negative height gradient accelerate it, surface friction (fairway, then rough, then sand, each stronger) and a small constant drag slow it, each axis moves separately and reflects off walls at 60 %, and a ball inside the cup radius below a speed limit drops. A ball that settles below a rest speed for 20 frames, or rolls for 20 seconds, stops.
  • Hazards. Water or out of bounds costs a penalty stroke and replays from where the ball was struck.
  • Stroke. Left / Right aim (Square held for fine steps), Cross starts a power meter that climbs to 100 and falls back, Cross again strikes (Circle returns to aiming).
  • Order. Tee shots in seat order on the first hole, then lowest previous-hole score first; afterwards the ball farthest from the cup plays. A player who reaches the hole's cap (max_strokes, default par + 5) picks up and scores the cap.
  • Result. Lowest total strokes wins.

AI seats look ahead with the same physics: they simulate candidate aims and powers, keep the shot ending nearest the cup (holing out first, hazards last), then perturb it by their tier's error.

Racing

A track is a centerline polyline with a half-width: a circuit closes back on its first point and is run for laps, a sprint runs once from the first point to the last. Every point within the half-width of a segment is drivable, and every centerline point is a checkpoint gate.

  • Checkpoints. A racer passes the next checkpoint when the segment nearest to it becomes the segment leaving that checkpoint. The switch happens on the corner's bisector, so a cut corner still counts and a skipped segment never does; only the next checkpoint in order counts. On a circuit, passing checkpoint 0 closes a lap; a sprint finishes on crossing the line through its last point.
  • Driving. Heading plus scalar speed: Cross accelerates, Square brakes, Left / Right steer, R1 spends a full boost meter (it refills a unit a frame), and boost-pad segments give a short boost free. Triangle uses the power-up the racer carries.
  • Contact. At the edge the racer is pushed back inside, the velocity's outward component is dropped and the rest turns the heading along the edge at seven eighths speed. Overlapping racers are pushed apart and the one behind loses a quarter of its speed. A finished racer coasts and no longer collides.
  • End. All racers finished, finish_grace frames after the first finisher, or the time limit; unfinished racers place behind the finishers by laps, checkpoints and distance to the next one.

AI racers steer by pure pursuit at a point two half-widths ahead along the centerline, offset by a per-racer line; tiers cap their top speed, brake for sharp turns and use the boost.

Power-ups

A race's pickups lie on and beside the course. A racer with nothing in hand takes one by running over it, carries it and uses it with Triangle; the spot stands empty for its respawn delay (600 frames by default) and comes back.

Power-upEffect
Boost100 frames at boost speed
Hop40 frames in the air: no bumps, and the edge lets the racer run a half-width wider - to cut a corner or reach a pickup off the course
Shield600 frames of free bumps; blocks the next slow, and is spent doing so
SlowEvery rival runs 120 frames at 55% of its top speed; a shielded rival loses its shield instead

AI racers reach for a ready power-up lying on the track just ahead and use what they carry by rule: a boost on a straight, a hop into a sharp turn or over a rival, a shield at once, a slow when not leading. The spots and every racer's power-ups are in the state hash, so a race with them replays and runs in lockstep like any other.

Prop hunt

Seats split into hunters and hiders, rotating each round so every seat hunts in turn.

  1. Choosehiders pick a prop from the arena's list; the cursor's prop is taken when the timer runs out
  2. Hidehiders move; hunters are held at the spawn until the hide timer ends
  3. Hunthunters inspect the nearest prop in reach; hiders may taunt

An inspect checks the nearest prop within reach (its radius plus a fixed reach), a hider winning a distance tie. A disguised hider is found and out for the round; a decoy costs the hunter miss_penalty health, and a hunter at zero is out; nothing in reach is a free whiff. Inspects have a cooldown. A taunt (Triangle) earns a few points and reports the hider's position to every host.

The round ends when every hider is found (hunters win), every hunter is out, or the timer expires (hiders win). Hiders score a point per second of hunt survived and a bonus for surviving; hunters score per find and a bonus for a won round. After the last round the highest total wins.

AI hiders stand beside a decoy of their own prop (the expert picks the prop with the most decoys), and novices fidget and taunt. AI hunters inspect unchecked props nearest first; regulars and experts prefer a prop they saw move.

In-world golf and racing

A golf course or a race that names a pack map (map = "<id>" in modes.toml) is played in the 3D field scene that map builds instead of on a top-down board; a race may instead name a kingdom overworld (scene = "map01") and run across it. The session stays the only authority: the map only lays the course or track out, and what is drawn is read off the session's view each frame and never fed back. An instance with no map is always the top-down screen.

  • Laying it out. A map hole is the map's tile grid inside the hole's area, one golf tile per map tile: a walled tile is a wall, a void tile or anything outside the area is out of bounds, and an open tile plays as its legend surface. A race's centerline is its waypoints' tile centres. MapFrame places the mode units on the field - four world units a mode unit for golf, two for racing.
  • The stage. WorldStage turns the view into a frame: in golf the party leader stands at the player's ball facing the aim, swings and walks to the new lie, other seats stand at their balls, and the camera sits behind the ball looking down its aim; in a race the leader is the player's racer, lifted through a hop, and the camera chases from behind; the track carries a gate over every checkpoint, the next in gold, and the power-ups floating over their spots. Banners carry the hole, par, countdown, laps and results.
  • Bodies and online play. The leader is the session's local seat, so a networked peer stands its own golfer or racer. An instance's bodies names map NPCs that stand in for the other seats, posed and animated by World::stage_pack_mode.
  • Entering the map. World::pack_mode_wants_map names the map and the scene host enters it at the instance's spawn, then puts the seat back under the expansion mode; a map that fails to enter falls back to the top-down screen.
  • On the overworld. World::pack_mode_wants_scene names a race's overworld; the scene host enters it under the seat, noting where the party stood, and takes the party back there - scene, pack map, pose and mode - when the seat closes. The racers, kerbs, gates and power-ups follow the overworld's heightfield through the floor sampler, the native window draws the overworld under the seat as in the overworld walk, and nothing of the walk runs (no encounters, no entrances). The sample pack's courses are checked against map01's collision grid by a disc-gated test.
  • Drawing it. Both hosts draw the field as for any scene, with the camera from the shared resolver's venue arm, and the balls, cup, aim arrow, bodies and boost trail as depth-tested screen primitives built in code from no disc bytes, under the in-world HUD.

Party night

party::PartyNight chains short rounds of the other modes into one points-based night: a round-intro card, a scoreboard after every round, a podium and prizes. It plays no round itself: a host ticks it once a frame, builds each cued round from party::round_section (the base instance cut short by the round's knobs, widened to seat everyone), plays it and reports the placements back. Two machines that build the night from the same definition, players and seed and report the same results hold the same scoreboard, which scoreboard_hash digests (timers excluded). A night is a [[party_nights]] table in modes.toml; the built-in set carries Practice Night, so a night is playable with no pack.

RuleHow it scores
Placementpoints[place - 1]; tied seats share a place and its points; a place past the table scores 0.
Double pointsA round marked double, and the final round with double_final (the default).
WagerEvery player still in stakes wager percent of their points; the round's winners split the pot.
Last place picksBefore every round after the first, last place picks the next from those left; an AI rival at once, a slow pick after ten seconds.
Grid DuelKing of the hill: the two lowest duel first, the winner holds the hill, the leader defends last; a draw keeps the king.
VoidA round that cannot finish scores nothing and takes no stakes; the night goes on.
LeavingThe total stands; nothing more is scored and no prize won.
PrizesBy final place: gold, coins, boosters, cards. None for a night ended early or one whose every round was void.

Played alone

World::enter_pack_mode with a night's id (a map trigger's mode event, a dialogue choice) opens the night against its AI rivals under SceneMode::Expansion. Between rounds the mode screen draws the party card; a round plays on the seat like any instance but settles nothing of its own, and its results stand three seconds before the night scores them. A Grid Duel match between two rivals plays out at once. Start on a party card ends the night early; any button closes the podium. Online, the room's host directs one copy per player (netplay).

The party card is one engine-ui composition for both hosts: the intro card slides in with the round's kind, title, detail and tags over the standings; the scoreboard's rows step in with each player's last-round points; the podium raises 2nd / 1st / 3rd blocks with a figure in each player's colour under falling confetti.

A night whose pack names a podium ends on it in the world, alone or online. Online every peer raises it from its own verified copy of the night at the pack's anchor (World::show_net_podium), so the blocks agree everywhere; each peer stands its own leader on its block, and the others are their roaming avatars, or stand-ins where they are not in view. Nothing about it travels.

Grid Duel at a table

A duel that names a pack map and a table (map and [card_duels.table] in modes.toml) is played on a board laid on a table in the 3D field scene: the opponent seated across it, the party leader standing at the player's side, spectators around, and the camera taking its shots. The session is unchanged and the only authority; the table reads its view each frame and feeds nothing back.

  • The layout. DuelTableFrame places the table in its own axes - across from the player's seat toward the opponent's, and the player's right hand. Each hand is fanned on its seat's edge of the board, tilted toward its holder: the player reads their faces, the opponent's show their backs (whether a card shows its face is the projected quad's winding). The layout is made from the local seat, so each networked peer sits in the near seat and sees the table from their own chair.
  • The timeline (duel_stage::DuelStage, its own frame clock, deterministic): the deal flies the ten cards from the dealer's pile into the hands; a placed card slides from the hand in an arc and drops onto its cell with a dust ring; each captured card then turns over about its long axis, its owner's colour changing at the half turn with a burst of sparks; a chain capture follows a beat later with a larger burst and "Chain!"; at the end the last flips play out and the result banner goes up.
  • The NPCs. The opponent stands on its seat facing the board and plays its think clip while choosing; spectators keep their tiles and turn to the board. A move taking two or more cards, and the result, set their moods: the spectators cheer for the player and slump against them, the opponent the reverse.
  • The shots. An establishing shot of the room while the cards are dealt, easing into a close shot over the board from behind the player's seat that drifts slowly; a push in over the cell being chosen, a pull back while captures flip, and at the end a low reaction shot across the board on the opponent. The results card waits for it and then draws over the table.
  • Sound. Retail cue ids for the deal, the place, the flip and the result, pushed on the field's SFX ring (the table's sfx, or menu cues).
  • Drawing it. Depth-tested screen primitives in explicit ordering-table bands: a wooden table on four legs (or a prop of the map), the gold-rimmed felt board (or a texture of the base scene), pulsing rings on the elemental cells and the cursor's frame, the cards' shadows, the cards themselves (owner quad, face or back subdivided so the affine mapping bends with the card, a raised card's rim light) and additive sparks. The HUD is only the score plates, the turn prompt and the selected card's panel.

The sample pack plays the Warden's Duel at the card room's table on its 5F floor (--expansion packs/sample --map genesis_5f --mode wardens-duel); the duel_table test pack is the minimal case.

Poker at a table

A poker instance that names a pack map and a table (map and [poker.table]) is dealt on an oval felt table in the 3D field scene, the other seats' NPCs in chairs round it. The session is unchanged and the only authority; poker::table::PokerTableFrame places the table and poker_stage::PokerStage turns each frame's view into cards, chips, NPC poses, sounds and a shot.

  • Chairs and hidden cards. Each peer sits in the near chair and the others keep their seat order round the oval, so a networked game shows every player the table from their own chair. A hole card shows its face only to its own seat, and to everyone at a showdown while its seat is still in; every other card lies face down and the card page holds no face for it at all.
  • What plays. Each hand deals two rounds from the dealer's deck beside the button; the flop, turn and river fly to the board and turn up; bets slide out with the amount floating over them and into the pot when the street closes; folded cards slide in face down; at a showdown the live hands turn over while the camera sweeps half round the table, then holds on the winner; the pot slides to its winner with a burst and a banner naming the hand.
  • The NPCs. Each plays its think clip on its turn, fold as it folds, all_in while all in, cheer on a won pot and slump when it busts.
  • Drawing it. An oval felt lit brightest in the middle, a gold betting line, a padded rail on four legs; chips as stacked discs coloured by the stack's size against the big blind; the dealer button and a pulsing ring at the chair to act. The HUD is a plate per other seat, the pot, the player's own plate with the hand it makes, and the action menu.

The sample pack deals the Tower High Table in the Gambler's Rest hall (--expansion packs/sample --map genesis_5f --mode tower-high-table).

Hosts

Both play hosts run the modes inside the running game, through one seat on the World and one screen composition.

The seat

active::ActiveMode is any one of the five sessions behind a common surface: build an instance by id from a mode section and a card section, tick it, read its view, settle it. PackModeSeat (in engine-expansion) holds one ActiveMode at a time, or the instance picker, or a finished session's results, and the mode the world returns to. World::open_pack_mode_picker and World::enter_pack_mode(id) seat it and switch the world to SceneMode::Expansion; the mode tick then runs World::tick_pack_mode in place of field and battle dispatch, feeding seat 0 the frame's pad word. World::exit_pack_mode closes it from outside (a session still in play is forfeited).

The instances are the active pack's modes.toml (with its cards.toml as the duel's card book) when the pack defines any mode, else the built-in practice set. A pack duel deals the player their collection's deck; the practice duel, and a pack duel before the player owns five cards, deal the first five cards of the card section. A golf round seats one regular AI beside the player.

SceneMode::Expansion is not a retail mode: GameMode::for_scene_mode answers None for it, as for Title, and nothing reaches it without a host entering the seat, so a world that never does plays exactly as before.

Rewards

When a session finishes, the seat settles it: the session's own rewards, plus, when the player placed first, every cards.toml reward rule whose source is that mode's id. Seat 0's grants land at once - a currency change in the party gold purse or the casino coin bank (clamped to 0..=9,999,999), boosters and cards in ExpansionState::pending_card_rewards for the card economy to collect. Quitting through the quit prompt grants nothing, a lost poker stack included.

Entry

HostEntry
Native window` (backquote) on the field or overworld opens the picker; pressed again inside, it leaves. --mode <INSTANCE> enters an instance once the launch scene is entered, --mode picker opens the picker.
Browser play pageThe Game mode select in the play tools: the picker, every instance by name, and Leave the mode.
Card screensNative: \ (backslash) opens and closes the card menu; --mode cards, cards-binder and cards-dealer[:ID] open one at launch. Browser: play_cards_open / play_cards_close, with play_cards_json and play_card_notices_json. A pack map opens a dealer through its card_dealer trigger.

Controls

Inside every mode, Start raises the quit prompt (Cross leaves, Circle resumes). The picker takes Up / Down and Cross, and Circle or Start backs out; the results card leaves on Cross.

ModeControls
PokerLeft / Right: Fold, Check-Call, Raise, All-in. Up / Down: raise size in big blinds. Cross: act, or skip the showdown.
Grid DuelLeft / Right: hand card. Cross: lift it, then D-pad to a cell and Cross to place. Circle: put it back.
GolfLeft / Right: aim (hold Square for fine steps). Cross: start the power meter, Cross again to strike. Circle: back to aiming.
RacingCross: accelerate. Square: brake. Left / Right: steer. R1: boost. Triangle: use the power-up carried.
Prop huntChoose: Left / Right and Cross. Then the D-pad moves; a hunter inspects with Cross, a hider taunts with Triangle.

Screens

legaia_engine_ui::expansion_modes builds every screen in the 320x240 stage as flat panels plus dialog-font labels; mode_screen_prims and mode_screen_texts keep the two halves of one composition, and no panel's geometry depends on a label's measured width. legaia_engine_screens::expansion_modes projects the seat into it: while the seat owns the frame, screen_layers::compose_screen_prims returns its panels as the whole under-text list (only the pause wipe may cover it), and each host's text layer appends pack_mode_stage_texts, scaled with the rest of its stage group.

ScreenShows
PickerEvery instance with its mode kind, the cursor row, the last entry error.
PokerOpponent plates along the far rail (chips, dealer button, card backs, bets, revealed hands at showdown), the board and pot, the player's hole cards and stack, the action menu, the event log.
Grid DuelThe 3x3 grid on its board with elemental cells, both hands fanned down the sides, the score plates, the turn line and the selected card's panel; cards are the card page's faces.
GolfThe hole top-down from its tiles (surfaces, height shading, slope arrows), cup and flag, every ball, the aim line, the power meter and the scorecard.
RacingA functional top-down track map with boost segments, the start and finish lines and the ready power-ups, every racer, and the lap or gate / place / speed / boost / power-up panel.
Prop huntA functional top-down arena with blockers, decoys, hunters and hiders, and the round / timer / role / score panel. A hidden hider draws exactly like a decoy while the player hunts.
ResultsPlacements with each mode's score, and what was won.

Tests

  • Rules. Each module tests its rules directly: hand categories, kickers, ties, the wheel and the exhaustive five-card census; blinds, the big blind's option, minimum raises, short all-ins, side pots and odd chips; booster guarantees, reward chances, trade atomicity and binder order; captures, chains and elemental cells; golf surfaces, walls, hazards, slopes and turn order; checkpoint order, edge sliding and boost; inspect, misses, taunts and role rotation.
  • Determinism. tests/determinism.rs runs each sample-pack mode twice with the same seed and scripted pads and asserts identical state_hash sequences and results, then that a different seed diverges, so the hash cannot be vacuous (compare determinism replay for the whole World).
  • Soak. tests/soak.rs drives every mode with random pads over many seeds and asserts no panic, termination within a frame bound, chip conservation, a full duel grid, a complete golf scorecard within the stroke cap, and no avatar inside a wall.
  • Pack. tests/sample_pack.rs parses the sample pack's cards.toml / modes.toml and converts every instance into a session config.
  • Seat. engine-modes active builds every built-in instance and rolls a won mode's reward rules; engine-expansion modes drives the picker, the quit prompt and the results card.
  • World. engine-core tests/expansion_modes_world.rs enters each built-in instance from the field, drives it to its end through World::tick, and checks the coin bank, the pending card rewards and the restored field.
  • The poker table. engine-modes poker::table and poker::art test the chairs and the page; engine-expansion poker_stage::tests runs AI hands through the stage (hashes unchanged, deterministic frames, hidden cards faceless until a showdown from any chair, NPCs never in the viewer's chair, chips and a won pot).
  • The duel table. engine-modes cards::table and cards::table_art test the table's axes, seats and the card page; engine-expansion duel_stage::tests runs whole AI duels through the stage (session hashes unchanged, same seed same frames, the deal, cells, mid-flip colour change, the far seat's view, the held results); engine-core tests/expansion_duel_table_world.rs plays a duel to its end through World::tick at its table, checks its hashes against the 2D screen and the card page's write and restore; the disc-gated engine-scene-host expansion_duel_table_disc enters the test pack's duel and the sample pack's Warden's Duel through the scene host.
  • Screens. engine-ui expansion_modes::tests checks each screen's key labels and that every panel lies on the stage; expansion_modes::binder checks the card screens' labels, the portrait runs and the toast placement.
  • Card economy. engine-modes cards::economy, cards::screen and cards::trade test buying, opening (the same saved stream opens the same pack), selling, the starter, the duel deck, every screen stage driven by pad, and the trade table's revisions, atomicity and codec. engine-expansion tests the grant drain, battle sources, stakes, toasts and the save block's card record; engine-core world::tests::expansion_cards checks that drops leave the retail spoils and RNG word untouched while crediting the collection; the disc-gated engine-scene-host expansion_cards_disc checks every sample-pack card's art against a real monster record and renders every portrait.