diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 368 |
1 files changed, 297 insertions, 71 deletions
@@ -126,6 +126,7 @@ function checkpoint() { resolve_active_and_proceed(); } 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'), @@ -187,6 +188,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(); } const engine_functions = { @@ -194,6 +196,7 @@ const engine_functions = { checkpoint, end_of_player_turn, end_of_turn, + end_resolving_event_effects, setup_bag_of_glory, setup_choose_card, setup_final_bid, @@ -239,6 +242,18 @@ function get_active_node(engine = game.engine) { const a = get_active(engine); return a === null ? null : a.node; } +function get_nodes_for_state(state, engine = game.engine) { + 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() { const node = get_active_node(game.engine); if (node.t === leaf_node || node.t === function_node) { @@ -292,6 +307,9 @@ function next() { return; } game.active = next_active; + if (states[game.state].auto_resolve && states[game.state].auto_resolve()) { + resolve_active_and_proceed(); + } } } function resolve_active_node(checkpoint = false) { @@ -314,14 +332,20 @@ function game_view(state, current) { engine: game.engine, 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, @@ -483,7 +507,7 @@ function start_turn() { const card = cards[cardId]; 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')); } @@ -492,6 +516,12 @@ function start_turn() { } next(); } +function player_can_resolve_icon(icon) { + if (icon === 'teamwork_on' && game.bonuses[data_1.TEAMWORK_BONUS] === data_1.ON) { + return false; + } + return true; +} states.activate_icon = { inactive: 'activate an icon', prompt() { @@ -500,6 +530,9 @@ states.activate_icon = { const c = cards[game.played_card]; for (const i of c.icons) { gen_action(i); + if (!player_can_resolve_icon(i)) { + view.actions[i] = 0; + } } }, spend_hp() { @@ -687,6 +720,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(); @@ -701,7 +737,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 = []; if (target === data_1.ANY) { fronts = get_fronts_to_add_to(data_1.ANY, n); @@ -722,6 +758,18 @@ states.attack_front = { fronts.length === 1 ? `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() { @@ -869,7 +917,11 @@ states.choose_card = { inactive: 'choose a card', prompt() { gen_spend_hero_points(); + const { src } = get_active_node_args(); 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) { @@ -884,6 +936,10 @@ states.choose_card = { card(c) { const faction = get_active_faction(); game.selected_cards[faction].push(c); + const { src } = get_active_node_args(); + if (src === 'momentum') { + game.played_card = game.selected_cards[faction][0]; + } resolve_active_and_proceed(); }, }; @@ -916,6 +972,31 @@ states.choose_final_bid = { resolve_active_and_proceed(true); }, }; +function setup_momentum() { + const faction = get_active_faction(); + if (game.faction_turn !== faction) { + insert_after_active_node(resolve_effect((0, data_1.create_effect)('play_card', faction, 1), 'momentum')); + return; + } + const node = 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 ?? {}; + if (player_needs_to_play_card || + use_ap || + use_morale_bonus || + resolving_event) { + node.a = { + ...(node.a || {}), + use_momentum: true, + }; + } + else { + insert_before_active_node(create_leaf_node('choose_card', faction, { + src: 'momentum', + })); + } +} states.choose_medallion = { inactive: 'choose a medallion', prompt() { @@ -945,7 +1026,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); @@ -991,19 +1072,26 @@ states.draw_card = { states.end_of_year_discard = { inactive: 'discard cards from hand and tableau', prompt() { - view.prompt = 'Discard a card'; const faction_id = get_active_faction(); const hand = game.hands[faction_id]; const hand_limit = get_hand_limit(faction_id); - if (hand.length > hand_limit) { + const needs_to_discard_from_hand = hand.length > hand_limit; + if (needs_to_discard_from_hand) { for (let c of hand) gen_action_card(c); } const tableau = game.tableaus[faction_id]; - if (tableau.length > game.year) { + const needs_to_discard_from_tableau = tableau.length > game.year; + if (needs_to_discard_from_tableau) { for (let c of tableau) gen_action_card(c); } + if (needs_to_discard_from_hand && needs_to_discard_from_tableau) { + view.prompt = 'Discard a card from your hand or tableau'; + } + else { + view.prompt = `Discard a card from your ${needs_to_discard_from_hand ? 'hand' : 'tableau'}`; + } }, card(c) { const faction_id = get_active_faction(); @@ -1126,6 +1214,9 @@ states.move_track = { else if (track === data_1.GOVERNMENT && value === data_1.AWAY_FROM_CENTER) { 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 === data_1.LIBERTY_OR_COLLECTIVIZATION) { gen_action_standee(data_1.LIBERTY); gen_action_standee(data_1.COLLECTIVIZATION); @@ -1242,41 +1333,71 @@ function resolve_spend_hp() { log('Spends Hero Points'); next(); } +function set_player_turn_prompt({ can_play_card, can_spend_hp, use_ap, use_momentum, use_morale_bonus, }) { + 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(); + use_morale_bonus = use_morale_bonus && game.bonuses[data_1.MORALE_BONUS] === data_1.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'); - } - if (can_spend_hp) { - text_options.push('spend Hero points'); + 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; } - 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) { @@ -1285,21 +1406,21 @@ states.player_turn = { if (use_morale_bonus && game.bonuses[data_1.MORALE_BONUS] === data_1.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'); + const { strength } = play_card(faction, 'play_to_tableau'); update_active_node_args({ use_morale_bonus: true, use_ap: true, @@ -1310,7 +1431,12 @@ states.player_turn = { play_for_event() { const faction = get_active_faction(); const { effects } = play_card(faction, 'play_for_event'); - insert_before_active_node(create_effects_node(effects)); + update_active_node_args({ + resolving_event: true, + }); + const node = create_effects_node(effects); + node.c.push(create_function_node('end_resolving_event_effects')); + insert_before_active_node(node); next(); }, use_ap() { @@ -1324,6 +1450,14 @@ states.player_turn = { })); next(); }, + use_momentum() { + update_active_node_args({ + use_morale_bonus: false, + use_momentum: false, + }); + setup_momentum(); + next(); + }, use_morale_bonus() { update_active_node_args({ use_morale_bonus: false, @@ -1387,7 +1521,7 @@ states.remove_attack_from_fronts = { front(id) { const { f, v: card_id } = get_active_node_args(); 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; update_active_node_args({ f: fronts }); @@ -1446,31 +1580,60 @@ 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(data_1.ARCHIVES_MEDALLION_ID, faction)) { - gen_action('remove_blank_marker'); - } - if (can_use_medallion(data_1.VOLUNTEERS_MEDALLION_ID, faction)) { - gen_action('add_to_front'); + if (!(move_track || turn_on_bonus)) { + gen_action('draw_card'); + if (can_use_medallion(data_1.ARCHIVES_MEDALLION_ID, faction)) { + gen_action('remove_blank_marker'); + } + if (can_use_medallion(data_1.VOLUNTEERS_MEDALLION_ID, faction)) { + gen_action('add_to_front'); + } } if (hero_points < 2) { return; } - gen_action_standee(data_1.FOREIGN_AID); - gen_action_standee(data_1.SOVIET_SUPPORT); + if (!(move_track || turn_on_bonus)) { + gen_action('move_track'); + } for (const bonus of bonuses) { - if (game.bonuses[bonus] === data_1.OFF) { + let bonus_off = false; + if (!move_track && game.bonuses[bonus] === data_1.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(data_1.FOREIGN_AID); + gen_action_standee(data_1.SOVIET_SUPPORT); if (hero_points < 3) { return; } @@ -1497,13 +1660,24 @@ states.spend_hero_points = { resolve_active_and_proceed(); }, bonus(b) { + update_active_node_args({ + turn_on_bonus: false, + }); update_bonus(b, data_1.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(); @@ -1520,6 +1694,9 @@ states.spend_hero_points = { resolve_active_and_proceed(); }, standee(track_id) { + update_active_node_args({ + move_track: false, + }); let amount = 2; if (track_id === data_1.LIBERTY || track_id === data_1.COLLECTIVIZATION) { amount = 3; @@ -1538,6 +1715,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 = { inactive: 'swap cards', @@ -1609,11 +1792,18 @@ 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(); @@ -1627,6 +1817,9 @@ states.take_hero_points = { Moderate() { resolve_take_hero_points(data_1.MODERATES_ID); }, + skip() { + resolve_active_and_proceed(); + }, }; states.use_organization_medallion = { inactive: 'use Organization Medallion', @@ -1776,12 +1969,12 @@ function setup_return_card_from_trash() { resolve_active_and_proceed(); } function add_glory(faction, amount, indent = 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); } @@ -1874,19 +2067,27 @@ function end_of_year() { return; } } + else { + log_h1('End of year'); + } const glory_to_draw = [0, 1, 2, 5]; const glory_this_year = { 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) => `<ft${faction_id}>`) + .join('')}`); if (game.year === 3) { determine_winner(); return; @@ -1899,6 +2100,14 @@ function end_of_year() { game.top_of_events_deck = null; next(); } +function end_resolving_event_effects() { + const node = get_nodes_for_state('player_turn')[0]; + node.a = { + ...(node.a || {}), + resolving_event: false, + }; + resolve_active_and_proceed(); +} function gain_hero_points_in_player_order(factions, value) { for (const f of get_player_order()) { if (factions.includes(f)) { @@ -1911,11 +2120,16 @@ function gain_hero_points(faction_id, value, skip_abilities = false) { return; } if (!skip_abilities && - ((faction_id === data_1.ANARCHISTS_ID && - (game.active_abilities || []).includes(data_1.ANARCHIST_EXTRA_HERO_POINT)) || - (faction_id === data_1.COMMUNISTS_ID && - (game.active_abilities || []).includes(data_1.COMMUNIST_EXTRA_HERO_POINT)))) { + faction_id === data_1.ANARCHISTS_ID && + (game.active_abilities || []).includes(data_1.ANARCHIST_EXTRA_HERO_POINT)) { value++; + game.active_abilities = (game.active_abilities || []).filter((ability) => ability !== data_1.ANARCHIST_EXTRA_HERO_POINT); + } + if (!skip_abilities && + faction_id === data_1.COMMUNISTS_ID && + (game.active_abilities || []).includes(data_1.COMMUNIST_EXTRA_HERO_POINT)) { + value++; + game.active_abilities = (game.active_abilities || []).filter((ability) => ability !== data_1.COMMUNIST_EXTRA_HERO_POINT); } const gain = Math.min(game.hero_points.pool, value); game.hero_points.pool -= gain; @@ -1942,13 +2156,14 @@ function play_card(faction, type) { 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; @@ -1977,7 +2192,7 @@ function resolve_fascist_test() { log('The Test is failed'); } 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); } @@ -2081,7 +2296,7 @@ function move_track(track_id, change) { if (space_id !== 0) { game.triggered_track_effects.push(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); } @@ -2204,10 +2419,11 @@ const effect_type_state_map = { take_hero_points: 'take_hero_points', track: 'move_track', }; -function resolve_effect(effect) { +function resolve_effect(effect, source) { const args = { t: effect.target, v: effect.value, + src: source, }; const faction = get_faction_to_resolve_effect(effect); if (effect.type === 'function') { @@ -2216,6 +2432,7 @@ function resolve_effect(effect) { if (effect.type === 'state') { return create_leaf_node(effect.target, faction, { v: effect.value, + src: source, }); } let state = effect_type_state_map[effect.type]; @@ -2233,9 +2450,7 @@ function resolve_effect(effect) { { condition: effect.type === 'hero_points' && effect.target === data_1.ALL_PLAYERS, resolve: () => { - return create_seq_node(get_player_order().map((faction) => create_leaf_node('hero_points', faction, { - v: effect.value, - }))); + return create_seq_node(get_player_order().map((faction) => create_leaf_node('hero_points', faction, args))); }, }, { @@ -2279,17 +2494,13 @@ function resolve_effect(effect) { { condition: effect.type === 'draw_card' && effect.target === data_1.ALL_PLAYERS, resolve: () => { - return create_seq_node(get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, { - v: effect.value, - }))); + return create_seq_node(get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, args))); }, }, { condition: effect.type === 'draw_card' && effect.target === data_1.OTHER_PLAYERS, resolve: () => { - const leaf_nodes = get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, { - v: effect.value, - })); + const leaf_nodes = get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, args)); array_remove(leaf_nodes, 0); return create_seq_node(leaf_nodes); }, @@ -2298,8 +2509,8 @@ function resolve_effect(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 }), ]); }, }, @@ -2384,6 +2595,12 @@ function log_h3(msg) { log_br(); log('.h3 ' + msg); } +function lowerCaseFirstLetter(val) { + return String(val).charAt(0).toLowerCase() + String(val).slice(1); +} +function add_prompt_prefix(prompt, prefix) { + return `${prefix}: ${lowerCaseFirstLetter(prompt)}`; +} function get_active_faction() { return player_faction_map[game.active]; } @@ -2458,6 +2675,15 @@ function get_player_order_in_game(first_player = game.initiative) { } return order; } +function get_source_name(source) { + const prefix_map = { + 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() { let most_hero_points = null; let faction_ids = []; |