diff options
-rw-r--r-- | data.js | 4 | ||||
-rw-r--r-- | data.ts | 4 | ||||
-rw-r--r-- | play.css | 11 | ||||
-rw-r--r-- | play.html | 4 | ||||
-rw-r--r-- | play.js | 21 | ||||
-rw-r--r-- | rules.js | 379 | ||||
-rw-r--r-- | rules.ts | 415 | ||||
-rw-r--r-- | types.d.ts | 3 |
8 files changed, 497 insertions, 344 deletions
@@ -1784,12 +1784,12 @@ const data = { { id: PROPAGANDA_MEDALLION_ID, name: 'Propaganda', - tooltip: '+1 card when drawing from your deck & keep +1 in hand at End of the Year.' + tooltip: "Gain 2 extra Hero points form each Successful Test (even if you didn't support)." }, { id: INTELLIGENCE_MEDALLION_ID, name: 'Intelligence', - tooltip: "Gain 2 extra Hero points form each Successful Test (even if you didn't support)." + tooltip: '+1 card when drawing from your deck & keep +1 in hand at End of the Year.' }, { id: VOLUNTEERS_MEDALLION_ID, @@ -1857,12 +1857,12 @@ const data: StaticData = { { id: PROPAGANDA_MEDALLION_ID, name: 'Propaganda', - tooltip: '+1 card when drawing from your deck & keep +1 in hand at End of the Year.' + tooltip: "Gain 2 extra Hero points form each Successful Test (even if you didn't support)." }, { id: INTELLIGENCE_MEDALLION_ID, name: 'Intelligence', - tooltip: "Gain 2 extra Hero points form each Successful Test (even if you didn't support)." + tooltip: '+1 card when drawing from your deck & keep +1 in hand at End of the Year.' }, { id: VOLUNTEERS_MEDALLION_ID, @@ -166,6 +166,17 @@ body.Observer #toggle_trash { /* CONTAINERS */ +#hero_point_pool { + position: absolute; + top: 150px; + left: 26px; + width: 60px; +} + +#hero_point_pool .token { + margin-bottom: -30px; +} + .front_container { position: absolute; display: flex; @@ -44,8 +44,8 @@ <main> <div id="mapwrap"> <div id="map"> - <div id="pieces"> - </div> + <div id="hero_point_pool"></div> + <div id="pieces"></div> </div> </div> @@ -2,9 +2,6 @@ /* global view, action_button, send_action */ -// TODO: pool for hero points to animate ? -// TODO: pool for faction markers to animate ? - const ui = { header: document.querySelector("header"), status: document.getElementById("status"), @@ -37,6 +34,7 @@ const ui = { document.getElementById("tokens_c"), document.getElementById("tokens_m"), ], + hero_point_pool: document.getElementById("hero_point_pool"), bag_of_glory: document.getElementById("bag_of_glory"), // spaces @@ -275,6 +273,13 @@ function on_init() { return on_init_once = true + let favicon = document.querySelector('link[rel="icon"]') + switch (params.role) { + case "Anarchist": favicon.href = "images/tokens150/player_anarchist.png"; break + case "Communist": favicon.href = "images/tokens150/player_communist.png"; break + case "Moderate": favicon.href = "images/tokens150/player_moderate.png"; break + } + ui.roles_list = document.getElementById("roles"), ui.roles = [ document.getElementById("role_Anarchist"), @@ -550,6 +555,9 @@ function on_update() { // eslint-disable-line no-unused-vars update_hero_points(ui.tokens[1], view.hero_points[1]) update_hero_points(ui.tokens[2], view.hero_points[2]) + ui.hero_point_pool.replaceChildren() + update_hero_points(ui.hero_point_pool, view.hero_points[3]) + update_front(ui.status_fronts[0], ui.str_fronts[0], ui.con_fronts[0], view.fronts[0], "a") update_front(ui.status_fronts[1], ui.str_fronts[1], ui.con_fronts[1], view.fronts[1], "m") update_front(ui.status_fronts[2], ui.str_fronts[2], ui.con_fronts[2], view.fronts[2], "n") @@ -577,8 +585,10 @@ function on_update() { // eslint-disable-line no-unused-vars action_button("Communist", "Communist") action_button("Moderate", "Moderate") + action_button("archives", "Archives") + action_button("volunteers", "Volunteers") + action_button("add_glory", "Add to Bag of Glory") - action_button("add_to_front", "+1 to a Front") action_button("draw_card", "Draw a Card") action_button("draw_cards", "Draw Cards") action_button("draw_glory", "Draw from Bag of Glory") @@ -586,7 +596,6 @@ function on_update() { // eslint-disable-line no-unused-vars action_button("lose_hp", "Lose Hero Points") action_button("play_to_tableau", "Action Points") action_button("play_for_event", "Event") - action_button("remove_blank_marker", "Remove Blank marker") action_button("use_momentum", "Momentum") action_button("use_ap", "Action Points") @@ -614,10 +623,12 @@ function on_focus_card_tip(x) { } function on_focus_medallion_tip(x) { + ui.status.textContent = data.medallions[x].name + ": " + data.medallions[x].tooltip ui.tooltip.className = "pink token medallion medallion_" + x } function on_blur_tip(x) { + ui.status.textContent = "" ui.tooltip.className = "hide" } @@ -76,7 +76,7 @@ function gen_spend_hero_points() { gen_action('spend_hp'); } } -const multiactive_states = ['choose_card', 'end_of_year_discard']; +const multiactive_states = ['choose_card', 'end_of_year_discard', 'choose_final_bid']; function action(state, player, action, arg) { game = state; if (action !== 'undo' && !multiactive_states.includes(game.state)) { @@ -140,8 +140,7 @@ function setup_choose_card() { function setup_final_bid() { game.fascist = 0; log_header('Final Bid', 't'); - const player_order = get_player_order(); - game.engine = player_order.map((faction_id) => create_state_node('choose_final_bid', faction_id)); + game.engine = [create_state_node('choose_final_bid', 'all')]; game.engine.push(create_function_node('checkpoint')); game.engine.push(create_function_node('resolve_final_bid')); game.engine.push(create_function_node('setup_choose_card')); @@ -207,7 +206,7 @@ const engine_functions = { start_year, resolve_fascist_test, resolve_final_bid, - log_trigger, + place_blank_marker, card1_event2, card3_event2, card10_event2, @@ -311,7 +310,7 @@ function next(checkpoint = false) { const current_active = game.active; const next_active = get_next_active(node.p); if (next_active !== current_active && game.undo.length > 0) { - insert_before_active_node(create_state_node('confirm_turn', get_active_faction())); + insert_before_active_node(create_state_node('confirm_turn', get_active_faction(), { f: next_active })); game.state = 'confirm_turn'; return; } @@ -369,7 +368,8 @@ function game_view(state, current) { } else if (current !== game.active && !game.active.includes(current)) { - let inactive = states[game.state].inactive || game.state; + let src = get_active_node_args()?.src; + let inactive = src ? get_source_inactive(src) : states[game.state].inactive || game.state; view.prompt = Array.isArray(game.active) ? `Waiting for ${game.active.join(' and ')} to ${inactive}.` : `Waiting for ${game.active} to ${inactive}.`; @@ -456,6 +456,7 @@ function setup(seed, _scenario, options) { [], ], triggered_track_effects: [], + untriggered_track_effects: [], log: [], undo: [], used_medallions: [], @@ -480,6 +481,7 @@ function setup(seed, _scenario, options) { function draw_hand_cards(faction_id, count, indent = true) { const deck = list_deck(faction_id); if (game.medallions[faction_id].includes(data_1.INTELLIGENCE_MEDALLION_ID)) { + log(">M" + data_1.INTELLIGENCE_MEDALLION_ID); count++; } let drawn_cards = 0; @@ -549,7 +551,7 @@ const track_icon_to_track_id_map = { d_soviet_support: data_1.SOVIET_SUPPORT, }; states.activate_icon = { - inactive: 'activate an icon', + inactive: 'activate a Morale Bonus icon', prompt() { gen_spend_hero_points(); const c = cards[game.played_card]; @@ -623,51 +625,19 @@ states.activate_icon = { update_front(f, get_icon_count_in_tableau('add_to_front'), get_active_faction()); resolve_active_and_proceed(); }, - tr0(x) { + trX(x, track) { + let old = game.tracks[track]; + move_track_to(track, x); if (can_use_medallion(data_1.ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(data_1.LIBERTY, x); - } - else { - move_track_to(0, x); - } - resolve_active_and_proceed(); - }, - tr1(x) { - if (can_use_medallion(data_1.ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(data_1.COLLECTIVIZATION, x); - } - else { - move_track_to(1, x); - } - resolve_active_and_proceed(); - }, - tr2(x) { - if (can_use_medallion(data_1.ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(data_1.GOVERNMENT, x); - } - else { - move_track_to(2, x); - } - resolve_active_and_proceed(); - }, - tr3(x) { - if (can_use_medallion(data_1.ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(data_1.SOVIET_SUPPORT, x); - } - else { - move_track_to(3, x); - } - resolve_active_and_proceed(); - }, - tr4(x) { - if (can_use_medallion(data_1.ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(data_1.FOREIGN_AID, x); - } - else { - move_track_to(4, x); + insert_use_organization_medallion_node(track, game.tracks[track] - old); } resolve_active_and_proceed(); }, + tr0(x) { this.trX(x, 0); }, + tr1(x) { this.trX(x, 1); }, + tr2(x) { this.trX(x, 2); }, + tr3(x) { this.trX(x, 3); }, + tr4(x) { this.trX(x, 4); }, draw_card() { draw_hand_cards(get_active_faction(), get_icon_count_in_tableau('draw_card')); resolve_active_and_proceed(); @@ -736,7 +706,7 @@ states.add_to_front = { const args = get_active_node_args(); const possible_fronts = get_fronts_to_add_to(args.t); if (possible_fronts.length === 0) { - view.prompt = 'No valid front to add strength to.'; + view.prompt = 'Cannot support ' + front_names[args.t] + '.'; gen_action('skip'); } else if (possible_fronts.length === 4) { @@ -769,7 +739,7 @@ states.attack_front = { const possible_fronts = get_fronts_to_add_to(target, n); const number_of_fronts = possible_fronts.length; if (number_of_fronts === 0) { - view.prompt = 'No valid front to attack.'; + view.prompt = 'Cannot attack ' + front_names[target] + '.'; gen_action('skip'); } else if (possible_fronts.length === 4) { @@ -795,7 +765,7 @@ states.attack_front = { states.break_tie_final_bid = { inactive: 'break tie for Final Bid', prompt() { - view.prompt = 'Choose the winner of the Final Bid'; + view.prompt = 'Choose the winner of the Final Bid.'; const { winners } = get_active_node_args(); for (const f of winners) { gen_action(faction_player_map[f]); @@ -817,7 +787,7 @@ states.break_tie_final_bid = { states.break_tie_winner = { inactive: 'break tie for winner of the game', prompt() { - view.prompt = 'Choose the winner of the game'; + view.prompt = 'Choose the winner of the game.'; const { winners } = get_active_node_args(); for (const f of winners) { gen_action(faction_player_map[f]); @@ -849,7 +819,7 @@ states.change_active_player = { }, }; states.choose_area_ap = { - inactive: 'choose area to use Action Points', + inactive: 'use action points', prompt() { gen_spend_hero_points(); const use_morale_bonus = game.can_use_mb === 1 && game.bonuses[data_1.MORALE_BONUS] === data_1.ON; @@ -947,7 +917,7 @@ states.choose_area_ap = { }, }; states.change_bonus = { - inactive: 'select Bonus', + inactive: 'toggle Bonus', prompt() { gen_spend_hero_points(); const args = get_active_node_args(); @@ -955,7 +925,10 @@ states.change_bonus = { game.bonuses[data_1.TEAMWORK_BONUS] === data_1.ON && game.bonuses[data_1.MORALE_BONUS] === data_1.ON) || (args.v === data_1.OFF && game.bonuses[args.t] === data_1.OFF)) { - view.prompt = `${bonus_names[args.t]} is already ${args.v === data_1.OFF ? 'off' : 'on'}.`; + if (args.t === 'any') + view.prompt = `Both bonuses are already ${args.v === data_1.OFF ? 'off' : 'on'}.`; + else + view.prompt = `${bonus_names[args.t]} is already ${args.v === data_1.OFF ? 'off' : 'on'}.`; gen_action('skip'); } else if (args.t === data_1.ANY && args.v === data_1.ON) { @@ -981,15 +954,15 @@ states.change_bonus = { }, skip() { const args = get_active_node_args(); - logi(`${bonus_names[args.t]} ${args.v === data_1.OFF ? 'off' : 'on'}`); + logi(`Bonus already ${args.v === data_1.OFF ? 'off' : 'on'}`); resolve_active_and_proceed(); }, }; states.play_card = { - inactive: 'play a card', + inactive: 'play another card', prompt() { gen_spend_hero_points(); - view.prompt = 'Play a card.'; + view.prompt = 'Play another card.'; const faction = get_active_faction(); const hand = game.hands[faction]; for (let c of hand) { @@ -1011,7 +984,7 @@ states.play_card = { game.played_card = game.selected_cards[faction][game.selected_cards[faction].length - 1]; const args = get_active_node_args(); if (args && args.src === 'momentum') { - log_header("~ Momentum ~\nC" + game.played_card, faction); + log_header("~ M" + data_1.MOMENTUM_MEDALLION_ID + " ~\nC" + game.played_card, faction); } else { log_header("~ Play Card ~\nC" + game.played_card, faction); @@ -1024,10 +997,9 @@ states.play_card = { }, }; states.choose_card = { - inactive: 'choose a card', + inactive: 'play a card for this turn', prompt(player) { - gen_spend_hero_points(); - view.prompt = 'Choose a card to play this turn.'; + view.prompt = 'Play a card for this turn.'; const faction = player_faction_map[player]; if (game.selected_cards[faction].length === 0) { view.actions.undo = 0; @@ -1046,9 +1018,6 @@ states.choose_card = { gen_action('skip'); } }, - spend_hp() { - resolve_spend_hp(); - }, card(c, player) { const faction = player_faction_map[player]; game.selected_cards[faction] = [c]; @@ -1071,32 +1040,43 @@ states.choose_card = { }, }; states.choose_final_bid = { - inactive: 'choose Final Bid', - prompt() { - view.prompt = 'Add a card to the Final Bid.'; - const faction = get_active_faction(); - for (let c of game.hands[faction]) { - if (!game.selected_cards[faction].includes(c)) { - gen_action_card(c); + inactive: 'add cards to the Final Bid', + prompt(player) { + const faction = player_faction_map[player]; + const number_selected = game.selected_cards[faction].length; + const number_hand = game.hands[faction].length; + if (number_selected < 3 && !(number_hand < 4 && number_selected === number_hand - 1)) { + for (let c of game.hands[faction]) { + if (!game.selected_cards[faction].includes(c)) { + gen_action_card(c); + } } } - gen_action('done'); + let n = 0; + for (let c of game.selected_cards[faction]) { + n += cards[c].strength; + } + if (n > 0) + view.prompt = `Final Bid for Glory: Discard up to 3 cards. Your bid is ${n} strength.`; + else + view.prompt = `Final Bid for Glory: Discard up to 3 cards for strength.`; + gen_action('confirm'); + if (game.selected_cards[faction].length > 0) + gen_action('undo'); }, - card(c) { - const faction = get_active_faction(); + card(c, player) { + const faction = player_faction_map[player]; game.selected_cards[faction].push(c); - const number_selected = game.selected_cards[faction].length; - const number_hand = game.hands[faction].length; - if (number_selected === 3 || - (number_hand < 4 && number_selected === number_hand - 1)) { + }, + undo(_, player) { + const faction = player_faction_map[player]; + game.selected_cards[faction].length--; + }, + confirm(_, player) { + set_delete(game.active, player); + if (game.active.length === 0) { resolve_active_and_proceed(); } - else { - next(); - } - }, - done() { - resolve_active_and_proceed(true); }, }; function setup_momentum() { @@ -1112,7 +1092,7 @@ function setup_momentum() { } } states.choose_medallion = { - inactive: 'claim a medallion', + inactive: 'claim a Medallion', prompt() { gen_spend_hero_points(); view.prompt = 'Claim a Medallion.'; @@ -1164,9 +1144,16 @@ states.choose_medallion = { }, }; states.confirm_turn = { - inactive: 'confirm their turn', + inactive: 'confirm their move', prompt() { - view.prompt = 'You will not be able to undo this action.'; + if (game.fascist === 2) + view.prompt = 'Fascist Test: Done.'; + else if (game.fascist === 1) { + let f = get_active_node_args().f; + view.prompt = `Fascist Event: ${f} needs to act.`; + } + else + view.prompt = 'You will not be able to undo this action.'; gen_action('confirm'); }, confirm() { @@ -1174,7 +1161,7 @@ states.confirm_turn = { }, }; states.confirm_fascist_turn = { - inactive: 'confirm fascist turn', + inactive: 'end the Fascist turn', prompt() { view.prompt = "Done."; gen_action('confirm'); @@ -1184,7 +1171,7 @@ states.confirm_fascist_turn = { }, }; states.draw_card = { - inactive: 'draw a card', + inactive: 'draw cards', auto_resolve() { const { src, v } = get_active_node_args(); if (src !== 'fascist_test') { @@ -1217,18 +1204,15 @@ function draw_glory_from_bag() { const index = random(game.bag_of_glory.length); const faction = game.bag_of_glory[index]; game.glory.push(faction); - game.glory_current_year = game.glory_current_year = [ - false, - false, - false, - ]; game.glory_current_year[faction] = true; array_remove(game.bag_of_glory, index); - logi(`Pulled T${faction} from the Bag`); + log(`Pulled T${faction} from the Bag.`); } states.draw_glory = { inactive: 'draw from the Bag of Glory', auto_resolve() { + if (get_active_faction() === game.initiative) + return false; draw_glory_from_bag(); return true; }, @@ -1260,14 +1244,12 @@ states.end_of_year_discard = { 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 if (needs_to_discard_from_hand || needs_to_discard_from_tableau) { - view.prompt = `Discard a card from your ${needs_to_discard_from_hand ? 'hand' : 'tableau'}`; + view.prompt = JSON.stringify({ needs_to_discard_from_hand, needs_to_discard_from_tableau }); + if (needs_to_discard_from_hand || needs_to_discard_from_tableau) { + view.prompt = 'End of Year: Discard cards from your hand and tableau.'; } else { - view.prompt = 'Confirm discard'; + view.prompt = 'End of Year: Done.'; view.actions.confirm = 1; } if (discarded[faction_id].h.length > 0 || discarded[faction_id].t.length > 0) { @@ -1321,7 +1303,7 @@ states.end_of_year_discard = { }, }; states.hero_points = { - inactive: 'gain Hero points', + inactive: 'gain or lose Hero points', auto_resolve() { const { src, v } = get_active_node_args(); if (src !== 'fascist_test') { @@ -1341,14 +1323,14 @@ states.hero_points = { if (value < 0) { view.prompt = value < -1 - ? `Lose ${Math.abs(value)} Hero points` - : 'Lose 1 Hero point'; + ? `Lose ${Math.abs(value)} Hero points.` + : 'Lose a Hero point.'; gen_action('lose_hp'); return; } if (game.hero_points[POOL_ID] > 0) { view.prompt = - value > 1 ? `Gain ${value} Hero points.` : 'Gain 1 Hero point.'; + value > 1 ? `Gain ${value} Hero points.` : 'Gain a Hero point.'; gen_action('gain_hp'); } else { @@ -1392,7 +1374,7 @@ function resolve_player_with_most_hero_points(faction) { resolve_active_and_proceed(); } states.select_player_with_most_hero_points = { - inactive: 'choose a Player', + inactive: 'choose who will gain or lose Hero points', prompt() { gen_spend_hero_points(); const { v } = get_active_node_args(); @@ -1456,6 +1438,7 @@ states.move_track = { can_move_track = gen_move_track(track, game.tracks[track] + value) || can_move_track; } if (!can_move_track) { + view.prompt = view.prompt.replace("Move", "Cannot move"); gen_action('skip'); } }, @@ -1483,6 +1466,14 @@ states.move_track = { resolve_active_and_proceed(); }, skip() { + const node = get_active_node(); + const track = node.a.t; + if (track === data_1.GOVERNMENT) + logi(`Government +0`); + else if (track === data_1.LIBERTY_OR_COLLECTIVIZATION) + logi("Liberty or Collectivization +0"); + else + logi(`${get_track_name(track)} +0`); resolve_active_and_proceed(); }, }; @@ -1514,7 +1505,7 @@ function can_move_track_down(track_id) { return true; } states.move_track_up_or_down = { - inactive: 'move a track', + inactive: 'move a Track', auto_resolve() { const { track_id, strength } = get_active_node_args(); const can_move_up = can_move_track_up(track_id); @@ -1570,7 +1561,7 @@ states.peek_fascist_cards = { inactive: 'peek at Fascist cards', prompt() { gen_spend_hero_points(); - view.prompt = 'Return one card to the top of the Fascist deck.'; + view.prompt = 'Return one Fascist card to the top of the Fascist deck and discard the others.'; view.fascist_cards = game.fascist_cards; for (const c of game.fascist_cards) { gen_action_card(c); @@ -1580,6 +1571,7 @@ states.peek_fascist_cards = { resolve_spend_hp(); }, card(c) { + log(">Peeked at top three Fascist cards, returned one, and discarded the others"); game.top_of_events_deck = c; for (const ec of game.fascist_cards) { if (ec !== c) { @@ -1605,12 +1597,12 @@ function set_player_turn_prompt({ can_play_card, use_ap, use_momentum, use_moral else if (use_ap) view.prompt = "Use Action Points."; else if (use_momentum) - view.prompt = "Play a second card."; + view.prompt = "Use Momentum Medallion."; else - view.prompt = "Player Turn: Done."; + view.prompt = "Player turn done."; } states.player_turn = { - inactive: 'play their turn', + inactive: 'play their card', prompt() { gen_spend_hero_points(); const faction_id = get_active_faction(); @@ -1699,11 +1691,17 @@ states.player_turn = { next(); }, }; +function remove_blank_marker(b) { + const track_id = Math.floor(b / 11); + const space_id = b % 11; + set_delete(game.triggered_track_effects, b); + logi(`Removed Blank from ${get_track_name(track_id)} ${space_id}`); +} states.remove_blank_marker = { inactive: 'remove a Blank marker', prompt() { gen_spend_hero_points(); - view.prompt = 'Remove a Blank marker'; + view.prompt = 'Remove a Blank marker.'; for (const b of game.triggered_track_effects) { gen_action_blank_marker(b); } @@ -1716,12 +1714,30 @@ states.remove_blank_marker = { resolve_spend_hp(); }, blank_marker(b) { + remove_blank_marker(b); + resolve_active_and_proceed(); + }, + skip() { + resolve_active_and_proceed(); + }, +}; +states.remove_blank_marker_archives = { + inactive: 'remove a Blank marker', + prompt() { + view.prompt = 'Archives Medallion: Remove a Blank marker.'; + for (const b of game.triggered_track_effects) { + gen_action_blank_marker(b); + } + if (game.triggered_track_effects.length === 0) { + view.prompt = 'No Blank marker to remove.'; + gen_action('skip'); + } + }, + blank_marker(b) { const faction = get_active_faction(); pay_hero_points(faction, 1); - const track_id = Math.floor(b / 11); - const space_id = b % 11; - logp(`removed blank marker from ${get_track_name(track_id)} ${space_id}`); - game.triggered_track_effects = game.triggered_track_effects.filter((id) => id !== b); + log(">M" + data_1.ARCHIVES_MEDALLION_ID); + remove_blank_marker(b); game.used_medallions.push(data_1.ARCHIVES_MEDALLION_ID); resolve_active_and_proceed(); }, @@ -1751,7 +1767,7 @@ states.remove_attack_from_fronts = { gen_action_front(id); }); if (!is_front_with_attacks) { - view.prompt = 'No valid Front to remove attacks from.'; + view.prompt = 'No Front to remove attacks from.'; gen_action('skip'); } }, @@ -1844,13 +1860,13 @@ states.spend_hero_points = { } gen_action('draw_card'); if (can_use_medallion(data_1.ARCHIVES_MEDALLION_ID, faction)) { - gen_action('remove_blank_marker'); + gen_action('archives'); if (game.triggered_track_effects.length === 0) { - view.actions['remove_blank_marker'] = 0; + view.actions['archives'] = 0; } } if (can_use_medallion(data_1.VOLUNTEERS_MEDALLION_ID, faction)) { - gen_action('add_to_front'); + gen_action('volunteers'); } if (hero_points < 2) { return; @@ -1872,10 +1888,12 @@ states.spend_hero_points = { } gen_spend_hero_points_move_track(data_1.GOVERNMENT, Math.floor(hero_points / 4)); }, - add_to_front() { + volunteers() { const faction = get_active_faction(); + log(">M" + data_1.VOLUNTEERS_MEDALLION_ID); pay_hero_points(faction, 1); insert_after_active_node(create_state_node('add_to_front', faction, { + src: 'volunteers', t: data_1.ANY, v: 1, })); @@ -1895,7 +1913,7 @@ states.spend_hero_points = { draw_hand_cards(faction, 1); resolve_active_and_proceed(); }, - remove_blank_marker() { + archives() { const faction = get_active_faction(); if (game.used_medallions) { game.used_medallions.push(data_1.ARCHIVES_MEDALLION_ID); @@ -1903,7 +1921,7 @@ states.spend_hero_points = { else { game.used_medallions = [data_1.ARCHIVES_MEDALLION_ID]; } - insert_after_active_node(create_state_node('remove_blank_marker', faction)); + insert_after_active_node(create_state_node('remove_blank_marker_archives', faction)); resolve_active_and_proceed(); }, tr0(x) { @@ -1933,7 +1951,7 @@ states.spend_hero_points = { }, }; states.swap_card_tableau_hand = { - inactive: 'swap cards', + inactive: 'swap cards in their tableau and hand', prompt() { gen_spend_hero_points(); view.prompt = 'Swap a card in your tableau with a card in your hand.'; @@ -2047,10 +2065,11 @@ function trash_card(faction) { resolve_active_and_proceed(); } states.use_organization_medallion = { - inactive: 'use Organization Medallion', + inactive: 'choose to use Organization Medallion', prompt() { - gen_spend_hero_points(); - view.prompt = 'Use Organization Medallion?'; + let { t, v } = get_active_node_args(); + view.prompt = `Organization Medallion: Spend 1 Hero point to increase ${get_track_name(t)} movement?`; + gen_action(track_action_name[t], v); gen_action('yes'); gen_action('no'); }, @@ -2062,33 +2081,36 @@ states.use_organization_medallion = { pay_hero_points(faction, 1); game.used_medallions.push(data_1.ORGANIZATION_MEDALLION_ID); let { t, v } = get_active_node_args(); - if (v > game.tracks[t]) { - v++; - } - else { - v--; - } - move_track(t, v - game.tracks[t]); + log("M" + data_1.ORGANIZATION_MEDALLION_ID + ":"); + move_track_to(t, v); resolve_active_and_proceed(); }, + tr0() { this.yes(); }, + tr1() { this.yes(); }, + tr2() { this.yes(); }, + tr3() { this.yes(); }, + tr4() { this.yes(); }, no() { - const { t, v } = get_active_node_args(); - move_track(t, v); resolve_active_and_proceed(); }, }; states.use_strategy_medallion = { - inactive: 'use Strategy Medallion', + inactive: 'choose to use Strategy Medallion', prompt() { - gen_spend_hero_points(); - view.prompt = 'Use Strategy Medallion?'; + const { f } = get_active_node_args(); + view.prompt = `Strategy Medallion: Add 1 strength to ${front_names[f]}?`; + gen_action_front(f); gen_action('yes'); gen_action('no'); }, spend_hp() { resolve_spend_hp(); }, + front(_) { + this.yes(); + }, yes() { + log(">M" + data_1.STRATEGY_MEDALLION_ID); game.used_medallions.push(data_1.STRATEGY_MEDALLION_ID); const { f } = get_active_node_args(); const faction = get_active_faction(); @@ -2407,6 +2429,7 @@ function resolve_fascist_test() { ? 2 : 0; if (can_use_medallion(data_1.PROPAGANDA_MEDALLION_ID, faction)) { + log(">M" + data_1.PROPAGANDA_MEDALLION_ID); hero_points_gain += 2; } if (hero_points_gain > 0) { @@ -2431,12 +2454,13 @@ function resolve_fascist_test() { function resolve_final_bid() { let highest_bid = 0; let winners = []; + log("Final Bid for Glory:"); for (const f of get_player_order()) { let player_bid = 0; for (const c of game.selected_cards[f]) { player_bid += cards[c].strength; } - log(`${faction_player_map[f]} bid ${player_bid} cards`); + log(`>${faction_player_map[f]} ${player_bid} strength`); if (player_bid === highest_bid) { winners.push(f); } @@ -2553,15 +2577,16 @@ function move_track_to(track_id, new_value) { : make_list(new_value, current_value - 1); triggered_spaces.forEach((space_id) => { const trigger = tracks[track_id].triggers[space_id]; + const blank = get_blank_marker_id(track_id, space_id); if (trigger !== null && - !game.triggered_track_effects.includes(get_blank_marker_id(track_id, space_id))) { - if (space_id !== 0) { - game.triggered_track_effects.push(get_blank_marker_id(track_id, space_id)); - } + !game.triggered_track_effects.includes(blank) && + !game.untriggered_track_effects.includes(blank)) { + set_delete(game.triggered_track_effects, blank); + set_add(game.untriggered_track_effects, blank); const node = resolve_effect(trigger, tracks[track_id].action); if (node !== null) { insert_after_active_node(node); - insert_after_active_node(create_function_node('log_trigger', [track_id, space_id])); + insert_after_active_node(create_function_node('place_blank_marker', [track_id, space_id])); } } }); @@ -2581,11 +2606,20 @@ function can_use_medallion(medallion_id, faction) { return can_use; } } -function insert_use_organization_medallion_node(track_id, value) { +function insert_use_organization_medallion_node(track_id, delta) { const faction = get_active_faction(); + if (delta > 0) + delta = 1; + else if (delta < 0) + delta = -1; + else + return; + let v = game.tracks[track_id] + delta; + if (v < 0 || v > 10) + return; insert_after_active_node(create_state_node('use_organization_medallion', faction, { t: track_id, - v: value, + v: v, })); } function update_bonus(bonus_id, status) { @@ -2800,7 +2834,8 @@ function resolve_effect(effect, source) { } function win_final_bid(faction_id) { log_br(); - log(`${faction_player_map[faction_id]} won the Final Bid`); + log(`${faction_player_map[faction_id]} won the Final Bid:`); + logi("Placed T" + faction_id); game.glory.push(faction_id); } function win_game(player, glory) { @@ -2858,9 +2893,12 @@ function log_header(msg, prefix) { log(`#${prefix} ${msg}`); log_br(); } -function log_trigger(args) { +function place_blank_marker(args) { let [track_id, space_id] = args; + let blank = get_blank_marker_id(track_id, space_id); log(`Trigger ${get_track_name(track_id)} ${space_id}:`); + set_delete(game.untriggered_track_effects, blank); + set_add(game.triggered_track_effects, blank); resolve_active_and_proceed(); } function logi(msg) { @@ -2996,12 +3034,34 @@ function get_source_name(source) { case 'tr3': return tracks[3].name + ' Trigger'; case 'tr4': return tracks[4].name + ' Trigger'; case 'track_icon': - return 'Track Trigger'; + throw "UNUSED"; + case 'volunteers': + return 'Volunteers Medallion'; case data_1.MOMENTUM: return 'Momentum'; } return source; } +function get_source_inactive(source) { + switch (source) { + case 'player_event': + return 'execute ' + cards[game.played_card].title; + case 'fascist_event': + return 'execute ' + cards[game.current_events[game.current_events.length - 1]].title; + case 'fascist_test': + return 'resolve Test'; + case 'tr0': return 'trigger ' + tracks[0].name + ' icon'; + case 'tr1': return 'trigger ' + tracks[1].name + ' icon'; + case 'tr2': return 'trigger ' + tracks[2].name + ' icon'; + case 'tr3': return 'trigger ' + tracks[3].name + ' icon'; + case 'tr4': return 'trigger ' + tracks[4].name + ' icon'; + case 'track_icon': + throw "UNUSED"; + case data_1.MOMENTUM: + return 'use Momentum'; + } + return source; +} function get_factions_with_most_hero_poins() { let most_hero_points = null; let faction_ids = []; @@ -3037,6 +3097,8 @@ function list_deck(id) { return; if (game.discard[id].includes(card)) return; + if (game.fascist_cards && game.fascist_cards.includes(card)) + return; } else if (game.hands[id].includes(card) || game.discard[id].includes(card) || @@ -3136,6 +3198,21 @@ function array_insert(array, index, item) { array[i] = array[i - 1]; array[index] = item; } +function set_add(set, item) { + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) + b = m - 1; + else if (item > x) + a = m + 1; + else + return set; + } + return array_insert(set, a, item); +} function set_delete(set, item) { let a = 0; let b = set.length - 1; @@ -168,7 +168,7 @@ function gen_spend_hero_points() { } } -const multiactive_states = ['choose_card', 'end_of_year_discard']; +const multiactive_states = ['choose_card', 'end_of_year_discard', 'choose_final_bid']; export function action( state: Game, @@ -257,10 +257,8 @@ function setup_final_bid() { game.fascist = 0; log_header('Final Bid', 't'); - const player_order = get_player_order(); - game.engine = player_order.map((faction_id) => - create_state_node('choose_final_bid', faction_id) - ); + + game.engine = [create_state_node('choose_final_bid', 'all')]; game.engine.push(create_function_node('checkpoint')); game.engine.push(create_function_node('resolve_final_bid')); game.engine.push(create_function_node('setup_choose_card')); @@ -337,7 +335,7 @@ const engine_functions: Record<string, Function> = { start_year, resolve_fascist_test, resolve_final_bid, - log_trigger, + place_blank_marker, // Unique card effects card1_event2, card3_event2, @@ -484,7 +482,7 @@ function next(checkpoint = false) { if (next_active !== current_active && game.undo.length > 0) { insert_before_active_node( - create_state_node('confirm_turn', get_active_faction()) + create_state_node('confirm_turn', get_active_faction(), { f: next_active }) ); game.state = 'confirm_turn'; return; @@ -561,7 +559,8 @@ function game_view(state: Game, current: Player | 'Observer') { current !== game.active && !game.active.includes(current as Player) ) { - let inactive = states[game.state].inactive || game.state; + let src = get_active_node_args()?.src; + let inactive = src ? get_source_inactive(src) : states[game.state].inactive || game.state; view.prompt = Array.isArray(game.active) ? `Waiting for ${game.active.join(' and ')} to ${inactive}.` : `Waiting for ${game.active} to ${inactive}.`; @@ -653,6 +652,7 @@ export function setup(seed: number, _scenario: string, options: Record<string,bo [], ], triggered_track_effects: [], + untriggered_track_effects: [], log: [], undo: [], used_medallions: [], @@ -688,6 +688,7 @@ function draw_hand_cards(faction_id: FactionId, count: number, indent = true) { const deck = list_deck(faction_id); if (game.medallions[faction_id].includes(INTELLIGENCE_MEDALLION_ID)) { + log(">M" + INTELLIGENCE_MEDALLION_ID); count++; } let drawn_cards = 0; @@ -774,7 +775,7 @@ const track_icon_to_track_id_map = { }; states.activate_icon = { - inactive: 'activate an icon', + inactive: 'activate a Morale Bonus icon', prompt() { gen_spend_hero_points(); const c = cards[game.played_card] as PlayerCard; @@ -867,46 +868,19 @@ states.activate_icon = { ); resolve_active_and_proceed(); }, - tr0(x: number) { - if (can_use_medallion(ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(LIBERTY, x); - } else { - move_track_to(0, x); - } - resolve_active_and_proceed(); - }, - tr1(x: number) { - if (can_use_medallion(ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(COLLECTIVIZATION, x); - } else { - move_track_to(1, x); - } - resolve_active_and_proceed(); - }, - tr2(x: number) { + trX(x: number, track: number) { + let old = game.tracks[track] + move_track_to(track, x); if (can_use_medallion(ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(GOVERNMENT, x); - } else { - move_track_to(2, x); - } - resolve_active_and_proceed(); - }, - tr3(x: number) { - if (can_use_medallion(ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(SOVIET_SUPPORT, x); - } else { - move_track_to(3, x); - } - resolve_active_and_proceed(); - }, - tr4(x: number) { - if (can_use_medallion(ORGANIZATION_MEDALLION_ID)) { - insert_use_organization_medallion_node(FOREIGN_AID, x); - } else { - move_track_to(4, x); + insert_use_organization_medallion_node(track, game.tracks[track] - old); } resolve_active_and_proceed(); }, + tr0(x: number) { this.trX(x, 0) }, + tr1(x: number) { this.trX(x, 1) }, + tr2(x: number) { this.trX(x, 2) }, + tr3(x: number) { this.trX(x, 3) }, + tr4(x: number) { this.trX(x, 4) }, draw_card() { draw_hand_cards( get_active_faction(), @@ -981,7 +955,7 @@ states.add_to_front = { const args = get_active_node_args(); const possible_fronts = get_fronts_to_add_to(args.t); if (possible_fronts.length === 0) { - view.prompt = 'No valid front to add strength to.' + view.prompt = 'Cannot support ' + front_names[args.t] + '.' gen_action('skip'); } else if (possible_fronts.length === 4) { view.prompt = `Support any Front.`; @@ -1016,7 +990,7 @@ states.attack_front = { const number_of_fronts = possible_fronts.length; if (number_of_fronts === 0) { - view.prompt = 'No valid front to attack.'; + view.prompt = 'Cannot attack ' + front_names[target] + '.' gen_action('skip'); } else if (possible_fronts.length === 4) { view.prompt = `Attack any Front.`; @@ -1042,7 +1016,7 @@ states.attack_front = { states.break_tie_final_bid = { inactive: 'break tie for Final Bid', prompt() { - view.prompt = 'Choose the winner of the Final Bid'; + view.prompt = 'Choose the winner of the Final Bid.'; const { winners } = get_active_node_args(); for (const f of winners) { gen_action(faction_player_map[f]); @@ -1065,7 +1039,7 @@ states.break_tie_final_bid = { states.break_tie_winner = { inactive: 'break tie for winner of the game', prompt() { - view.prompt = 'Choose the winner of the game'; + view.prompt = 'Choose the winner of the game.'; const { winners } = get_active_node_args(); for (const f of winners) { gen_action(faction_player_map[f]); @@ -1104,7 +1078,7 @@ states.change_active_player = { }; states.choose_area_ap = { - inactive: 'choose area to use Action Points', + inactive: 'use action points', prompt() { gen_spend_hero_points(); const use_morale_bonus = game.can_use_mb === 1 && game.bonuses[MORALE_BONUS] === ON; @@ -1213,7 +1187,7 @@ states.choose_area_ap = { }; states.change_bonus = { - inactive: 'select Bonus', + inactive: 'toggle Bonus', prompt() { gen_spend_hero_points(); const args = get_active_node_args(); @@ -1223,7 +1197,10 @@ states.change_bonus = { game.bonuses[MORALE_BONUS] === ON) || (args.v === OFF && game.bonuses[args.t] === OFF) ) { - view.prompt = `${bonus_names[args.t]} is already ${args.v === OFF ? 'off' : 'on'}.`; + if (args.t === 'any') + view.prompt = `Both bonuses are already ${args.v === OFF ? 'off' : 'on'}.`; + else + view.prompt = `${bonus_names[args.t]} is already ${args.v === OFF ? 'off' : 'on'}.`; gen_action('skip'); } else if (args.t === ANY && args.v === ON) { @@ -1248,18 +1225,18 @@ states.change_bonus = { }, skip() { const args = get_active_node_args(); - logi(`${bonus_names[args.t]} ${args.v === OFF ? 'off' : 'on'}`); + logi(`Bonus already ${args.v === OFF ? 'off' : 'on'}`); resolve_active_and_proceed(); }, }; // Used for effects that allow play of an extra card states.play_card = { - inactive: 'play a card', + inactive: 'play another card', prompt() { gen_spend_hero_points(); - view.prompt = 'Play a card.'; + view.prompt = 'Play another card.'; const faction = get_active_faction(); @@ -1286,7 +1263,7 @@ states.play_card = { const args = get_active_node_args(); if (args && args.src === 'momentum') { - log_header("~ Momentum ~\nC" + game.played_card, faction); + log_header("~ M" + MOMENTUM_MEDALLION_ID + " ~\nC" + game.played_card, faction); } else { log_header("~ Play Card ~\nC" + game.played_card, faction); } @@ -1302,10 +1279,9 @@ states.play_card = { // Multiactive choose card state states.choose_card = { - inactive: 'choose a card', + inactive: 'play a card for this turn', prompt(player: Player) { - gen_spend_hero_points(); - view.prompt = 'Choose a card to play this turn.'; + view.prompt = 'Play a card for this turn.'; const faction = player_faction_map[player]; @@ -1326,9 +1302,6 @@ states.choose_card = { gen_action('skip'); } }, - spend_hp() { - resolve_spend_hp(); - }, card(c: CardId, player: Player) { const faction = player_faction_map[player]; game.selected_cards[faction] = [c]; @@ -1352,35 +1325,47 @@ states.choose_card = { }; states.choose_final_bid = { - inactive: 'choose Final Bid', - prompt() { - view.prompt = 'Add a card to the Final Bid.'; - const faction = get_active_faction(); - for (let c of game.hands[faction]) { - if (!game.selected_cards[faction].includes(c)) { - gen_action_card(c); + inactive: 'add cards to the Final Bid', + prompt(player: Player) { + const faction = player_faction_map[player]; + + const number_selected = game.selected_cards[faction].length; + const number_hand = game.hands[faction].length; + if (number_selected < 3 && !(number_hand < 4 && number_selected === number_hand - 1)) { + for (let c of game.hands[faction]) { + if (!game.selected_cards[faction].includes(c)) { + gen_action_card(c); + } } } - gen_action('done'); + + let n = 0 + for (let c of game.selected_cards[faction]) { + n += (cards[c] as PlayerCard).strength + } + if (n > 0) + view.prompt = `Final Bid for Glory: Discard up to 3 cards. Your bid is ${n} strength.` + else + view.prompt = `Final Bid for Glory: Discard up to 3 cards for strength.` + + gen_action('confirm'); + if (game.selected_cards[faction].length > 0) + gen_action('undo'); }, - card(c: CardId) { - const faction = get_active_faction(); + card(c: CardId, player: Player) { + const faction = player_faction_map[player]; game.selected_cards[faction].push(c); - const number_selected = game.selected_cards[faction].length; - - const number_hand = game.hands[faction].length; - if ( - number_selected === 3 || - (number_hand < 4 && number_selected === number_hand - 1) - ) { + }, + undo(_, player: Player) { + const faction = player_faction_map[player]; + game.selected_cards[faction].length--; + }, + confirm(_, player: Player) { + set_delete(game.active as Player[], player); + if (game.active.length === 0) { resolve_active_and_proceed(); - } else { - next(); } }, - done() { - resolve_active_and_proceed(true); - }, }; function setup_momentum() { @@ -1401,7 +1386,7 @@ function setup_momentum() { } states.choose_medallion = { - inactive: 'claim a medallion', + inactive: 'claim a Medallion', prompt() { gen_spend_hero_points(); view.prompt = 'Claim a Medallion.'; @@ -1457,9 +1442,15 @@ states.choose_medallion = { }; states.confirm_turn = { - inactive: 'confirm their turn', + inactive: 'confirm their move', prompt() { - view.prompt = 'You will not be able to undo this action.'; + if (game.fascist === 2) + view.prompt = 'Fascist Test: Done.' + else if (game.fascist === 1) { + let f = get_active_node_args().f + view.prompt = `Fascist Event: ${f} needs to act.` + } else + view.prompt = 'You will not be able to undo this action.' gen_action('confirm'); }, confirm() { @@ -1468,7 +1459,7 @@ states.confirm_turn = { }; states.confirm_fascist_turn = { - inactive: 'confirm fascist turn', + inactive: 'end the Fascist turn', prompt() { view.prompt = "Done."; gen_action('confirm'); @@ -1479,7 +1470,7 @@ states.confirm_fascist_turn = { }; states.draw_card = { - inactive: 'draw a card', + inactive: 'draw cards', auto_resolve() { const { src, v } = get_active_node_args(); if (src !== 'fascist_test') { @@ -1515,22 +1506,19 @@ function draw_glory_from_bag() { game.glory.push(faction); - game.glory_current_year = game.glory_current_year = [ - false, - false, - false, - ]; - game.glory_current_year[faction] = true; array_remove(game.bag_of_glory, index); - logi(`Pulled T${faction} from the Bag`); + log(`Pulled T${faction} from the Bag.`); } states.draw_glory = { inactive: 'draw from the Bag of Glory', auto_resolve() { + // active player to trigger the draw + if (get_active_faction() === game.initiative) + return false; draw_glory_from_bag(); return true; }, @@ -1566,14 +1554,11 @@ states.end_of_year_discard = { 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 if (needs_to_discard_from_hand || needs_to_discard_from_tableau) { - view.prompt = `Discard a card from your ${ - needs_to_discard_from_hand ? 'hand' : 'tableau' - }`; + view.prompt = JSON.stringify({needs_to_discard_from_hand,needs_to_discard_from_tableau}); + if (needs_to_discard_from_hand || needs_to_discard_from_tableau) { + view.prompt = 'End of Year: Discard cards from your hand and tableau.'; } else { - view.prompt = 'Confirm discard'; + view.prompt = 'End of Year: Done.'; view.actions.confirm = 1; } if (discarded[faction_id].h.length > 0 || discarded[faction_id].t.length > 0) { @@ -1633,7 +1618,7 @@ states.end_of_year_discard = { }; states.hero_points = { - inactive: 'gain Hero points', + inactive: 'gain or lose Hero points', auto_resolve() { const { src, v } = get_active_node_args(); if (src !== 'fascist_test') { @@ -1652,14 +1637,14 @@ states.hero_points = { if (value < 0) { view.prompt = value < -1 - ? `Lose ${Math.abs(value)} Hero points` - : 'Lose 1 Hero point'; + ? `Lose ${Math.abs(value)} Hero points.` + : 'Lose a Hero point.'; gen_action('lose_hp'); return; } if (game.hero_points[POOL_ID] > 0) { view.prompt = - value > 1 ? `Gain ${value} Hero points.` : 'Gain 1 Hero point.'; + value > 1 ? `Gain ${value} Hero points.` : 'Gain a Hero point.'; gen_action('gain_hp'); } else { view.prompt = 'No Hero points available in pool.'; @@ -1704,7 +1689,7 @@ function resolve_player_with_most_hero_points(faction: FactionId) { } states.select_player_with_most_hero_points = { - inactive: 'choose a Player', + inactive: 'choose who will gain or lose Hero points', prompt() { gen_spend_hero_points(); const { v } = get_active_node_args(); @@ -1774,6 +1759,7 @@ states.move_track = { can_move_track = gen_move_track(track, game.tracks[track] + value) || can_move_track; } if (!can_move_track) { + view.prompt = view.prompt.replace("Move", "Cannot move") gen_action('skip'); } }, @@ -1801,6 +1787,14 @@ states.move_track = { resolve_active_and_proceed(); }, skip() { + const node = get_active_node(); + const track = node.a.t + if (track === GOVERNMENT) + logi(`Government +0`) + else if (track === LIBERTY_OR_COLLECTIVIZATION) + logi("Liberty or Collectivization +0") + else + logi(`${get_track_name(track)} +0`) resolve_active_and_proceed(); }, }; @@ -1856,7 +1850,7 @@ function can_move_track_down(track_id): boolean { // NOTE: we can probably remove this state. I don't think it's used anywhere anymore states.move_track_up_or_down = { - inactive: 'move a track', + inactive: 'move a Track', auto_resolve() { const { track_id, strength } = get_active_node_args(); const can_move_up = can_move_track_up(track_id); @@ -1910,7 +1904,7 @@ states.peek_fascist_cards = { inactive: 'peek at Fascist cards', prompt() { gen_spend_hero_points(); - view.prompt = 'Return one card to the top of the Fascist deck.'; + view.prompt = 'Return one Fascist card to the top of the Fascist deck and discard the others.'; view.fascist_cards = game.fascist_cards; for (const c of game.fascist_cards) { gen_action_card(c); @@ -1920,6 +1914,7 @@ states.peek_fascist_cards = { resolve_spend_hp(); }, card(c: CardId) { + log(">Peeked at top three Fascist cards, returned one, and discarded the others") game.top_of_events_deck = c; for (const ec of game.fascist_cards) { if (ec !== c) { @@ -1956,13 +1951,13 @@ function set_player_turn_prompt({ else if (use_ap) view.prompt = "Use Action Points." else if (use_momentum) - view.prompt = "Play a second card." + view.prompt = "Use Momentum Medallion." else - view.prompt = "Player Turn: Done." + view.prompt = "Player turn done." } states.player_turn = { - inactive: 'play their turn', + inactive: 'play their card', prompt() { gen_spend_hero_points(); const faction_id = get_active_faction(); @@ -2070,11 +2065,18 @@ states.player_turn = { }, }; +function remove_blank_marker(b: number) { + const track_id = Math.floor(b / 11); + const space_id = b % 11; + set_delete(game.triggered_track_effects, b); + logi(`Removed Blank from ${get_track_name(track_id)} ${space_id}`); +} + states.remove_blank_marker = { inactive: 'remove a Blank marker', prompt() { gen_spend_hero_points(); - view.prompt = 'Remove a Blank marker'; + view.prompt = 'Remove a Blank marker.'; for (const b of game.triggered_track_effects) { gen_action_blank_marker(b); @@ -2088,15 +2090,31 @@ states.remove_blank_marker = { resolve_spend_hp(); }, blank_marker(b: number) { + remove_blank_marker(b); + resolve_active_and_proceed(); + }, + skip() { + resolve_active_and_proceed(); + }, +}; + +states.remove_blank_marker_archives = { + inactive: 'remove a Blank marker', + prompt() { + view.prompt = 'Archives Medallion: Remove a Blank marker.'; + for (const b of game.triggered_track_effects) { + gen_action_blank_marker(b); + } + if (game.triggered_track_effects.length === 0) { + view.prompt = 'No Blank marker to remove.'; + gen_action('skip'); + } + }, + blank_marker(b: number) { const faction = get_active_faction(); pay_hero_points(faction, 1); - - const track_id = Math.floor(b / 11); - const space_id = b % 11; - logp(`removed blank marker from ${get_track_name(track_id)} ${space_id}`); - game.triggered_track_effects = game.triggered_track_effects.filter( - (id) => id !== b - ); + log(">M" + ARCHIVES_MEDALLION_ID); + remove_blank_marker(b); game.used_medallions.push(ARCHIVES_MEDALLION_ID); resolve_active_and_proceed(); }, @@ -2134,7 +2152,7 @@ states.remove_attack_from_fronts = { gen_action_front(id); }); if (!is_front_with_attacks) { - view.prompt = 'No valid Front to remove attacks from.'; + view.prompt = 'No Front to remove attacks from.'; gen_action('skip'); } }, @@ -2244,13 +2262,13 @@ states.spend_hero_points = { gen_action('draw_card'); if (can_use_medallion(ARCHIVES_MEDALLION_ID, faction)) { - gen_action('remove_blank_marker'); + gen_action('archives'); if (game.triggered_track_effects.length === 0) { - view.actions['remove_blank_marker'] = 0; + view.actions['archives'] = 0; } } if (can_use_medallion(VOLUNTEERS_MEDALLION_ID, faction)) { - gen_action('add_to_front'); + gen_action('volunteers'); } if (hero_points < 2) { @@ -2285,11 +2303,13 @@ states.spend_hero_points = { gen_spend_hero_points_move_track(GOVERNMENT, Math.floor(hero_points / 4)); }, - add_to_front() { + volunteers() { const faction = get_active_faction(); + log(">M" + VOLUNTEERS_MEDALLION_ID) pay_hero_points(faction, 1); insert_after_active_node( create_state_node('add_to_front', faction, { + src: 'volunteers', t: ANY, v: 1, }) @@ -2310,14 +2330,14 @@ states.spend_hero_points = { draw_hand_cards(faction, 1); resolve_active_and_proceed(); }, - remove_blank_marker() { + archives() { const faction = get_active_faction(); if (game.used_medallions) { game.used_medallions.push(ARCHIVES_MEDALLION_ID); } else { game.used_medallions = [ARCHIVES_MEDALLION_ID]; } - insert_after_active_node(create_state_node('remove_blank_marker', faction)); + insert_after_active_node(create_state_node('remove_blank_marker_archives', faction)); resolve_active_and_proceed(); }, tr0(x: number) { @@ -2355,7 +2375,7 @@ states.spend_hero_points = { Use the length of selected_cards[faction] to figure out where we are. */ states.swap_card_tableau_hand = { - inactive: 'swap cards', + inactive: 'swap cards in their tableau and hand', prompt() { gen_spend_hero_points(); view.prompt = 'Swap a card in your tableau with a card in your hand.'; @@ -2476,11 +2496,12 @@ function trash_card(faction: FactionId) { } states.use_organization_medallion = { - inactive: 'use Organization Medallion', + inactive: 'choose to use Organization Medallion', prompt() { - gen_spend_hero_points(); - view.prompt = 'Use Organization Medallion?'; - + // gen_spend_hero_points(); // confusing when available during this question + let { t, v } = get_active_node_args(); + view.prompt = `Organization Medallion: Spend 1 Hero point to increase ${get_track_name(t)} movement?`; + gen_action(track_action_name[t], v) gen_action('yes'); gen_action('no'); }, @@ -2491,42 +2512,39 @@ states.use_organization_medallion = { const faction = get_active_faction(); pay_hero_points(faction, 1); game.used_medallions.push(ORGANIZATION_MEDALLION_ID); - - // Value is the clicked location on the track let { t, v } = get_active_node_args(); - - // If player uses medallion we need to add or subtract - // depending on direction of movement - if (v > game.tracks[t]) { - v++; - } else { - v--; - } - - move_track(t, v - game.tracks[t]); + log("M" + ORGANIZATION_MEDALLION_ID + ":") + move_track_to(t, v); resolve_active_and_proceed(); }, + tr0() { this.yes() }, + tr1() { this.yes() }, + tr2() { this.yes() }, + tr3() { this.yes() }, + tr4() { this.yes() }, no() { - const { t, v } = get_active_node_args(); - - move_track(t, v); resolve_active_and_proceed(); }, }; states.use_strategy_medallion = { - inactive: 'use Strategy Medallion', + inactive: 'choose to use Strategy Medallion', prompt() { - gen_spend_hero_points(); - view.prompt = 'Use Strategy Medallion?'; - + // gen_spend_hero_points(); // confusing when available during this question + const { f } = get_active_node_args(); + view.prompt = `Strategy Medallion: Add 1 strength to ${front_names[f]}?`; + gen_action_front(f) gen_action('yes'); gen_action('no'); }, spend_hp() { resolve_spend_hp(); }, + front(_) { + this.yes() + }, yes() { + log(">M" + STRATEGY_MEDALLION_ID); game.used_medallions.push(STRATEGY_MEDALLION_ID); const { f } = get_active_node_args(); const faction = get_active_faction(); @@ -2960,6 +2978,7 @@ function resolve_fascist_test() { ? 2 : 0; if (can_use_medallion(PROPAGANDA_MEDALLION_ID, faction)) { + log(">M" + PROPAGANDA_MEDALLION_ID); hero_points_gain += 2; } if (hero_points_gain > 0) { @@ -2991,12 +3010,13 @@ function resolve_fascist_test() { function resolve_final_bid() { let highest_bid = 0; let winners: FactionId[] = []; + log("Final Bid for Glory:") for (const f of get_player_order()) { let player_bid = 0; for (const c of game.selected_cards[f]) { player_bid += (cards[c] as PlayerCard).strength; } - log(`${faction_player_map[f]} bid ${player_bid} cards`); + log(`>${faction_player_map[f]} ${player_bid} strength`); if (player_bid === highest_bid) { winners.push(f); } else if (player_bid > highest_bid) { @@ -3136,21 +3156,18 @@ function move_track_to(track_id: number, new_value: number) { triggered_spaces.forEach((space_id) => { const trigger = tracks[track_id].triggers[space_id]; + const blank = get_blank_marker_id(track_id, space_id); if ( trigger !== null && - !game.triggered_track_effects.includes( - get_blank_marker_id(track_id, space_id) - ) + !game.triggered_track_effects.includes(blank) && + !game.untriggered_track_effects.includes(blank) ) { - if (space_id !== 0) { - game.triggered_track_effects.push( - get_blank_marker_id(track_id, space_id) - ); - } + set_delete(game.triggered_track_effects, blank); + set_add(game.untriggered_track_effects, blank); const node = resolve_effect(trigger, tracks[track_id].action); if (node !== null) { insert_after_active_node(node); - insert_after_active_node(create_function_node('log_trigger', [track_id, space_id])); + insert_after_active_node(create_function_node('place_blank_marker', [track_id, space_id])); } } }); @@ -3176,14 +3193,22 @@ function can_use_medallion(medallion_id: number, faction?: FactionId) { function insert_use_organization_medallion_node( track_id: number, - value: number + delta: number ) { const faction = get_active_faction(); - + if (delta > 0) + delta = 1 + else if (delta < 0) + delta = -1 + else + return; + let v = game.tracks[track_id] + delta + if (v < 0 || v > 10) + return; insert_after_active_node( create_state_node('use_organization_medallion', faction, { t: track_id, - v: value, + v: v, }) ); } @@ -3471,7 +3496,8 @@ function resolve_effect(effect: Effect, source?: EffectSource): EngineNode { function win_final_bid(faction_id: FactionId) { log_br(); - log(`${faction_player_map[faction_id]} won the Final Bid`); + log(`${faction_player_map[faction_id]} won the Final Bid:`); + logi("Placed T" + faction_id) game.glory.push(faction_id); } @@ -3555,9 +3581,12 @@ function log_header(msg: string, prefix: string | number) { log_br(); } -function log_trigger(args) { +function place_blank_marker(args) { let [ track_id, space_id ] = args; + let blank = get_blank_marker_id(track_id, space_id); log(`Trigger ${get_track_name(track_id)} ${space_id}:`); + set_delete(game.untriggered_track_effects, blank); + set_add(game.triggered_track_effects, blank); resolve_active_and_proceed(); } @@ -3729,13 +3758,36 @@ function get_source_name(source: EffectSource): string { case 'tr3': return tracks[3].name + ' Trigger'; case 'tr4': return tracks[4].name + ' Trigger'; case 'track_icon': - return 'Track Trigger'; + throw "UNUSED" + case 'volunteers': + return 'Volunteers Medallion' case MOMENTUM: return 'Momentum'; } return source; } +function get_source_inactive(source: EffectSource): string { + switch (source) { + case 'player_event': + return 'execute ' + cards[game.played_card].title; + case 'fascist_event': + return 'execute ' + cards[game.current_events[game.current_events.length - 1]].title; + case 'fascist_test': + return 'resolve Test'; + case 'tr0': return 'trigger ' + tracks[0].name + ' icon'; + case 'tr1': return 'trigger ' + tracks[1].name + ' icon'; + case 'tr2': return 'trigger ' + tracks[2].name + ' icon'; + case 'tr3': return 'trigger ' + tracks[3].name + ' icon'; + case 'tr4': return 'trigger ' + tracks[4].name + ' icon'; + case 'track_icon': + throw "UNUSED" + case MOMENTUM: + return 'use Momentum'; + } + return source; +} + function get_factions_with_most_hero_poins(): FactionId[] { let most_hero_points = null; let faction_ids = []; @@ -3771,6 +3823,7 @@ function list_deck(id: FactionId | FascistId) { if (id === FASCIST_ID) { if (game.current_events.includes(card)) return; if (game.discard[id].includes(card)) return; + if (game.fascist_cards && game.fascist_cards.includes(card)) return; } else if ( game.hands[id].includes(card) || game.discard[id].includes(card) || @@ -3921,19 +3974,19 @@ function array_insert<T>(array: T[], index: number, item: T) { // return false; // } -// function set_add<T>(set: T[], item: T) { -// // eslint-disable-line @typescript-eslint/no-unused-vars -// let a = 0; -// let b = set.length - 1; -// while (a <= b) { -// const m = (a + b) >> 1; -// const x = set[m]; -// if (item < x) b = m - 1; -// else if (item > x) a = m + 1; -// else return set; -// } -// return array_insert(set, a, item); -// } +function set_add<T>(set: T[], item: T) { + // eslint-disable-line @typescript-eslint/no-unused-vars + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) b = m - 1; + else if (item > x) a = m + 1; + else return set; + } + return array_insert(set, a, item); +} // function set_delete<T>(set: T[], item: T) { // let a = 0; @@ -75,6 +75,7 @@ export interface Game { tracks: number[]; trash: CardId[][]; triggered_track_effects: number[]; + untriggered_track_effects: number[]; used_medallions: number[]; glory_current_year?: boolean[] | null; fascist: 0 | 1 | 2; @@ -190,7 +191,7 @@ export interface PlayerCard extends CardBase { icons: Icon[]; } -export type EffectSource = 'player_event' | 'fascist_event' | 'fascist_test' | 'track_icon' | 'momentum' | 'tr0' | 'tr1' | 'tr2' | 'tr3' | 'tr4'; +export type EffectSource = 'player_event' | 'fascist_event' | 'fascist_test' | 'track_icon' | 'momentum' | 'tr0' | 'tr1' | 'tr2' | 'tr3' | 'tr4' | 'volunteers'; export interface Effect { type: |