Files
Kevin 4d78937dca Rebuild the board and reducer around the real content
shared/board.js: rebuilt from shared/tileInventory.js instead of the
sketch-inspired placeholder — one chain() per segment (same helper as
before), TEMP_SEGMENT_ORDER concatenating all 12 segments in the design
doc's own order since board-graph.json (the real segment connections/STOP
forks) doesn't exist yet — loudly commented as a placeholder to replace, not
a guess at real topology. A synthetic `finish` space is appended since no
tile in the source data has that type (win condition is itself an open
question). Movement is now one tile per turn (board.spaces[pos].next) —
no tile in the data implies a movement die, every `die` belongs to that
tile's own effect resolution, so walkForward() is gone.

shared/tileEffects.js (new): pure resolveTileEffect(space, rolls) covering
all 9 real tile types (payday, action_space, dice_space, roll_table_ref,
inline_table, cash_bonus, event, choice, stop) plus rollsNeededFor(space) so
the server knows what to pre-roll before constructing an action. Real
roll_table_ref/action_space/dice_space tiles resolve against the actual 48
placeholder tables; the two real content gaps (inline_table's 85 tiles,
payday/cash_bonus amounts) resolve against a synthesized generic banded
table per die size, every result clearly flagged `todo: true` so it's
visible in the game log, not just in docs.

shared/game.js: player state is now flat {cash, love, education, wealth,
age} fields (mirrors a roll-table effect object's shape directly). Landing
logic calls resolveTileEffect() instead of applying a single cash delta, and
only pauses for a decision when a space's `choices` array is actually
populated (true of nothing yet) — keeps the existing pending-choice
machinery intact for once board-graph.json exists, instead of needing
choice/stop tiles to block on options nobody has defined.

server/rooms.js: REQUEST_ROLL now looks up the player's next tile,
determines what it needs via rollsNeededFor(), and pre-rolls each die via
crypto.randomInt before constructing the ROLL action — same
randomness-happens-once-at-the-server-boundary pattern as before, just
covering N table rolls instead of one movement die. server/index.js:
default config now carries startingStats instead of startingCash, and
maxPlayers is 8.
2026-07-28 10:06:59 -07:00

90 lines
3.6 KiB
JavaScript

/**
* Life Journey — the real board: 211 tiles across 12 segments, built from
* shared/tileInventory.js (parsed from the design doc).
*
* TEMPORARY: the 12 segments have no defined connections yet — that's what
* shared/BRANCHING-TEMPLATE.md is an open request for (the STOP forks, and
* which segment's exit feeds which segment's entry). Until board-graph.json
* exists, TEMP_SEGMENT_ORDER below just concatenates all 12 segments in the
* design doc's own listed order into one line, so movement/tests/rendering
* have something real to walk. This is NOT the real board topology — delete
* this placeholder and replace `next` wiring once board-graph.json exists.
*
* Similarly, no tile in the source data has type 'finish' (win condition is
* itself an open question in GAME-REVIEW.md) — a synthetic `finish` space is
* appended after the last tile so the reducer has something to end on.
*
* Movement is one tile per turn (no tile in the data implies a movement die —
* every `die` value belongs to that tile's own effect resolution), so unlike
* the old board there is no walkForward()/multi-step-with-early-stop concept
* here: a turn is just "go to `next`, then resolve whatever that tile needs."
*
* Space shape: { id, type, label, die, externalTables, statsTouched, next }
* type: one of the 9 tile-inventory types — payday, action_space,
* dice_space, roll_table_ref, inline_table, cash_bonus, event, choice, stop
* die: 'D100'|'D20'|'D10'|'D8'|'D6'|'D2'|null — what to roll to resolve this tile
* externalTables: source_doc_id[] into shared/rollTables.js (0-3 entries)
* statsTouched: string[] hint from the source data, informational only
* next: id of the following space (absent only on the synthetic 'finish')
* choices: NOT set on any tile yet (no real branch destinations are known) —
* shared/game.js only pauses a 'choice'/'stop' space for a decision when
* `choices` is non-empty, so today every one of these is a harmless
* pass-through, not a dead end.
*/
import tileInventory from './tileInventory.js';
const TEMP_SEGMENT_ORDER = [
'starting_strip',
'career',
'investment',
'investment_bottom',
'high_risk',
'gap_year',
'education',
'relationship_top',
'relationship_bottom',
'retirement_top',
'retirement_middle',
'retirement_bottom',
];
const segmentsById = Object.fromEntries(tileInventory.segments.map((s) => [s.id, s]));
function buildSegmentSpaces(segmentId, nextAfterSegment) {
const segment = segmentsById[segmentId];
if (!segment) throw new Error(`Unknown segment id in TEMP_SEGMENT_ORDER: ${segmentId}`);
return segment.tiles.map((tile, i) => ({
id: `${segmentId}_${i}`,
type: tile.type,
label: tile.name,
die: tile.die ?? null,
externalTables: tile.external_tables ?? [],
statsTouched: tile.stats_touched ?? [],
next: i < segment.tiles.length - 1 ? `${segmentId}_${i + 1}` : nextAfterSegment,
}));
}
const spaceList = TEMP_SEGMENT_ORDER.flatMap((segmentId, i) => {
const isLastSegment = i === TEMP_SEGMENT_ORDER.length - 1;
const nextAfterSegment = isLastSegment ? 'finish' : `${TEMP_SEGMENT_ORDER[i + 1]}_0`;
return buildSegmentSpaces(segmentId, nextAfterSegment);
});
spaceList.push({
id: 'finish',
type: 'finish',
label: 'Finish',
die: null,
externalTables: [],
statsTouched: [],
// no `next` — this is the temporary end of TEMP_SEGMENT_ORDER, not a real
// win-condition tile from the source data.
});
export const board = {
id: 'life-journey-full-v1',
startSpaceId: `${TEMP_SEGMENT_ORDER[0]}_0`,
spaces: Object.fromEntries(spaceList.map((space) => [space.id, space])),
};