diff options
-rw-r--r-- | data.js | 2 | ||||
-rw-r--r-- | data.ts | 2 | ||||
-rw-r--r-- | land-and-freedom.css | 46 | ||||
-rw-r--r-- | land-and-freedom.scss | 52 | ||||
-rw-r--r-- | play.html | 15 | ||||
-rw-r--r-- | play.js | 68 | ||||
-rw-r--r-- | play.ts | 82 | ||||
-rw-r--r-- | rules.js | 333 | ||||
-rw-r--r-- | rules.ts | 408 | ||||
-rw-r--r-- | types.d.ts | 62 |
10 files changed, 898 insertions, 172 deletions
@@ -1686,7 +1686,7 @@ const data = { test: { front: MADRID, value: 0, - pass: create_effect('play_card', INITIATIVE_PLAYER, 1), + pass: create_effect('play_card', INITIATIVE_PLAYER, 1, INITIATIVE_PLAYER), fail: create_effect('bonus', MORALE_BONUS, OFF), }, title: 'MILITARY DICTATORSHIP', @@ -1753,7 +1753,7 @@ const data: StaticData = { test: { front: MADRID, value: 0, - pass: create_effect('play_card', INITIATIVE_PLAYER, 1), + pass: create_effect('play_card', INITIATIVE_PLAYER, 1, INITIATIVE_PLAYER), fail: create_effect('bonus', MORALE_BONUS, OFF), }, title: 'MILITARY DICTATORSHIP', diff --git a/land-and-freedom.css b/land-and-freedom.css index 09c834b..0fb77dc 100644 --- a/land-and-freedom.css +++ b/land-and-freedom.css @@ -26,6 +26,10 @@ main { background-image: url(images/map100.png); } } +#current_events .card:last-child { + transform: scale(1.2); +} + /* CURRENT CARD */ #turn_info { border-bottom: 1px solid black; @@ -133,6 +137,10 @@ main { min-height: 260px; } +.panel_body[data-active=inactive] { + display: none; +} + .panel_header { color: floralwhite; user-select: none; @@ -142,18 +150,41 @@ main { background-color: red; } -#hand_header { +#player_area_header { background-color: #273b09; + display: flex; + border-bottom: none; +} +#player_area_header .player_area_tab { + cursor: pointer; + width: 25%; + border-bottom: 2px solid #333; + border-left: 1px solid #333; + border-right: 1px solid #333; +} +#player_area_header .player_area_tab[data-active=active] { + width: 25%; + background-color: #58641d; + border-bottom: none; +} +#player_area_header #hand_tab { + border-left: none; +} +#player_area_header #trash_tab { + border-right: none; } +#player_area_header[data-faction-id=a], .panel_header[data-faction-id=a] { background-color: rgb(93, 89, 106); } +#player_area_header[data-faction-id=c], .panel_header[data-faction-id=c] { background-color: rgb(237, 36, 27); } +#player_area_header[data-faction-id=m], .panel_header[data-faction-id=m] { background-color: rgb(134, 44, 97); } @@ -661,6 +692,15 @@ main { background-size: 100% 100%; } +#log .faction_token { + display: inline-block; + width: 25px; + height: 25px; + border-radius: 2px; + vertical-align: middle; + margin: 0px 1px; +} + #glory .faction_token { position: absolute; width: 34px; @@ -710,7 +750,7 @@ main { .front.action, .medallion.action, .standee.action { - box-shadow: 0 0 0 3px white; + box-shadow: 0 0 0 3px yellow; } .blank_marker.action { @@ -724,7 +764,7 @@ main { .front.action:hover, .medallion.action:hover, .standee.action:hover { - box-shadow: 0 0 0 3px yellow; + box-shadow: 0 0 0 3px blue; } .blank_marker.action:hover { diff --git a/land-and-freedom.scss b/land-and-freedom.scss index 14ede73..07c55b2 100644 --- a/land-and-freedom.scss +++ b/land-and-freedom.scss @@ -1,8 +1,8 @@ // @use "sass:math"; @use 'sass:map'; -$selectable-color: white; // yellow; -$selected-color: yellow; //blue; +$selectable-color: yellow; // yellow; +$selected-color: blue; //blue; $anarchist-color: rgb(93, 89, 106); $communist-color: rgb(237, 36, 27); @@ -50,6 +50,12 @@ main { } } +#current_events { + .card:last-child { + transform: scale(1.2); + } +} + /* CURRENT CARD */ #turn_info { @@ -172,6 +178,10 @@ main { min-height: 260px; } +.panel_body[data-active="inactive"] { + display: none; +} + .panel_header { color: floralwhite; user-select: none; @@ -181,18 +191,45 @@ main { background-color: red; } -#hand_header { +#player_area_header { background-color: #273b09; + display: flex; + border-bottom: none; + + .player_area_tab { + cursor: pointer; + width: 25%; + border-bottom: 2px solid #333; + border-left: 1px solid #333; + border-right: 1px solid #333; + } + + .player_area_tab[data-active="active"] { + width: 25%; + background-color: #58641d; + border-bottom: none; + } + + #hand_tab { + border-left: none; + } + + #trash_tab { + border-right: none; + } } +#player_area_header[data-faction-id='a'], .panel_header[data-faction-id='a'] { background-color: $anarchist-color; } +#player_area_header[data-faction-id='c'], .panel_header[data-faction-id='c'] { background-color: $communist-color; } +#player_area_header[data-faction-id='m'], .panel_header[data-faction-id='m'] { background-color: $moderate-color; } @@ -303,6 +340,15 @@ main { // box-shadow: 0 0 0 1px #333; } +#log .faction_token { + display: inline-block; + width: 25px; + height: 25px; + border-radius: 2px; + vertical-align: middle; + margin: 0px 1px; +} + #glory .faction_token { position: absolute; width: 34px; @@ -35,6 +35,7 @@ <div class="game_info"> <span id="year"></span> <span id="pool_hero_points"></span> + <span id="bag_of_glory"></span> </div> <div id="roles"> <div id="role_Anarchist" class="role"> @@ -96,9 +97,17 @@ </div> </div> <div id="selectable_cards"></div> - <div id="hand_area" class="panel"> - <div id="hand_header" class="panel_header">Hand</div> - <div id="hand" class="panel_body"></div> + <div id="player_area" class="panel"> + <div id="player_area_header" class="panel_header"> + <div id="hand_tab" data-active="active" class="player_area_tab">Hand</div> + <div id="deck_tab" data-active="inactive" class="player_area_tab">Deck</div> + <div id="discard_tab" data-active="inactive" class="player_area_tab">Discard Pile</div> + <div id="trash_tab" data-active="inactive" class="player_area_tab">Trash</div> + </div> + <div id="hand" data-active="active" class="panel_body"></div> + <div id="deck" data-active="inactive" class="panel_body"></div> + <div id="discard" data-active="inactive" class="panel_body"></div> + <div id="trash" data-active="inactive" class="panel_body"></div> </div> <div id="player_areas"> <div id="player_area_Anarchist" class="panel"> @@ -9,6 +9,7 @@ const TRACK_COUNT = 5; const TRACK_LENGTH = 11; const FACTIONS = ['a', 'c', 'm']; const ui = { + bag_of_glory: document.getElementById('bag_of_glory'), map: document.getElementById('map'), medallions_container: document.getElementById('medallions'), markers: document.getElementById('markers'), @@ -36,7 +37,7 @@ const ui = { }, glory_container: document.getElementById('glory'), hand: document.getElementById('hand'), - hand_area: document.getElementById('hand_area'), + player_area: document.getElementById('player_area'), current_events: document.getElementById('current_events'), roles: { a: { @@ -59,6 +60,18 @@ const ui = { Moderate: document.getElementById('role_Moderate'), container: document.getElementById('roles'), }, + player_area_tabs: { + hand_tab: document.getElementById('hand_tab'), + deck_tab: document.getElementById('deck_tab'), + discard_tab: document.getElementById('discard_tab'), + trash_tab: document.getElementById('trash_tab'), + }, + player_area_cards: { + hand: document.getElementById('hand'), + deck: document.getElementById('deck'), + discard: document.getElementById('discard'), + trash: document.getElementById('trash'), + }, player_areas: { container: document.getElementById('player_areas'), Anarchist: document.getElementById('player_area_Anarchist'), @@ -194,6 +207,21 @@ function on_click_action(evt) { if (send_action(evt.target.my_action, evt.target.my_id)) evt.stopPropagation(); } +function on_click_tab(evt) { + evt.stopPropagation(); + const { id } = evt.target; + Object.entries(ui.player_area_tabs).forEach(([tab, elt]) => { + const cards_area = ui.player_area_cards[tab.split('_')[0]]; + if (tab === id) { + elt.setAttribute('data-active', 'active'); + cards_area.setAttribute('data-active', 'active'); + } + else { + elt.setAttribute('data-active', 'inactive'); + cards_area.setAttribute('data-active', 'inactive'); + } + }); +} function is_action(action, arg) { if (arg === undefined) return !!(view.actions && view.actions[action] === 1); @@ -206,13 +234,21 @@ function on_init() { if (on_init_once) return; on_init_once = true; + Object.values(ui.player_area_tabs).forEach((element) => { + element.addEventListener('click', on_click_tab); + }); for (const player of view.player_order) { ui.player_areas.container.insertAdjacentElement('beforeend', ui.player_areas[player]); ui.roles.container.insertAdjacentElement('beforeend', ui.roles[player]); } ui.roles.container.insertAdjacentElement('beforeend', ui.turn_info); if (view.current === 'Observer') { - ui.hand_area.style.display = 'none'; + ui.player_area.style.display = 'none'; + } + else { + document + .getElementById('player_area_header') + .setAttribute('data-faction-id', view.current_player_faction); } for (let t = 0; t < TRACK_COUNT; ++t) { for (let s = 0; s < TRACK_LENGTH; ++s) { @@ -289,6 +325,7 @@ function on_update() { for (let key of Object.keys(view.hero_points)) { ui.roles[key].hero_points.replaceChildren(`Hero Points: ${view.hero_points[key]}`); } + ui.bag_of_glory.replaceChildren(`Bag of Glory: ${view.bag_of_glory_count}`); ui.current_events.replaceChildren(); for (let i = 0; i < view.current_events.length; i++) { const cardId = view.current_events[i]; @@ -309,6 +346,13 @@ function on_update() { ui.bonuses[bonus_id].setAttribute('data-bonus-on', view.bonuses[bonus_id] + 0); } place_cards(ui.hand, view.hand); + ui.player_area_tabs.hand_tab.replaceChildren(`Hand (${view.hand.length})`); + place_cards(ui.player_area_cards.deck, view.deck); + ui.player_area_tabs.deck_tab.replaceChildren(`Deck (${view.deck.length})`); + place_cards(ui.player_area_cards.discard, view.discard); + ui.player_area_tabs.discard_tab.replaceChildren(`Discard (${view.discard.length})`); + place_cards(ui.player_area_cards.trash, view.trash); + ui.player_area_tabs.trash_tab.replaceChildren(`Trash (${view.trash.length})`); place_cards(ui.selectable_cards, view.selectable_cards); for (let faction_id of FACTIONS) { place_cards(ui.tableaus[faction_id], view.tableaus[faction_id]); @@ -349,6 +393,7 @@ function on_update() { ui.turn_info.style.display = ''; ui.turn_info_card.setAttribute('data-card-id', view.played_card + ''); } + Object.values(ui.glory).forEach((elt) => elt.removeAttribute('data-faction-id')); for (let g = 0; g < view.glory.length; ++g) { ui.glory[g].setAttribute('data-faction-id', view.glory[g]); } @@ -375,10 +420,12 @@ function on_update() { action_button('lose_hp', 'Lose Hero Points'); action_button('draw_card', 'Draw a card'); action_button('draw_cards', 'Draw cards'); - action_button('play_for_ap', 'Play card for Action Points'); + action_button('play_to_tableau', 'Play card to Tableau'); action_button('play_for_event', 'Play card for Event'); action_button('use_ap', 'Use Action Points'); action_button('use_morale_bonus', 'Use Morale Bonus'); + action_button('move_track', 'Move a Track'); + action_button('turn_on_bonus', 'Turn on a Bonus'); action_button('add_glory', 'Add to Bag of Glory'); action_button('up', 'Up'); action_button('down', 'Down'); @@ -389,9 +436,14 @@ function on_update() { action_button('no', 'No'); action_button('skip', 'Skip'); action_button('spend_hp', 'Spend Hero Points'); + action_button('use_momentum', 'Play second card (Momentum)'); action_button('done', 'Done'); + action_button('end_turn', 'End turn'); action_button('undo', 'Undo'); } +const IMG_FTA = '<span class="faction_token" data-faction-id="a"></span>'; +const IMG_FTC = '<span class="faction_token" data-faction-id="c"></span>'; +const IMG_FTM = '<span class="faction_token" data-faction-id="m"></span>'; function on_log(text) { let p = document.createElement('div'); if (text.match(/^>>/)) { @@ -402,9 +454,6 @@ function on_log(text) { text = text.substring(1); p.className = 'i'; } - text = text.replace(/&/g, '&'); - text = text.replace(/</g, '<'); - text = text.replace(/>/g, '>'); if (text.match(/^\.h1/)) { text = text.substring(4); p.className = 'h1'; @@ -425,6 +474,10 @@ function on_log(text) { text = text.substring(11); p.className = 'h2 fascist'; } + else if (text.match(/^\.h2\.glory/)) { + text = text.substring(9); + p.className = 'h2 glory'; + } else if (text.match(/^\.h2/)) { text = text.substring(4); p.className = 'h2'; @@ -433,6 +486,9 @@ function on_log(text) { text = text.substring(4); p.className = 'h3'; } + text = text.replace(/<fta>/g, IMG_FTA); + text = text.replace(/<ftc>/g, IMG_FTC); + text = text.replace(/<ftm>/g, IMG_FTM); p.innerHTML = text; return p; } @@ -22,6 +22,7 @@ const FACTIONS = ['a', 'c', 'm']; // const ROLES = ['Anarchist', 'Communist', 'Moderate']; const ui = { + bag_of_glory: document.getElementById('bag_of_glory'), map: document.getElementById('map'), medallions_container: document.getElementById('medallions'), markers: document.getElementById('markers'), @@ -49,7 +50,7 @@ const ui = { }, glory_container: document.getElementById('glory'), hand: document.getElementById('hand'), - hand_area: document.getElementById('hand_area'), + player_area: document.getElementById('player_area'), current_events: document.getElementById('current_events'), roles: { a: { @@ -72,6 +73,18 @@ const ui = { Moderate: document.getElementById('role_Moderate'), container: document.getElementById('roles'), }, + player_area_tabs: { + hand_tab: document.getElementById('hand_tab'), + deck_tab: document.getElementById('deck_tab'), + discard_tab: document.getElementById('discard_tab'), + trash_tab: document.getElementById('trash_tab'), + }, + player_area_cards: { + hand: document.getElementById('hand'), + deck: document.getElementById('deck'), + discard: document.getElementById('discard'), + trash: document.getElementById('trash'), + }, player_areas: { container: document.getElementById('player_areas'), Anarchist: document.getElementById('player_area_Anarchist'), @@ -244,6 +257,22 @@ function on_click_action(evt) { evt.stopPropagation(); } +function on_click_tab(evt) { + evt.stopPropagation(); + const { id } = evt.target as HTMLElement; + + Object.entries(ui.player_area_tabs).forEach(([tab, elt]) => { + const cards_area = ui.player_area_cards[tab.split('_')[0]]; + if (tab === id) { + elt.setAttribute('data-active', 'active'); + cards_area.setAttribute('data-active', 'active'); + } else { + elt.setAttribute('data-active', 'inactive'); + cards_area.setAttribute('data-active', 'inactive'); + } + }); +} + function is_action(action, arg) { if (arg === undefined) return !!(view.actions && view.actions[action] === 1); return !!( @@ -259,15 +288,26 @@ function on_init() { if (on_init_once) return; on_init_once = true; + Object.values(ui.player_area_tabs).forEach((element) => { + element.addEventListener('click', on_click_tab); + }); + // Reorder tableaus and roles based on player order for (const player of view.player_order) { - ui.player_areas.container.insertAdjacentElement('beforeend', ui.player_areas[player]); + ui.player_areas.container.insertAdjacentElement( + 'beforeend', + ui.player_areas[player] + ); ui.roles.container.insertAdjacentElement('beforeend', ui.roles[player]); } ui.roles.container.insertAdjacentElement('beforeend', ui.turn_info); if (view.current === 'Observer') { - ui.hand_area.style.display = 'none'; + ui.player_area.style.display = 'none'; + } else { + document + .getElementById('player_area_header') + .setAttribute('data-faction-id', view.current_player_faction); } // Create blank_markers @@ -346,7 +386,7 @@ function on_init() { }); } -function place_cards(e: HTMLElement, cards: CardId[]) { +function place_cards(e: HTMLElement, cards: number[]) { e.replaceChildren(); for (let c of cards) { ui.cards[c].classList.remove('selected'); @@ -367,6 +407,7 @@ function on_update() { `Hero Points: ${view.hero_points[key]}` ); } + ui.bag_of_glory.replaceChildren(`Bag of Glory: ${view.bag_of_glory_count}`); // for (let s = 0; s < SPACE_COUNT; ++s) ui.spaces[s].replaceChildren(); @@ -400,6 +441,14 @@ function on_update() { } place_cards(ui.hand, view.hand); + ui.player_area_tabs.hand_tab.replaceChildren(`Hand (${view.hand.length})`); + place_cards(ui.player_area_cards.deck, view.deck); + ui.player_area_tabs.deck_tab.replaceChildren(`Deck (${view.deck.length})`); + place_cards(ui.player_area_cards.discard, view.discard); + ui.player_area_tabs.discard_tab.replaceChildren(`Discard (${view.discard.length})`); + place_cards(ui.player_area_cards.trash, view.trash); + ui.player_area_tabs.trash_tab.replaceChildren(`Trash (${view.trash.length})`); + place_cards(ui.selectable_cards, view.selectable_cards); for (let faction_id of FACTIONS) { @@ -451,6 +500,7 @@ function on_update() { ui.turn_info_card.setAttribute('data-card-id', view.played_card + ''); } + Object.values(ui.glory).forEach((elt: HTMLElement) => elt.removeAttribute('data-faction-id')); for (let g = 0; g < view.glory.length; ++g) { ui.glory[g].setAttribute('data-faction-id', view.glory[g]); } @@ -485,11 +535,14 @@ function on_update() { action_button('draw_card', 'Draw a card'); action_button('draw_cards', 'Draw cards'); // action_button('draw_card', 'Draw card'); - action_button('play_for_ap', 'Play card for Action Points'); + action_button('play_to_tableau', 'Play card to Tableau'); action_button('play_for_event', 'Play card for Event'); action_button('use_ap', 'Use Action Points'); action_button('use_morale_bonus', 'Use Morale Bonus'); + action_button('move_track', 'Move a Track'); + action_button('turn_on_bonus', 'Turn on a Bonus'); + action_button('add_glory', 'Add to Bag of Glory'); action_button('up', 'Up'); action_button('down', 'Down'); @@ -500,10 +553,16 @@ function on_update() { action_button('no', 'No'); action_button('skip', 'Skip'); action_button('spend_hp', 'Spend Hero Points'); + action_button('use_momentum', 'Play second card (Momentum)'); action_button('done', 'Done'); + action_button('end_turn', 'End turn'); action_button('undo', 'Undo'); } +const IMG_FTA = '<span class="faction_token" data-faction-id="a"></span>'; +const IMG_FTC = '<span class="faction_token" data-faction-id="c"></span>'; +const IMG_FTM = '<span class="faction_token" data-faction-id="m"></span>'; + // @ts-ignore function on_log(text) { let p = document.createElement('div'); @@ -518,9 +577,9 @@ function on_log(text) { p.className = 'i'; } - text = text.replace(/&/g, '&'); - text = text.replace(/</g, '<'); - text = text.replace(/>/g, '>'); + // text = text.replace(/&/g, '&'); + // text = text.replace(/</g, '<'); + // text = text.replace(/>/g, '>'); // text = text.replace(/C(\d+)/g, sub_card_name) // text = text.replace(/S(\d+)/g, sub_space_name) // text = text.replace(/U(\d+)/g, sub_unit_name) @@ -543,6 +602,9 @@ function on_log(text) { } else if (text.match(/^\.h2\.fascist/)) { text = text.substring(11); p.className = 'h2 fascist'; + } else if (text.match(/^\.h2\.glory/)) { + text = text.substring(9); + p.className = 'h2 glory'; } else if (text.match(/^\.h2/)) { text = text.substring(4); p.className = 'h2'; @@ -551,6 +613,10 @@ function on_log(text) { p.className = 'h3'; } + text = text.replace(/<fta>/g, IMG_FTA) + text = text.replace(/<ftc>/g, IMG_FTC) + text = text.replace(/<ftm>/g, IMG_FTM) + p.innerHTML = text; return p; } @@ -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')); } @@ -687,6 +711,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 +728,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 +749,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 +908,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 +927,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 +963,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 +1017,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); @@ -1133,6 +1205,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); @@ -1249,41 +1324,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) { @@ -1292,21 +1397,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, @@ -1317,7 +1422,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() { @@ -1331,6 +1441,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, @@ -1394,7 +1512,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 }); @@ -1453,31 +1571,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; } @@ -1504,13 +1651,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(); @@ -1527,6 +1685,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; @@ -1545,6 +1706,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', @@ -1616,11 +1783,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(); @@ -1634,6 +1808,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', @@ -1783,12 +1960,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); } @@ -1881,19 +2058,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; @@ -1906,6 +2091,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)) { @@ -1954,13 +2147,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; @@ -1989,7 +2183,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); } @@ -2093,7 +2287,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); } @@ -2216,10 +2410,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') { @@ -2228,6 +2423,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]; @@ -2245,9 +2441,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))); }, }, { @@ -2291,17 +2485,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); }, @@ -2310,8 +2500,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 }), ]); }, }, @@ -2396,6 +2586,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]; } @@ -2470,6 +2666,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 = []; @@ -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 = []; @@ -79,17 +79,23 @@ export interface View { log: number | string[]; active?: string | null; prompt: string | null; + state: Game['state']; actions?: any; victory?: string; current: Player | 'Observer'; - selected_cards: CardId[]; - bag_of_glory: Game['bag_of_glory']; + current_player_faction: FactionId | null; + selected_cards: number[]; + bag_of_glory: Game['bag_of_glory']; // TODO: remove + bag_of_glory_count: number; bonuses: Game['bonuses']; current_events: CardId[]; first_player: Game['first_player']; fronts: Game['fronts']; glory: Game['glory']; - hand: CardId[]; + hand: number[]; + discard: number[]; + deck: number[]; + trash: number[]; hero_points: Game['hero_points']; initiative: Game['initiative']; medallions: Game['medallions']; @@ -121,11 +127,11 @@ export interface SeqNode { c: EngineNode[]; } -export interface LeafNode { +export interface LeafNode<T = any> { t: 'l'; s: string; // State p: FactionId | 'None'; // Player - a?: any; // args + a?: T; // args r?: 0 | 1; // 1 if resolved } @@ -171,6 +177,8 @@ export interface PlayerCard extends CardBase { icons: Icon[]; } +export type EffectSource = 'fascist_event' | 'fascist_test' | 'track_icon' | 'momentum'; + export interface Effect { type: | 'attack' @@ -211,3 +219,47 @@ export interface StaticData { triggers: Array<null | Effect>; }>; } + +// #region engine node args + +export interface EngineNodeArgsBase { + /** + * If set, node was added to engine as result of given effect. + * Used for prompts + */ + src?: EffectSource; +} + +export interface ChooseCardArgs extends EngineNodeArgsBase { +} + +export interface PlayerTurnArgs extends EngineNodeArgsBase { + /** + * When set to true, player can use current card + * for action points (using for ap sets it to false) + */ + use_ap?: boolean; + /** + * When set to true, player can use current card for + * morale bonus if that is active (using morale bonus sets it to dalse) + */ + use_morale_bonus?: boolean; + /** + * Set when starting to resolve event effect from card. Will be set to false + * after all effects have been resolved. Used to determine if momentum medallion + * can be used. + */ + resolving_event?: boolean; + /** + * Strength of the current card + */ + strength?: number; + /** + * Set to true if player got momentum medallion, + * but was not able to resolve directly as they were still + * resolving their current card + */ + use_momentum?: boolean; +} + +// #endregion
\ No newline at end of file |