/** * Life Journey — full board. * * Transcribed from the hand-drawn sketch (assets/game_board.png), which uses * many space names more than once across its zones (Start A Business, * Family Reunion, Market Crash, ...) — that repetition is treated as * intentional recurring flavor, not deduplicated. The sketch's arrows get * genuinely ambiguous in a few spots (a hand-drawn/AI-generated mockup, not * an engineered spec), so rather than force a literal reverse-engineering of * every arrow, every distinct label from the sketch is kept and organized * into a clean DAG with the same shape Phase 1 already proved out: a fork, * a chain per branch, a convergence, repeated three times, then a long * shared retirement tail. More spaces can still be inserted into any branch * later — just splice into its chain() array below. * * Space shape: { id, type, label, next?, choices?, cash?, flavor? } * type: 'start' | 'event' | 'money' | 'choice' | 'finish' * next: id of the following space (absent on 'choice' and 'finish' spaces) * choices: [id, ...] of the branches offered by a 'choice' space (2 or more) * cash: fixed integer delta applied on landing (absent/0 for pure flavor spaces) */ /** Wires each entry's `next` to the following entry's id; the last one gets `finalNext`. */ function chain(entries, finalNext) { return entries.map((entry, i) => ({ ...entry, next: i < entries.length - 1 ? entries[i + 1].id : finalNext, })); } const careerChain = chain([ { id: 'career_1', type: 'event', label: 'High School Dropout' }, { id: 'career_2', type: 'event', label: 'New Job' }, { id: 'career_3', type: 'money', label: 'Performance Review', cash: 200 }, { id: 'career_4', type: 'event', label: 'Volunteered' }, { id: 'career_5', type: 'event', label: 'Direct Report' }, { id: 'career_6', type: 'money', label: 'Start A Business', cash: -250 }, { id: 'career_7', type: 'event', label: 'Office Happy Hour' }, { id: 'career_8', type: 'money', label: 'Work Trip', cash: -100 }, { id: 'career_9', type: 'event', label: 'Remote Vs Office' }, { id: 'career_10', type: 'event', label: 'Exit Interview' }, { id: 'career_11', type: 'event', label: 'Resignation' }, { id: 'career_12', type: 'money', label: 'Industry Shift', cash: -150 }, { id: 'career_13', type: 'money', label: 'Workplace Drama', cash: -150 }, { id: 'career_14', type: 'money', label: 'Big Career Moment', cash: 350 }, { id: 'career_15', type: 'event', label: 'Mentorship' }, ], 'join_1'); const eduChain = chain([ { id: 'edu_1', type: 'event', label: 'College Enrolled' }, { id: 'edu_2', type: 'money', label: 'Scholarship', cash: 400 }, { id: 'edu_3', type: 'event', label: '4-Year Degree' }, { id: 'edu_4', type: 'event', label: 'Community College' }, { id: 'edu_5', type: 'event', label: 'Apprenticeship' }, { id: 'edu_6', type: 'money', label: 'Campus Drama', cash: -100 }, { id: 'edu_7', type: 'money', label: 'Study Abroad', cash: -150 }, { id: 'edu_8', type: 'event', label: 'Trade Circuit' }, { id: 'edu_9', type: 'event', label: 'Mentor' }, { id: 'edu_10', type: 'money', label: 'Online Courses', cash: -50 }, { id: 'edu_11', type: 'event', label: 'Graduation' }, ], 'join_1'); const gapChain = chain([ { id: 'gap_1', type: 'event', label: 'Canada', flavor: '🇨🇦' }, { id: 'gap_2', type: 'event', label: 'Iceland', flavor: '🇮🇸' }, { id: 'gap_3', type: 'event', label: 'Travel Buddy' }, { id: 'gap_4', type: 'event', label: 'Mexico', flavor: '🇲🇽' }, { id: 'gap_5', type: 'money', label: 'Cultural Mistake', cash: -100 }, { id: 'gap_6', type: 'event', label: 'Australia', flavor: '🇦🇺' }, { id: 'gap_7', type: 'event', label: 'Found Community' }, { id: 'gap_8', type: 'event', label: 'Japan', flavor: '🇯🇵' }, { id: 'gap_9', type: 'money', label: 'Visa Problem', cash: -150 }, { id: 'gap_10', type: 'event', label: 'Germany', flavor: '🇩🇪' }, { id: 'gap_11', type: 'event', label: 'Italy', flavor: '🇮🇹' }, ], 'join_1'); const quarterLifeChain = chain([ { id: 'join_1', type: 'event', label: 'Quarter-Life Crisis' }, { id: 'q_2', type: 'money', label: 'Horrible Hangover', cash: -50 }, { id: 'q_3', type: 'event', label: '30th Birthday' }, { id: 'q_4', type: 'money', label: 'Mystery Tattoo', cash: -100 }, { id: 'q_5', type: 'money', label: 'Unexpected Joy', cash: 200 }, ], 'life_fork'); const familyChain = chain([ { id: 'family_1', type: 'event', label: 'New City' }, { id: 'family_2', type: 'event', label: 'Uncles Condo' }, { id: 'family_3', type: 'event', label: 'Dinner Party' }, { id: 'family_4', type: 'money', label: 'You Won!', cash: 300 }, { id: 'family_5', type: 'money', label: "Friends' Wedding", cash: -150 }, { id: 'family_6', type: 'money', label: 'Job Or Raise', cash: 250 }, { id: 'family_7', type: 'event', label: 'Podcast' }, { id: 'family_8', type: 'event', label: 'Hobby' }, { id: 'family_9', type: 'event', label: 'Perfect Impression' }, { id: 'family_10', type: 'money', label: 'First Home', cash: -300 }, { id: 'family_11', type: 'event', label: 'Family Reunion' }, { id: 'family_12', type: 'money', label: 'Raise Or Job', cash: 200 }, { id: 'family_13', type: 'money', label: 'House Maintenance', cash: -150 }, { id: 'family_14', type: 'money', label: 'Family Vacation', cash: -100 }, ], 'join_2'); const investChain = chain([ { id: 'invest_1', type: 'money', label: 'Side Investment', cash: -150 }, { id: 'invest_2', type: 'money', label: 'Market Move', cash: 350 }, { id: 'invest_3', type: 'money', label: '401K Contribution', cash: -100 }, { id: 'invest_4', type: 'event', label: 'Forced Partnership' }, { id: 'invest_5', type: 'money', label: 'Market Crash', cash: -300 }, { id: 'invest_6', type: 'money', label: 'Start A Business', cash: -250 }, { id: 'invest_7', type: 'money', label: 'Bet Against Player', cash: 200 }, { id: 'invest_8', type: 'money', label: 'Market Manipulation', cash: -200 }, { id: 'invest_9', type: 'money', label: 'Leveraged Buyout', cash: 300 }, { id: 'invest_10', type: 'money', label: '100K Windfall', cash: 500 }, { id: 'invest_11', type: 'money', label: 'Investigation', cash: -250 }, { id: 'invest_12', type: 'money', label: 'Rogue Trader', cash: -300 }, { id: 'invest_13', type: 'money', label: '10K Payout', cash: 250 }, ], 'join_2'); const highRiskChain = chain([ { id: 'risk_1', type: 'money', label: 'High Risk Bet', cash: 400 }, { id: 'risk_2', type: 'money', label: 'Office Buyout', cash: -200 }, { id: 'risk_3', type: 'money', label: 'Bankruptcy', cash: -500, flavor: 'Ouch.' }, { id: 'risk_4', type: 'money', label: 'Comeback Deal', cash: 450 }, { id: 'risk_5', type: 'money', label: 'Investigation', cash: -200 }, { id: 'risk_6', type: 'money', label: 'Rogue Trader', cash: -250 }, ], 'join_3'); const safeChain = chain([ { id: 'safe_1', type: 'money', label: 'Force Sale', cash: -150 }, { id: 'safe_2', type: 'money', label: 'Start A Business', cash: -100 }, { id: 'safe_3', type: 'money', label: 'Raise Or Job', cash: 200 }, { id: 'safe_4', type: 'event', label: 'Family Reunion' }, { id: 'safe_5', type: 'money', label: 'Family Vacation', cash: -100 }, { id: 'safe_6', type: 'money', label: 'Steady Savings', cash: 150 }, ], 'join_3'); const leisureChain = chain([ { id: 'leisure_1', type: 'money', label: 'Investment Paid Off', cash: 300 }, { id: 'leisure_2', type: 'event', label: 'Start A Band' }, { id: 'leisure_3', type: 'event', label: 'Jury Duty' }, { id: 'leisure_4', type: 'event', label: 'Hobby' }, { id: 'leisure_5', type: 'money', label: 'The Races', cash: -150 }, { id: 'leisure_6', type: 'event', label: 'Job Offer' }, { id: 'leisure_7', type: 'money', label: 'Bucket List Trip', cash: -200 }, { id: 'leisure_8', type: 'money', label: 'Home Renovation', cash: -250 }, { id: 'leisure_9', type: 'event', label: 'Hosted Thanksgiving' }, { id: 'leisure_10', type: 'event', label: 'Highschool Reunion' }, { id: 'leisure_11', type: 'money', label: 'Fantasy Football', cash: 100 }, { id: 'leisure_12', type: 'money', label: 'Office Pool', cash: 150 }, { id: 'leisure_13', type: 'event', label: 'Pickle Ball' }, { id: 'leisure_14', type: 'event', label: 'Bowling Night' }, { id: 'leisure_15', type: 'money', label: 'Dental Work', cash: -150 }, { id: 'leisure_16', type: 'money', label: 'Golf Trip', cash: -100 }, { id: 'leisure_17', type: 'event', label: 'Joined Facebook' }, { id: 'leisure_18', type: 'money', label: 'Speeding Ticket', cash: -75 }, ], 'retirement_party'); const spaceList = [ { id: 'start', type: 'start', label: 'Start', next: 'path_fork' }, { id: 'path_fork', type: 'choice', label: 'Which Path?', choices: ['career_1', 'edu_1', 'gap_1'] }, ...careerChain, ...eduChain, ...gapChain, ...quarterLifeChain, { id: 'life_fork', type: 'choice', label: 'Relationship or Investment?', choices: ['family_1', 'invest_1'] }, ...familyChain, ...investChain, { id: 'join_2', type: 'event', label: 'Settling Down', next: 'risk_fork' }, { id: 'risk_fork', type: 'choice', label: 'High Risk or Safe?', choices: ['risk_1', 'safe_1'] }, ...highRiskChain, ...safeChain, { id: 'join_3', type: 'event', label: 'Retirement Planning', next: 'leisure_1' }, ...leisureChain, { id: 'retirement_party', type: 'event', label: 'Retirement Party', next: 'finish' }, { id: 'finish', type: 'finish', label: 'Finish' }, ]; export const board = { id: 'life-journey-v1', startSpaceId: 'start', spaces: Object.fromEntries(spaceList.map((space) => [space.id, space])), }; /** * Advance from `startId` by up to `steps` spaces, stopping immediately on arrival * at a 'choice' or 'finish' space even if pips remain (they're discarded). */ export function walkForward(startId, steps) { let current = startId; for (let i = 0; i < steps; i++) { const space = board.spaces[current]; if (!space.next) break; // sitting on a choice/finish space already — nowhere to advance current = space.next; const landed = board.spaces[current]; if (landed.type === 'choice' || landed.type === 'finish') break; } return current; }