diff options
Diffstat (limited to 'rules.ts')
-rw-r--r-- | rules.ts | 408 |
1 files changed, 330 insertions, 78 deletions
@@ -2,7 +2,9 @@ import { CardId, + ChooseCardArgs, Effect, + EffectSource, EngineNode, EventCard, FactionId, @@ -14,6 +16,7 @@ import { LeafNode, Player, PlayerCard, + PlayerTurnArgs, SeqNode, States, View, @@ -219,10 +222,10 @@ const seq_node = 's'; const function_node = 'f'; const resolved = 1; -function create_leaf_node( +function create_leaf_node<T = any>( state: string, faction: FactionId | 'None', - args?: any + args?: T ): LeafNode { return { t: leaf_node, @@ -260,6 +263,7 @@ function checkpoint() { } function setup_bag_of_glory() { + log_h1('Bag of Glory'); game.engine = [ create_leaf_node('add_glory', game.initiative), create_function_node('end_of_turn'), @@ -339,6 +343,7 @@ function start_of_player_turn() { const player = faction_player_map[args.f]; log_h2(player, player); game.faction_turn = args.f; + game.played_card = game.selected_cards[args.f][0]; resolve_active_and_proceed(); } @@ -347,6 +352,7 @@ const engine_functions: Record<string, Function> = { checkpoint, end_of_player_turn, end_of_turn, + end_resolving_event_effects, setup_bag_of_glory, setup_choose_card, setup_final_bid, @@ -400,6 +406,22 @@ function get_active_node( return a === null ? null : a.node; } +function get_nodes_for_state( + state: string, + engine: EngineNode[] = game.engine +): LeafNode[] { + let nodes = []; + for (let i of engine) { + if (i.t === leaf_node && i.s === state) { + nodes.push(i); + } + if (i.t === seq_node) { + nodes = nodes.concat(get_nodes_for_state(state, i.c)); + } + } + return nodes; +} + function get_active_node_args<T = any>(): T { const node = get_active_node(game.engine); if (node.t === leaf_node || node.t === function_node) { @@ -408,7 +430,7 @@ function get_active_node_args<T = any>(): T { return null; } -function update_active_node_args<T = any>(args: T) { +function update_active_node_args<T = any>(args: Partial<T>) { const node = get_active_node(game.engine); if (node.t === leaf_node || node.t === function_node) { node.a = { @@ -471,6 +493,9 @@ function next() { return; } game.active = next_active; + if (states[game.state].auto_resolve && states[game.state].auto_resolve()) { + resolve_active_and_proceed(); + } } } @@ -505,14 +530,20 @@ function game_view(state: Game, current: Player | 'Observer') { engine: game.engine, // TODO: remove log: game.log, prompt: null, + state: game.state, bag_of_glory: game.bag_of_glory, + bag_of_glory_count: game.bag_of_glory.length, bonuses: game.bonuses, current, + current_player_faction: faction, current_events: game.current_events, first_player: game.first_player, fronts: game.fronts, glory: game.glory, hand: faction === null ? [] : game.hands[faction], + discard: faction === null ? [] : game.discard[faction], + trash: faction === null ? [] : game.trash[faction], + deck: faction === null ? [] : list_deck(faction), hero_points: game.hero_points, initiative: game.initiative, medallions: game.medallions, @@ -700,7 +731,9 @@ function start_turn() { log_h2('Fascist Event', 'fascist'); log(card.title); - game.engine = card.effects.map((effect) => resolve_effect(effect)); + game.engine = card.effects.map((effect) => + resolve_effect(effect, 'fascist_event') + ); if (game.year === 3 && game.turn === 4) { game.engine.push(create_function_node('setup_final_bid')); } else { @@ -914,6 +947,9 @@ states.add_to_front = { for (let f of possible_fronts) { gen_action_front(f); } + if (args.src) { + view.prompt = add_prompt_prefix(view.prompt, get_source_name(args.src)); + } }, spend_hp() { resolve_spend_hp(); @@ -929,7 +965,7 @@ states.attack_front = { inactive: 'attack a Front', prompt() { gen_spend_hero_points(); - const { t: target, n } = get_active_node_args(); + const { t: target, n, src } = get_active_node_args(); let fronts: Array<FrontId> = []; @@ -949,6 +985,18 @@ states.attack_front = { ? `Attack ${front_names[fronts[0]]}` : 'Attack a front'; + let prefix = ''; + if (src) { + prefix = `${get_source_name(src)}: `; + } + if (fronts.length === 1) { + view.prompt = prefix + ? `${prefix}attack ${front_names[fronts[0]]}` + : `Attack ${front_names[fronts[0]]}`; + } else { + view.prompt = prefix ? `${prefix}attack a front` : `Attack a front`; + } + fronts.forEach((id) => gen_action('front', id)); }, spend_hp() { @@ -1109,7 +1157,12 @@ states.choose_card = { inactive: 'choose a card', prompt() { gen_spend_hero_points(); + const { src } = get_active_node_args<ChooseCardArgs>(); view.prompt = 'Choose a card to play this turn'; + if (src === 'momentum') { + view.prompt = 'Choose a card to play'; + } + const faction = get_active_faction(); const hand = game.hands[faction]; for (let c of hand) { @@ -1124,6 +1177,10 @@ states.choose_card = { card(c: CardId) { const faction = get_active_faction(); game.selected_cards[faction].push(c); + const { src } = get_active_node_args<ChooseCardArgs>(); + if (src === 'momentum') { + game.played_card = game.selected_cards[faction][0]; + } resolve_active_and_proceed(); }, }; @@ -1160,6 +1217,50 @@ states.choose_final_bid = { }, }; +function setup_momentum() { + const faction = get_active_faction(); + + // Player received medallion outside of their normal turn + // and must be resolved + if (game.faction_turn !== faction) { + insert_after_active_node( + resolve_effect(create_effect('play_card', faction, 1), 'momentum') + ); + return; + } + + // Player gets medallion during their turn. Need to check if it can be player + // right away or not. Depends on whether card for this turn has been fully resolved or not + + // Get player turn node + const node: LeafNode<PlayerTurnArgs> = get_nodes_for_state('player_turn')[0]; + console.log('node', node); + + const player_needs_to_play_card = game.selected_cards[faction].length > 0; + const { use_ap, use_morale_bonus, resolving_event } = + node.a ?? ({} as PlayerTurnArgs); + // TO CHECK: or event needs to be fulle resolved + if ( + player_needs_to_play_card || + use_ap || + use_morale_bonus || + resolving_event + ) { + // Player hasn't fully resolved this turns card. Update args to enable button + node.a = { + ...(node.a || {}), + use_momentum: true, + }; + } else { + // Player can resolve choosing a new card + insert_before_active_node( + create_leaf_node<ChooseCardArgs>('choose_card', faction, { + src: 'momentum', + }) + ); + } +} + states.choose_medallion = { inactive: 'choose a medallion', prompt() { @@ -1193,7 +1294,7 @@ states.choose_medallion = { gain_hero_points(faction, 7); break; case 2: - insert_after_active_node(create_leaf_node('choose_card', faction)); + setup_momentum(); break; default: game.medallions[faction].push(m); @@ -1394,6 +1495,10 @@ states.move_track = { view.prompt = `Move ${name} away from center`; } + if (node.a.src) { + view.prompt = add_prompt_prefix(view.prompt, get_source_name(node.a.src)); + } + if (track === LIBERTY_OR_COLLECTIVIZATION) { gen_action_standee(LIBERTY); gen_action_standee(COLLECTIVIZATION); @@ -1544,43 +1649,82 @@ function resolve_spend_hp() { next(); } +function set_player_turn_prompt({ + can_play_card, + can_spend_hp, + use_ap, + use_momentum, + use_morale_bonus, +}: PlayerTurnArgs & { can_spend_hp: boolean; can_play_card: boolean }) { + console.log('set_player_turn_prompt', { + can_play_card, + use_ap, + use_morale_bonus, + use_momentum, + }); + if (can_play_card && can_spend_hp) { + view.prompt = 'Play a card or spend Hero points'; + } else if (can_play_card && !can_spend_hp) { + view.prompt = 'Play a card'; + } else if (use_ap || use_morale_bonus || use_momentum) { + const text_options = []; + if (use_ap) { + text_options.push('Action Points'); + } + if (use_morale_bonus) { + text_options.push('Morale Bonus'); + } + + if (can_spend_hp) { + text_options.push('spend Hero points'); + } + + if (use_momentum) { + view.prompt = can_spend_hp + ? 'Play second card or spend Hero Points' + : 'Play second card'; + } else { + view.prompt = `Use ${text_options.join(', ')} or end turn`; + } + } else if (can_spend_hp) { + view.prompt = 'Spend Hero Points or end turn'; + } else { + view.prompt = 'End turn'; + } +} + states.player_turn = { inactive: 'play their turn', prompt() { gen_spend_hero_points(); const faction_id = get_active_faction(); - const { use_ap, use_morale_bonus } = get_active_node_args(); + let { use_ap, use_morale_bonus, use_momentum } = + get_active_node_args<PlayerTurnArgs>(); + + use_morale_bonus = use_morale_bonus && game.bonuses[MORALE_BONUS] === ON; const can_spend_hp = game.faction_turn === faction_id && game.hero_points[faction_id] > 0; const can_play_card = game.selected_cards[faction_id].length > 0; - - if (can_play_card && can_spend_hp) { - view.prompt = 'Play a card or spend Hero points'; - } else if (can_play_card && !can_spend_hp) { - view.prompt = 'Play a card'; - } else if (use_ap || use_morale_bonus) { - const text_options = []; - if (use_ap) { - text_options.push('Action Points'); - } - if (use_morale_bonus) { - text_options.push('Morale Bonus'); + console.log('can_play_card', can_play_card); + if (use_momentum) { + gen_action('use_momentum'); + if (use_ap || use_morale_bonus || can_play_card) { + view.actions['use_momentum'] = 0; } - if (can_spend_hp) { - text_options.push('spend Hero points'); - } - - view.prompt = `Use ${text_options.join(', ')} or end turn`; - } else if (can_spend_hp) { - view.prompt = 'Spend Hero Points or end turn'; - } else { - view.prompt = 'End turn'; } + set_player_turn_prompt({ + can_play_card, + can_spend_hp, + use_ap, + use_momentum, + use_morale_bonus, + }); + if (can_play_card) { - gen_action('play_for_ap'); + gen_action('play_to_tableau'); gen_action('play_for_event'); } if (use_ap) { @@ -1589,22 +1733,22 @@ states.player_turn = { if (use_morale_bonus && game.bonuses[MORALE_BONUS] === ON) { gen_action('use_morale_bonus'); } - if (!can_play_card && !use_ap) { - gen_action('done'); + if (!(can_play_card || use_ap || use_morale_bonus || use_momentum)) { + gen_action('end_turn'); } }, spend_hp() { resolve_spend_hp(); }, - done() { + end_turn() { game.faction_turn = null; game.played_card = null; resolve_active_and_proceed(true); }, - play_for_ap() { + play_to_tableau() { const faction = get_active_faction(); - const { strength } = play_card(faction, 'play_for_ap'); - update_active_node_args({ + const { strength } = play_card(faction, 'play_to_tableau'); + update_active_node_args<PlayerTurnArgs>({ use_morale_bonus: true, use_ap: true, strength, @@ -1614,8 +1758,13 @@ states.player_turn = { play_for_event() { const faction = get_active_faction(); const { effects } = play_card(faction, 'play_for_event'); + update_active_node_args<PlayerTurnArgs>({ + resolving_event: true, + }); - insert_before_active_node(create_effects_node(effects)); + const node = create_effects_node(effects); + node.c.push(create_function_node('end_resolving_event_effects')); + insert_before_active_node(node); next(); }, @@ -1632,6 +1781,17 @@ states.player_turn = { ); next(); }, + use_momentum() { + // We need to update since there can be a case where + // morale bonus hasn't been used yet but is still set to true + // due to bonus being turned off. + update_active_node_args<PlayerTurnArgs>({ + use_morale_bonus: false, + use_momentum: false, + }); + setup_momentum(); + next(); + }, use_morale_bonus() { // Update args before inserting node before current node, // otherwise it will update args of inserted node @@ -1722,7 +1882,7 @@ states.remove_attack_from_fronts = { const removed_value = card_id === 6 ? 1 : Math.min(3, Math.abs(game.fronts[id].value)); - update_front(id, removed_value); + update_front(id, removed_value, get_active_faction()); const fronts = f ?? {}; fronts[id] = removed_value; @@ -1790,33 +1950,65 @@ states.return_card = { states.spend_hero_points = { inactive: 'spend Hero points', + auto_resolve() { + const hero_points = game.hero_points[get_active_faction()]; + if (hero_points === 0) { + return true; + } + return false; + }, prompt() { view.prompt = 'Spend your Hero points'; - gen_action('done'); + const faction = get_active_faction(); + const { move_track, turn_on_bonus } = get_active_node_args(); + + if (move_track) { + view.prompt = 'Spend Hero points: move a Track'; + } else if (turn_on_bonus) { + view.prompt = 'Spend Hero points: turn on a Bonus'; + } + + if (!(move_track || turn_on_bonus)) { + gen_action('done'); + } const hero_points = game.hero_points[get_active_faction()]; if (hero_points === 0) { return; } - gen_action('draw_card'); - if (can_use_medallion(ARCHIVES_MEDALLION_ID, faction)) { - gen_action('remove_blank_marker'); - } - if (can_use_medallion(VOLUNTEERS_MEDALLION_ID, faction)) { - gen_action('add_to_front'); + if (!(move_track || turn_on_bonus)) { + gen_action('draw_card'); + if (can_use_medallion(ARCHIVES_MEDALLION_ID, faction)) { + gen_action('remove_blank_marker'); + } + if (can_use_medallion(VOLUNTEERS_MEDALLION_ID, faction)) { + gen_action('add_to_front'); + } } if (hero_points < 2) { return; } - gen_action_standee(FOREIGN_AID); - gen_action_standee(SOVIET_SUPPORT); + if (!(move_track || turn_on_bonus)) { + gen_action('move_track'); + } + for (const bonus of bonuses) { - if (game.bonuses[bonus] === OFF) { + let bonus_off = false; + if (!move_track && game.bonuses[bonus] === OFF) { gen_action_bonus(bonus); + bonus_off = true; + } + if (bonus_off && !turn_on_bonus) { + gen_action('turn_on_bonus'); } } + if (turn_on_bonus) { + return; + } + gen_action_standee(FOREIGN_AID); + gen_action_standee(SOVIET_SUPPORT); if (hero_points < 3) { return; } @@ -1846,13 +2038,24 @@ states.spend_hero_points = { resolve_active_and_proceed(); }, bonus(b: number) { + update_active_node_args({ + turn_on_bonus: false, + }); update_bonus(b, ON); pay_hero_points(get_active_faction(), 2); + next(); }, draw_card() { const faction = get_active_faction(); pay_hero_points(faction, 1); draw_hand_cards(faction, 1); + next(); + }, + move_track() { + update_active_node_args({ + move_track: true, + }); + next(); }, remove_blank_marker() { const faction = get_active_faction(); @@ -1870,6 +2073,9 @@ states.spend_hero_points = { resolve_active_and_proceed(); }, standee(track_id: number) { + update_active_node_args({ + move_track: false, + }); let amount = 2; if (track_id === LIBERTY || track_id === COLLECTIVIZATION) { amount = 3; @@ -1889,6 +2095,12 @@ states.spend_hero_points = { ); resolve_active_and_proceed(); }, + turn_on_bonus() { + update_active_node_args({ + turn_on_bonus: true, + }); + next(); + }, }; states.swap_card_tableau_hand = { @@ -1981,11 +2193,19 @@ states.take_hero_points = { ? 'Choose a player to take a Hero Point from' : `Choose a player to take ${v} Hero Points from`; const active_faction = get_active_faction(); + let target_exists = false; for (const faction of role_ids) { - if (faction !== active_faction) { + if (faction !== active_faction && game.hero_points[faction] > 0) { gen_action(faction_player_map[faction]); + target_exists = true; } } + + if (!target_exists) { + view.prompt = + 'Not possible to take Hero Points from another player. You must skip'; + gen_action('skip'); + } }, spend_hp() { resolve_spend_hp(); @@ -1999,6 +2219,9 @@ states.take_hero_points = { Moderate() { resolve_take_hero_points(MODERATES_ID); }, + skip() { + resolve_active_and_proceed(); + }, }; states.use_organization_medallion = { @@ -2213,14 +2436,13 @@ function add_glory( amount: number, indent: boolean = false ) { + let tokens_log = ''; for (let i = 0; i < amount; ++i) { game.bag_of_glory.push(get_active_faction()); + tokens_log += `<ft${faction}>`; } - let text = - amount === 1 - ? `${faction_player_map[faction]} adds 1 token to the Bag of Glory` - : `${faction_player_map[faction]} adds ${amount} tokens to the Bag of Glory`; + let text = `${faction_player_map[faction]} adds ${tokens_log} to the Bag of Glory`; if (indent) { logi(text); @@ -2314,20 +2536,30 @@ function end_of_year() { resolve_active_and_proceed(); return; } + } else { + log_h1('End of year'); } + const glory_to_draw = [0, 1, 2, 5]; const glory_this_year: Record<FactionId, boolean> = { a: false, c: false, m: false, }; + const drawn_glory = []; for (let i = 0; i < glory_to_draw[game.year]; ++i) { const index = random(game.bag_of_glory.length); const faction = game.bag_of_glory[index]; game.glory.push(faction); + drawn_glory.push(faction); glory_this_year[faction] = true; array_remove(game.bag_of_glory, index); } + log( + `Tokens pulled from the Bag of Glory: ${drawn_glory + .map((faction_id: string) => `<ft${faction_id}>`) + .join('')}` + ); if (game.year === 3) { // end of game @@ -2354,6 +2586,18 @@ function end_of_year() { next(); } +function end_resolving_event_effects() { + // Get player turn node + const node: LeafNode<PlayerTurnArgs> = get_nodes_for_state('player_turn')[0]; + + // Update args + node.a = { + ...(node.a || {}), + resolving_event: false, + }; + resolve_active_and_proceed(); +} + function gain_hero_points_in_player_order(factions: FactionId[], value) { for (const f of get_player_order()) { if (factions.includes(f)) { @@ -2422,22 +2666,20 @@ function get_hand_limit(faction: FactionId) { function play_card( faction: FactionId, - type: 'play_for_event' | 'play_for_ap' + type: 'play_for_event' | 'play_to_tableau' ): PlayerCard { const index = game.selected_cards[faction].length - 1; const card_id = game.selected_cards[faction][index]; const card = cards[card_id]; game.played_card = card_id; - log_h3( - `${game.active} plays ${card.title} for the ${ - type === 'play_for_event' ? 'Event' : 'Action Points' - }` - ); + array_remove(game.hands[faction], game.hands[faction].indexOf(card_id)); array_remove(game.selected_cards[faction], index); if (type === 'play_for_event') { + log_h3(`${game.active} plays ${card.title} for the Event`); game.trash[faction].push(card_id); } else { + log_h3(`${game.active} plays ${card.title} to their Tableau`); game.tableaus[faction].push(card_id); } return card as PlayerCard; @@ -2471,7 +2713,7 @@ function resolve_fascist_test() { } const effect = test_passed ? test.pass : test.fail; - const node = resolve_effect(effect); + const node = resolve_effect(effect, 'fascist_test'); if (node !== null) { insert_after_active_node(node); @@ -2600,7 +2842,7 @@ function move_track(track_id: number, change: number) { get_blank_marker_id(track_id, space_id) ); } - const node = resolve_effect(trigger); + const node = resolve_effect(trigger, 'track_icon'); if (node !== null) { insert_after_active_node(node); } @@ -2731,7 +2973,7 @@ function victory_on_a_front(front_id: FrontId) { gain_hero_points_in_player_order(game.fronts[front_id].contributions, 3); } -function create_effects_node(effects: Effect[]): EngineNode { +function create_effects_node(effects: Effect[]): SeqNode { const nodes = effects.reduce((accrued: EngineNode[], current: Effect) => { const node = resolve_effect(current); if (node !== null) { @@ -2765,13 +3007,11 @@ const effect_type_state_map: Record<string, string> = { track: 'move_track', }; -function resolve_effect( - effect: Effect - // faction: FactionId = get_active_faction() // -): EngineNode { +function resolve_effect(effect: Effect, source?: EffectSource): EngineNode { const args = { t: effect.target, v: effect.value, + src: source, }; const faction = get_faction_to_resolve_effect(effect); @@ -2782,6 +3022,7 @@ function resolve_effect( if (effect.type === 'state') { return create_leaf_node(effect.target as string, faction, { v: effect.value, + src: source, }); } // Default cases where effect type is mapped to a state @@ -2809,9 +3050,7 @@ function resolve_effect( resolve: () => { return create_seq_node( get_player_order().map((faction) => - create_leaf_node('hero_points', faction, { - v: effect.value, - }) + create_leaf_node('hero_points', faction, args) ) ); }, @@ -2867,9 +3106,7 @@ function resolve_effect( resolve: () => { return create_seq_node( get_player_order(get_active_faction()).map((faction) => - create_leaf_node('draw_card', faction, { - v: effect.value, - }) + create_leaf_node('draw_card', faction, args) ) ); }, @@ -2878,12 +3115,9 @@ function resolve_effect( condition: effect.type === 'draw_card' && effect.target === OTHER_PLAYERS, resolve: () => { const leaf_nodes = get_player_order(get_active_faction()).map( - (faction) => - create_leaf_node('draw_card', faction, { - v: effect.value, - }) + (faction) => create_leaf_node('draw_card', faction, args) ); - array_remove(leaf_nodes, 0); + array_remove(leaf_nodes, 0); // Remove current player return create_seq_node(leaf_nodes); }, }, @@ -2891,8 +3125,8 @@ function resolve_effect( condition: effect.type === 'play_card', resolve: () => { return create_seq_node([ - create_leaf_node('choose_card', faction), - create_leaf_node('player_turn', faction), + create_leaf_node('choose_card', faction, { src: source }), + create_leaf_node('player_turn', faction, { src: source }), ]); }, }, @@ -3049,6 +3283,14 @@ function log_h3(msg: string) { // #region UTILITY +function lowerCaseFirstLetter(val: string) { + return String(val).charAt(0).toLowerCase() + String(val).slice(1); +} + +function add_prompt_prefix(prompt: string, prefix: string) { + return `${prefix}: ${lowerCaseFirstLetter(prompt)}`; +} + function get_active_faction(): FactionId { return player_faction_map[game.active]; } @@ -3150,6 +3392,16 @@ function get_player_order_in_game( return order; } +function get_source_name(source: EffectSource): string { + const prefix_map: Record<EffectSource, string> = { + fascist_event: 'Fascist Event', + fascist_test: 'Fascist Test', + track_icon: 'Track Trigger', + momentum: 'Momentum', + }; + return prefix_map[source]; +} + function get_factions_with_most_hero_poins(): FactionId[] { let most_hero_points = null; let faction_ids = []; |