diff options
author | Frans Bongers <fransbongers@macbookpro.home> | 2025-03-21 21:34:05 +0100 |
---|---|---|
committer | Frans Bongers <fransbongers@macbookpro.home> | 2025-03-21 21:34:05 +0100 |
commit | 8f5f3f54a610801a1d739636b49343a122f7113a (patch) | |
tree | bebc0159ea037d3ba7af600d537ff2d5ccdb1750 | |
parent | 9f01f50ce897eda24625d292402b6a731c28e1f6 (diff) | |
download | land-and-freedom-8f5f3f54a610801a1d739636b49343a122f7113a.tar.gz |
momentum refactor
-rw-r--r-- | data.js | 6 | ||||
-rw-r--r-- | data.ts | 5 | ||||
-rw-r--r-- | rules.js | 94 | ||||
-rw-r--r-- | rules.ts | 122 | ||||
-rw-r--r-- | types.d.ts | 5 |
5 files changed, 108 insertions, 124 deletions
@@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.LIBERTY_OR_COLLECTIVIZATION = exports.COMMUNIST_EXTRA_HERO_POINT = exports.ANARCHIST_EXTRA_HERO_POINT = exports.NORTHERN = exports.SOUTHERN = exports.ARAGON = exports.MADRID = exports.ORGANIZATION_MEDALLION_ID = exports.ARCHIVES_MEDALLION_ID = exports.VOLUNTEERS_MEDALLION_ID = exports.INTELLIGENCE_MEDALLION_ID = exports.PROPAGANDA_MEDALLION_ID = exports.STRATEGY_MEDALLION_ID = exports.FRONTS = exports.DEFEAT = exports.VICTORY = exports.TRASH = exports.TOWARDS_CENTER = exports.AWAY_FROM_CENTER = exports.SELF = exports.PLAYER_WITH_MOST_HERO_POINTS = exports.OTHER_PLAYERS = exports.ON = exports.OFF = exports.TEAMWORK_BONUS = exports.MORALE_BONUS = exports.FOREIGN_AID = exports.SOVIET_SUPPORT = exports.INITIATIVE_PLAYER = exports.GOVERNMENT = exports.COLLECTIVIZATION = exports.CLOSEST_TO_VICTORY = exports.CLOSEST_TO_DEFEAT = exports.LIBERTY = exports.ANY = exports.MODERATES_ID = exports.FASCIST_ID = exports.COMMUNISTS_ID = exports.ANARCHISTS_ID = exports.MODERATE = exports.COMMUNIST = exports.ANARCHIST = exports.ALL_PLAYERS = void 0; +exports.MOMENTUM = exports.LIBERTY_OR_COLLECTIVIZATION = exports.COMMUNIST_EXTRA_HERO_POINT = exports.ANARCHIST_EXTRA_HERO_POINT = exports.NORTHERN = exports.SOUTHERN = exports.ARAGON = exports.MADRID = exports.ORGANIZATION_MEDALLION_ID = exports.ARCHIVES_MEDALLION_ID = exports.VOLUNTEERS_MEDALLION_ID = exports.INTELLIGENCE_MEDALLION_ID = exports.PROPAGANDA_MEDALLION_ID = exports.STRATEGY_MEDALLION_ID = exports.FRONTS = exports.DEFEAT = exports.VICTORY = exports.TRASH = exports.TOWARDS_CENTER = exports.AWAY_FROM_CENTER = exports.SELF = exports.PLAYER_WITH_MOST_HERO_POINTS = exports.OTHER_PLAYERS = exports.ON = exports.OFF = exports.TEAMWORK_BONUS = exports.MORALE_BONUS = exports.FOREIGN_AID = exports.SOVIET_SUPPORT = exports.INITIATIVE_PLAYER = exports.GOVERNMENT = exports.COLLECTIVIZATION = exports.CLOSEST_TO_VICTORY = exports.CLOSEST_TO_DEFEAT = exports.LIBERTY = exports.ANY = exports.MODERATES_ID = exports.FASCIST_ID = exports.COMMUNISTS_ID = exports.ANARCHISTS_ID = exports.MODERATE = exports.COMMUNIST = exports.ANARCHIST = exports.ALL_PLAYERS = void 0; exports.create_effect = create_effect; const LIBERTY = 0; exports.LIBERTY = LIBERTY; @@ -22,7 +22,7 @@ const OFF = 0; exports.OFF = OFF; const ON = 1; exports.ON = ON; -const PLAYER_WITH_MOST_HERO_POINTS = 0; +const PLAYER_WITH_MOST_HERO_POINTS = 6; exports.PLAYER_WITH_MOST_HERO_POINTS = PLAYER_WITH_MOST_HERO_POINTS; const INITIATIVE_PLAYER = 'i'; exports.INITIATIVE_PLAYER = INITIATIVE_PLAYER; @@ -84,6 +84,8 @@ const ARCHIVES_MEDALLION_ID = 7; exports.ARCHIVES_MEDALLION_ID = ARCHIVES_MEDALLION_ID; const ORGANIZATION_MEDALLION_ID = 8; exports.ORGANIZATION_MEDALLION_ID = ORGANIZATION_MEDALLION_ID; +const MOMENTUM = 'momentum'; +exports.MOMENTUM = MOMENTUM; const ANARCHIST_EXTRA_HERO_POINT = 0; exports.ANARCHIST_EXTRA_HERO_POINT = ANARCHIST_EXTRA_HERO_POINT; const COMMUNIST_EXTRA_HERO_POINT = 1; @@ -13,7 +13,7 @@ const TEAMWORK_BONUS = 1; const OFF = 0; const ON = 1; -const PLAYER_WITH_MOST_HERO_POINTS = 0; +const PLAYER_WITH_MOST_HERO_POINTS = 6; // Should not overlap with factionId const INITIATIVE_PLAYER = 'i'; const ALL_PLAYERS = 'all'; @@ -53,6 +53,8 @@ const VOLUNTEERS_MEDALLION_ID = 6; const ARCHIVES_MEDALLION_ID = 7; const ORGANIZATION_MEDALLION_ID = 8; +const MOMENTUM = 'momentum'; + // Abilities granted by cards const ANARCHIST_EXTRA_HERO_POINT = 0; const COMMUNIST_EXTRA_HERO_POINT = 1; @@ -116,6 +118,7 @@ export { ANARCHIST_EXTRA_HERO_POINT, COMMUNIST_EXTRA_HERO_POINT, LIBERTY_OR_COLLECTIVIZATION, + MOMENTUM, }; const data: StaticData = { @@ -146,19 +146,18 @@ function setup_final_bid() { game.engine.push(create_function_node('setup_choose_card')); next(); } -function setup_player_turn() { +function setup_player_turn(faction_id) { game.fascist = 0; game.card_played = 0; const next_faction = game.first_player === null ? get_player_order()[0] - : get_next_faction_in_player_order(get_active_faction()); + : faction_id; if (game.first_player === null) { game.first_player = next_faction; } game.engine = [ create_function_node('start_of_player_turn', { f: next_faction }), create_state_node('player_turn', next_faction), - create_function_node('end_of_player_turn', { f: next_faction }), ]; next(); } @@ -172,7 +171,8 @@ function check_end_of_year_discard() { } function end_of_player_turn() { const { f: faction } = get_active_node_args(); - if (get_next_faction_in_player_order(faction) === game.first_player) { + const next_faction = get_next_faction_in_player_order(faction); + if (next_faction === game.first_player) { game.engine = [ create_state_node('change_active_player', game.initiative), create_function_node('resolve_fascist_test'), @@ -180,7 +180,7 @@ function end_of_player_turn() { ]; } else { - game.engine = [create_function_node('setup_player_turn')]; + game.engine = [create_function_node('setup_player_turn', next_faction)]; } next(); } @@ -443,6 +443,7 @@ function setup(seed, _scenario, options) { hero_points: [2, 2, 0, 14], initiative: data_1.MODERATES_ID, medallions: [[], [], [], []], + momentum: null, played_card: null, player_order: [data_1.MODERATE], selected_cards: [ @@ -972,7 +973,7 @@ states.play_card = { }, card(c) { const faction = get_active_faction(); - game.selected_cards[faction] = [c]; + game.selected_cards[faction].push(c); game.card_played = 0; game.played_card = game.selected_cards[faction][0]; resolve_active_and_proceed(); @@ -1047,26 +1048,14 @@ states.choose_final_bid = { }; 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')); + game.momentum = faction; + const momentum_nodes = resolve_effect((0, data_1.create_effect)('play_card', faction, 1), data_1.MOMENTUM); + if (game.faction_turn !== null) { + game.engine.push(momentum_nodes); return; } - const node = get_nodes_for_state('player_turn')[0]; - const player_needs_to_play_card = !game.card_played; - 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_after_active_node(create_state_node('play_card', faction, { - src: 'momentum', - })); + insert_after_active_node(momentum_nodes); } } states.choose_medallion = { @@ -1519,10 +1508,11 @@ states.player_turn = { prompt() { gen_spend_hero_points(); const faction_id = get_active_faction(); - let { use_ap, use_morale_bonus, use_momentum } = get_active_node_args(); + let { use_ap, use_morale_bonus, src } = 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.card_played; + const use_momentum = game.momentum === faction_id && src !== data_1.MOMENTUM; if (use_momentum) { gen_action('use_momentum'); if (use_ap || use_morale_bonus || can_play_card) { @@ -1554,10 +1544,16 @@ states.player_turn = { resolve_spend_hp(); }, end_turn() { - if (game.faction_turn === get_active_faction()) { + const { src } = get_active_node_args(); + const faction_id = get_active_faction(); + if (game.faction_turn === faction_id) { game.faction_turn = null; game.played_card = null; - game.selected_cards[get_active_faction()] = []; + game.engine.push(create_function_node('end_of_player_turn', { f: faction_id })); + } + game.selected_cards[faction_id].pop(); + if (src === data_1.MOMENTUM) { + game.momentum = null; } resolve_active_and_proceed(true); }, @@ -1599,18 +1595,10 @@ states.player_turn = { next(); }, use_momentum() { - log("Momentum:"); - const faction = get_active_faction(); - game.card_played = 0; - game.selected_cards[faction] = []; - update_active_node_args({ - use_morale_bonus: false, - use_momentum: false, - }); - insert_before_active_node(create_state_node('play_card', faction, { - src: 'momentum', - })); - next(); + const faction_id = get_active_faction(); + game.selected_cards[faction_id].pop(); + game.momentum = null; + resolve_active_and_proceed(); }, use_morale_bonus() { log(`Morale Bonus:`); @@ -1864,6 +1852,7 @@ states.swap_card_tableau_hand = { const selected_cards = game.selected_cards[faction]; const hand = game.hands[faction]; const tableau = game.tableaus[faction]; + const { s: selected_at_start } = get_active_node_args(); gen_action('skip'); if (tableau.length === 0) { view.prompt = 'No card in your tableau to swap.'; @@ -1873,14 +1862,14 @@ states.swap_card_tableau_hand = { view.prompt = 'No card in your hand to swap.'; return; } - if (selected_cards.length === 1) { + if (selected_cards.length === selected_at_start) { for (const c of hand) { - if (selected_cards[0] !== c) { + if (!selected_cards.includes(c)) { gen_action_card(c); } } } - if (selected_cards.length === 2) { + if (selected_cards.length === selected_at_start + 1) { for (const c of tableau) { gen_action_card(c); } @@ -1893,14 +1882,15 @@ states.swap_card_tableau_hand = { const faction = get_active_faction(); const selected_cards = game.selected_cards[faction]; selected_cards.push(c); - if (selected_cards.length === 3) { + const { s: selected_at_start } = get_active_node_args(); + if (selected_cards.length === selected_at_start + 2) { const hand = game.hands[faction]; const tableau = game.tableaus[faction]; - array_remove_item(hand, selected_cards[1]); - array_remove_item(tableau, selected_cards[2]); - hand.push(selected_cards[2]); - tableau.push(selected_cards[1]); - game.selected_cards[faction].length = 1; + array_remove_item(hand, selected_cards[selected_at_start + 1]); + array_remove_item(tableau, selected_cards[selected_at_start + 2]); + hand.push(selected_cards[selected_at_start + 2]); + tableau.push(selected_cards[selected_at_start + 1]); + game.selected_cards[faction].length = selected_at_start; resolve_active_and_proceed(); } }, @@ -2588,7 +2578,7 @@ function create_effects_node(effects, source) { return create_seq_node(nodes); } function get_faction_to_resolve_effect(effect) { - if (!effect.faction) { + if (effect.faction === undefined || effect.faction === null) { return get_active_faction(); } if (effect.faction === data_1.INITIATIVE_PLAYER) { @@ -2624,6 +2614,12 @@ function resolve_effect(effect, source) { src: source, }); } + if (effect.type === 'swap_card_tableau_hand') { + return create_state_node('swap_card_tableau_hand', faction, { + ...args, + s: game.selected_cards[get_active_faction()].length + }); + } let state = effect_type_state_map[effect.type]; if (state !== undefined) { return create_state_node(state, faction, args); @@ -2911,7 +2907,7 @@ function get_source_name(source) { case 'tr4': return tracks[4].name + ' Trigger'; case 'track_icon': return 'Track Trigger'; - case 'momentum': + case data_1.MOMENTUM: return 'Momentum'; } return source; @@ -68,6 +68,7 @@ import data, { ARAGON, FASCIST_ID, NORTHERN, + MOMENTUM, } from './data'; const OBSERVER = 'Observer'; @@ -263,14 +264,14 @@ function setup_final_bid() { next(); } -function setup_player_turn() { +function setup_player_turn(faction_id: FactionId) { game.fascist = 0; game.card_played = 0; const next_faction = game.first_player === null ? get_player_order()[0] - : get_next_faction_in_player_order(get_active_faction()); + : faction_id; if (game.first_player === null) { game.first_player = next_faction; @@ -279,7 +280,6 @@ function setup_player_turn() { game.engine = [ create_function_node('start_of_player_turn', { f: next_faction }), create_state_node('player_turn', next_faction), - create_function_node('end_of_player_turn', { f: next_faction }), ]; next(); @@ -301,14 +301,15 @@ function check_end_of_year_discard() { function end_of_player_turn() { const { f: faction } = get_active_node_args(); - if (get_next_faction_in_player_order(faction) === game.first_player) { + const next_faction = get_next_faction_in_player_order(faction); + if (next_faction === game.first_player) { game.engine = [ create_state_node('change_active_player', game.initiative), create_function_node('resolve_fascist_test'), create_function_node('setup_bag_of_glory'), ]; } else { - game.engine = [create_function_node('setup_player_turn')]; + game.engine = [create_function_node('setup_player_turn', next_faction)]; } next(); } @@ -629,6 +630,7 @@ export function setup(seed: number, _scenario: string, options: Record<string,bo hero_points: [2, 2, 0, 14], initiative: MODERATES_ID, medallions: [[],[],[],[]], + momentum: null, played_card: null, player_order: [MODERATE], selected_cards: [ @@ -1228,7 +1230,7 @@ states.play_card = { }, card(c: CardId) { const faction = get_active_faction(); - game.selected_cards[faction] = [c]; + game.selected_cards[faction].push(c); game.card_played = 0; // NOTE: I don't think we are using game.played_card in the UI at the moment? @@ -1314,43 +1316,17 @@ 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: StateNode<PlayerTurnArgs> = get_nodes_for_state('player_turn')[0]; - - const player_needs_to_play_card = !game.card_played; - const { use_ap, use_morale_bonus, resolving_event } = - node.a ?? ({} as PlayerTurnArgs); + game.momentum = faction; - 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, - }; + const momentum_nodes = resolve_effect(create_effect('play_card', faction, 1), MOMENTUM); + if (game.faction_turn !== null) { + // Player received medallion during a player turn, either their own or another players. + // Push to end of the engine so it's resolved after current turn ends + game.engine.push(momentum_nodes); + return; } else { - // Player can resolve choosing a new card - insert_after_active_node( - create_state_node<PlayCardArgs>('play_card', faction, { - src: 'momentum', - }) - ); + // Insert right after current node + insert_after_active_node(momentum_nodes) } } @@ -1863,7 +1839,7 @@ states.player_turn = { prompt() { gen_spend_hero_points(); const faction_id = get_active_faction(); - let { use_ap, use_morale_bonus, use_momentum } = + let { use_ap, use_morale_bonus, src } = get_active_node_args<PlayerTurnArgs>(); use_morale_bonus = use_morale_bonus && game.bonuses[MORALE_BONUS] === ON; @@ -1872,6 +1848,7 @@ states.player_turn = { game.faction_turn === faction_id && game.hero_points[faction_id] > 0; const can_play_card = !game.card_played; + const use_momentum = game.momentum === faction_id && src !== MOMENTUM; if (use_momentum) { gen_action('use_momentum'); if (use_ap || use_morale_bonus || can_play_card) { @@ -1905,10 +1882,16 @@ states.player_turn = { resolve_spend_hp(); }, end_turn() { - if (game.faction_turn === get_active_faction()) { + const {src} = get_active_node_args(); + const faction_id = get_active_faction(); + if (game.faction_turn === faction_id) { game.faction_turn = null; game.played_card = null; - game.selected_cards[get_active_faction()] = []; + game.engine.push(create_function_node('end_of_player_turn', { f: faction_id })); + } + game.selected_cards[faction_id].pop(); + if (src === MOMENTUM) { + game.momentum = null; } resolve_active_and_proceed(true); }, @@ -1954,23 +1937,10 @@ states.player_turn = { next(); }, use_momentum() { - log("Momentum:"); - const faction = get_active_faction(); - // 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. - game.card_played = 0; - game.selected_cards[faction] = []; - update_active_node_args<PlayerTurnArgs>({ - use_morale_bonus: false, - use_momentum: false, - }); - insert_before_active_node( - create_state_node<PlayCardArgs>('play_card', faction, { - src: 'momentum', - }) - ) - next(); + const faction_id = get_active_faction(); + game.selected_cards[faction_id].pop(); + game.momentum = null; + resolve_active_and_proceed(); }, use_morale_bonus() { log(`Morale Bonus:`) @@ -2280,6 +2250,7 @@ states.swap_card_tableau_hand = { const selected_cards = game.selected_cards[faction]; const hand = game.hands[faction]; const tableau = game.tableaus[faction]; + const { s: selected_at_start } = get_active_node_args(); gen_action('skip'); if (tableau.length === 0) { view.prompt = 'No card in your tableau to swap.'; @@ -2290,15 +2261,15 @@ states.swap_card_tableau_hand = { view.prompt = 'No card in your hand to swap.'; return; } - if (selected_cards.length === 1) { + if (selected_cards.length === selected_at_start) { for (const c of hand) { // do not swap currently executing card! - if (selected_cards[0] !== c) { + if (!selected_cards.includes(c)) { gen_action_card(c); } } } - if (selected_cards.length === 2) { + if (selected_cards.length === selected_at_start + 1) { for (const c of tableau) { gen_action_card(c); } @@ -2311,14 +2282,15 @@ states.swap_card_tableau_hand = { const faction = get_active_faction(); const selected_cards = game.selected_cards[faction]; selected_cards.push(c); - if (selected_cards.length === 3) { + const { s: selected_at_start } = get_active_node_args(); + if (selected_cards.length === selected_at_start + 2) { const hand = game.hands[faction]; const tableau = game.tableaus[faction]; - array_remove_item(hand, selected_cards[1]); - array_remove_item(tableau, selected_cards[2]); - hand.push(selected_cards[2]); - tableau.push(selected_cards[1]); - game.selected_cards[faction].length = 1; + array_remove_item(hand, selected_cards[selected_at_start + 1]); + array_remove_item(tableau, selected_cards[selected_at_start + 2]); + hand.push(selected_cards[selected_at_start + 2]); + tableau.push(selected_cards[selected_at_start + 1]); + game.selected_cards[faction].length = selected_at_start; resolve_active_and_proceed(); } }, @@ -3223,7 +3195,7 @@ function create_effects_node( } function get_faction_to_resolve_effect(effect: Effect): FactionId { - if (!effect.faction) { + if (effect.faction === undefined || effect.faction === null) { return get_active_faction(); } if (effect.faction === INITIATIVE_PLAYER) { @@ -3263,6 +3235,12 @@ function resolve_effect(effect: Effect, source?: EffectSource): EngineNode { src: source, }); } + if (effect.type === 'swap_card_tableau_hand') { + return create_state_node('swap_card_tableau_hand', faction, { + ...args, + s: game.selected_cards[get_active_faction()].length + }); + } // Default cases where effect type is mapped to a state let state = effect_type_state_map[effect.type]; @@ -3641,7 +3619,7 @@ function get_source_name(source: EffectSource): string { case 'tr4': return tracks[4].name + ' Trigger'; case 'track_icon': return 'Track Trigger'; - case 'momentum': + case MOMENTUM: return 'Momentum'; } return source; @@ -57,6 +57,11 @@ export interface Game { hero_points: number[]; initiative: FactionId; medallions: number[][]; + /** + * Set to FactionId if player has earned the Momentum medallion + * during the current turn + */ + momentum: FactionId | null; played_card: CardId | null; player_order: Player[]; selected_cards: CardId[][]; |