'use strict'; import { CardId, Effect, EngineNode, EventCard, FactionId, Front, FrontId, FunctionNode, Game, Icon, LeafNode, Player, PlayerCard, SeqNode, States, View, } from './types'; import data, { ANARCHIST, COMMUNIST, MODERATE, ANARCHISTS_ID, COMMUNISTS_ID, MODERATES_ID, // LIBERTY, // COLLECTIVIZATION, // GOVERNMENT, // SOVIET_SUPPORT, // FOREIGN_AID, // MORALE_BONUS, // TEAMWORK_BONUS, // OFF, LIBERTY, CLOSEST_TO_DEFEAT, CLOSEST_TO_VICTORY, COLLECTIVIZATION, GOVERNMENT, SOVIET_SUPPORT, FOREIGN_AID, ON, PLAYER_WITH_MOST_HERO_POINTS, SELF, ANY, TEAMWORK_BONUS, MORALE_BONUS, OFF, VICTORY, DEFEAT, FRONTS, create_effect, AWAY_FROM_CENTER, TOWARDS_CENTER, // StaticData, // PLAYER_WITH_MOST_HERO_POINTS, } from './data'; // interface State { // inactive: string; // prompt: () => void; // } const states = {} as States; let game = {} as Game; // = null var view = {} as View; // = null // export const ANARCHIST = 'Anarchist' as Player; // export const COMMUNIST = 'Communist' as Player; // export const MODERATE = 'Moderate' as Player; const role_ids = [ANARCHISTS_ID, COMMUNISTS_ID, MODERATES_ID]; const faction_player_map: Record = { [ANARCHISTS_ID]: ANARCHIST, [COMMUNISTS_ID]: COMMUNIST, [MODERATES_ID]: MODERATE, }; const player_faction_map: Record = { [ANARCHIST]: ANARCHISTS_ID, [COMMUNIST]: COMMUNISTS_ID, [MODERATE]: MODERATES_ID, }; const front_names: Record = { a: 'Aragon Front', m: 'Madrid Front', n: 'Northern Front', s: 'Southern Front', d: 'the Front closest to Defeat', v: 'the Front closest to Victory', }; const bonus_names: string[] = ['Morale Bonus', 'Teamwork Bonus']; const { cards, // fronts, tracks, } = data; const bonuses = [MORALE_BONUS, TEAMWORK_BONUS]; const faction_cards = { [ANARCHISTS_ID]: make_list(37, 54) as CardId[], [COMMUNISTS_ID]: make_list(19, 36) as CardId[], [MODERATES_ID]: make_list(1, 18) as CardId[], }; const medaillons = make_list(0, 8) as number[]; console.log('medaillons', medaillons); const fascist_decks = { 1: make_list(55, 72), 2: make_list(73, 90), 3: make_list(91, 108), }; export const scenarios = ['Standard']; export const roles: Player[] = [ANARCHIST, COMMUNIST, MODERATE]; function gen_action(action: string, argument?: number | string) { if (argument === undefined) { view.actions![action] = 1; } else { if (!(action in view.actions)) view.actions[action] = []; view.actions[action].push(argument); } } function gen_action_bonus(bonus_id: number) { gen_action('bonus', bonus_id); } function gen_action_card(card_id: CardId) { gen_action('card', card_id); } function gen_action_front(front_id: string) { gen_action('front', front_id); } function gen_action_standee(track_id: number) { gen_action('standee', track_id); } // function gen_action_space(space) { // gen_action('space', space); // } // function gen_action_piece(piece) { // gen_action('piece', piece); // } // function gen_action_card(card) { // gen_action('card', card); // } export function action( state: any, player: Player, action: string, arg: unknown ) { console.log('action', state, player, action, arg); game = state; let S = states[game.state]; if (action in S) S[action](arg, player); else if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo(); else throw new Error('Invalid action: ' + action); return game; } // #region ENGINE const leaf_node = 'l'; const seq_node = 's'; const function_node = 'f'; const resolved = 1; function create_leaf_node( state: string, faction: FactionId | 'None', args?: any ): LeafNode { return { t: leaf_node, s: state, p: faction, a: args, r: 0, }; } function create_function_node(func_name: string, args?: any): FunctionNode { return { t: function_node, f: func_name, a: args, r: 0, }; } function create_seq_node(children: EngineNode[]): SeqNode { return { t: seq_node, c: children, }; } function setup_bag_of_glory() { game.engine = [ create_leaf_node('add_glory', game.initiative), create_function_node('end_of_turn'), ]; next(); } function setup_choose_card() { const player_order = get_player_order(); game.engine = player_order.map((faction_id) => create_leaf_node('choose_card', faction_id) ); game.engine.push(create_function_node('setup_player_turn')); next(); } function setup_player_turn() { const player_order = get_player_order(); game.engine = player_order.map((faction_id) => create_seq_node([ create_function_node('start_of_player_turn', { f: faction_id }), create_leaf_node('player_turn', faction_id), ]) ); game.engine.push(create_function_node('resolve_fascist_test')); game.engine.push(create_function_node('setup_bag_of_glory')); next(); } function start_of_player_turn() { const args = get_active_node_args(); console.log('args', args); console.log('args'); const player = faction_player_map[args.f]; log_h2(player, player); resolve_active_and_proceed(); } const engine_functions: Record = { check_activate_icon, end_of_turn, end_of_year, setup_bag_of_glory, setup_choose_card, setup_player_turn, start_of_player_turn, resolve_fascist_test, }; function get_active( engine: EngineNode[] ): { parent: EngineNode[]; node: FunctionNode | LeafNode } | null { for (let i of engine) { if ((i.t === leaf_node || i.t === function_node) && i.r !== resolved) { return { parent: engine, node: i }; } if (i.t === seq_node) { const next_child = get_active(i.c); if (next_child !== null) { return next_child; } } } return null; } function get_active_node( engine: EngineNode[] = game.engine ): FunctionNode | LeafNode | null { const a = get_active(engine); return a === null ? null : a.node; } function get_active_node_args(): any { const node = get_active_node(game.engine); if (node.t === leaf_node || node.t === function_node) { return node.a; } return null; } function insert_before_or_after_active_node( node: EngineNode, position: 'before' | 'after', engine: EngineNode[] = game.engine ) { const a = get_active(engine); if (a === null) { return; } const i = a.parent.indexOf(a.node); console.log('insert_before_active_node', i); if (i >= 0) { array_insert(a.parent, i + (position == 'after' ? 1 : 0), node); } } function insert_after_active_node( node: EngineNode, engine: EngineNode[] = game.engine ) { insert_before_or_after_active_node(node, 'after', engine); } function insert_before_active_node( node: EngineNode, engine: EngineNode[] = game.engine ) { insert_before_or_after_active_node(node, 'before', engine); } function next() { const node = get_active_node(game.engine); if (node.t === function_node && engine_functions[node.f]) { const args = node.a; if (args) { engine_functions[node.f](args); } else { engine_functions[node.f](); } } else if (node.t === 'l') { game.state = node.s; game.active = faction_player_map[node.p]; } } function resolve_active_node() { const next_node = get_active_node(game.engine); console.log('resolve_active_node', next_node); if (next_node !== null) { next_node.r = resolved; } } function resolve_active_and_proceed() { resolve_active_node(); next(); } // #endregion // #region VIEW export { game_view as view }; function game_view(state: Game, player: Player) { game = state; const faction_id = player_faction_map[player]; view = { engine: game.engine, log: game.log, prompt: null, location: game.location, selected: game.selected, bag_of_glory: game.bag_of_glory, bonuses: game.bonuses, current_events: game.current_events, fronts: game.fronts, glory: game.glory, hand: game.hands[faction_id], hero_points: game.hero_points, initiative: game.initiative, medaillons: game.medaillons, selected_card: game.chosen_cards[faction_id], tableaus: game.tableaus, tracks: game.tracks, triggered_track_effects: game.triggered_track_effects, year: game.year, }; if (game.state === 'game_over') { view.prompt = game.victory; } else if (player !== game.active) { let inactive = states[game.state].inactive || game.state; view.prompt = `Waiting for ${game.active} to ${inactive}.`; } else { view.actions = {}; states[game.state].prompt(); if (game.undo && game.undo.length > 0) view.actions.undo = 1; else view.actions.undo = 0; } return view; } // #endregion // #region SETUP export function setup(seed: number, _scenario: string, _options: unknown) { // game.seed = seed; game = { seed: seed, state: null, active: ANARCHIST, bag_of_glory: [ANARCHISTS_ID, COMMUNISTS_ID, MODERATES_ID], blank_markers: [[], [], [], [], []], bonuses: [ON, ON], current_events: [], discard: { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], f: [], }, engine: [], fronts: { a: { value: -2, contributions: [], status: null, }, m: { value: -2, contributions: [], status: null, }, n: { value: -2, contributions: [], status: null, }, s: { value: -2, contributions: [], status: null, }, }, glory: [], hands: { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], }, hero_points: { [ANARCHISTS_ID]: 2, [COMMUNISTS_ID]: 2, [MODERATES_ID]: 0, pool: 14, }, chosen_cards: { [ANARCHISTS_ID]: null, [COMMUNISTS_ID]: null, [MODERATES_ID]: null, }, initiative: MODERATES_ID, medaillons: [ draw_item(medaillons), draw_item(medaillons), draw_item(medaillons), draw_item(medaillons), draw_item(medaillons), ], tableaus: { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], }, tracks: [5, 5, 6, 3, 3], trash: { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], }, triggered_track_effects: [[], [], [], [], []], log: [], undo: [], turn: 1, year: 1, state_data: null, }; start_year(); return game; } function draw_hand_cards(faction_id: FactionId, count: number) { const log = count === 1 ? `${get_player(faction_id)} draws 1 card` : `${get_player(faction_id)} draws ${count} cards`; logi(log); for (let i = 0; i < count; i++) { const deck = list_deck(faction_id); game.hands[faction_id].push(draw_card(deck)); } } // #endregion function start_year() { // log_h1('Year ' + game.year); game.current_events = []; role_ids.forEach((role) => { draw_hand_cards(role, 5); }); start_turn(); } function start_turn() { log_h1(`Year ${game.year} - Turn ${game.turn}`); const cardId = draw_card(list_deck('fascist')); game.current_events.push(cardId); const card = cards[cardId] as EventCard; log_h2('Fascist Event', 'fascist'); log(card.title); game.engine = card.effects.map((effect) => resolve_effect(effect, game.initiative) ); game.engine.push({ t: 'f', f: 'setup_choose_card', }); next(); // game.state = 'resolve_event'; // game.active = faction_player_map[game.initiative]; // game.state_data = { // current_effect: 0, // }; } // region STATES states.activate_icon = { inactive: 'activate an icon', prompt() { view.prompt = 'Choose an icon to activate'; const c = cards[game.chosen_cards[get_active_faction_id()]] as PlayerCard; for (const i of c.icons) { gen_action(i); } }, add_to_front() { insert_after_active_node( create_leaf_node('add_to_front', get_active_faction(), { t: ANY, v: get_icon_count_in_tableau('add_to_front'), }) ); resolve_active_and_proceed(); }, collectivization() { move_track(COLLECTIVIZATION, get_icon_count_in_tableau('collectivization')); resolve_active_and_proceed(); }, d_collectivization() { move_track( COLLECTIVIZATION, -1 * get_icon_count_in_tableau('d_collectivization') ); resolve_active_and_proceed(); }, d_foreign_aid() { move_track(FOREIGN_AID, -1 * get_icon_count_in_tableau('d_foreign_aid')); resolve_active_and_proceed(); }, d_government() { move_track(GOVERNMENT, -1 * get_icon_count_in_tableau('d_government')); resolve_active_and_proceed(); }, d_liberty() { move_track(LIBERTY, -1 * get_icon_count_in_tableau('d_liberty')); resolve_active_and_proceed(); }, d_soviet_support() { move_track( SOVIET_SUPPORT, -1 * get_icon_count_in_tableau('d_soviet_support') ); resolve_active_and_proceed(); }, draw_card() { draw_hand_cards( get_active_faction(), get_icon_count_in_tableau('draw_card') ); resolve_active_and_proceed(); }, foreign_aid() { move_track(FOREIGN_AID, get_icon_count_in_tableau('foreign_aid')); resolve_active_and_proceed(); }, government() { const direction = game.active === COMMUNIST ? -1 : 1; move_track(GOVERNMENT, direction * get_icon_count_in_tableau('government')); resolve_active_and_proceed(); }, government_to_center() { const direction = game.tracks[GOVERNMENT] >= 6 ? -1 : 1; move_track( GOVERNMENT, direction * get_icon_count_in_tableau('government_to_center') ); resolve_active_and_proceed(); }, liberty() { move_track(LIBERTY, get_icon_count_in_tableau('liberty')); resolve_active_and_proceed(); }, soviet_support() { move_track(SOVIET_SUPPORT, get_icon_count_in_tableau('soviet_support')); resolve_active_and_proceed(); }, teamwork_on() { game.bonuses[TEAMWORK_BONUS] = ON; resolve_active_and_proceed(); }, }; states.add_glory = { inactive: 'add tokens to the Bag of Glory', prompt() { view.prompt = 'Add tokens to the Bag of Glory'; gen_action('add_glory'); }, add_glory() { console.log('add_glory'); let number = 1; if (game.turn === 4) { number++; } game.bag_of_glory.push(get_active_faction()); if (number === 1) { log_h3(`${game.active} adds 1 token to the Bag of Glory`); } else { log_h3(`${game.active} adds ${number} tokens to the Bag of Glory`); } resolve_active_and_proceed(); }, }; states.add_to_front = { inactive: 'add strength to a Front', prompt() { const args = get_active_node_args(); const possible_fronts = get_fronts_to_add_to(args.t); view.prompt = possible_fronts.length === 1 ? `Add strength to ${front_names[possible_fronts[0]]}` : 'Add strength to a Front'; for (let f of possible_fronts) { gen_action_front(f); } }, front(f: FrontId) { const value = get_active_node_args().v; update_front(f, value, get_active_faction()); resolve_active_and_proceed(); }, }; states.attack_front = { inactive: 'attack a Front', prompt() { const node = get_active_node(); const front = node.a.t; let targets: Array = []; if (front === 'd' || front === 'v') { targets = get_fronts_closest_to(front); } else if (game.fronts[front].status === DEFEAT) { targets = get_fronts_closest_to('d'); } else if (game.fronts[front].status === VICTORY) { targets = get_fronts_to_add_to(ANY); } else { targets.push(front); } view.prompt = targets.length === 1 ? `Attack ${front_names[targets[0]]}` : 'Attack a front'; targets.forEach((id) => gen_action('front', id)); }, front(f: FrontId) { const node = get_active_node(); const value = node.a.v; update_front(f, value); resolve_active_and_proceed(); }, }; states.choose_area_ap = { inactive: 'choose area to use Action Points', prompt() { view.prompt = 'Choose area of the board to affect'; for (const track of tracks) { gen_action_standee(track.id); } const fronts = get_fronts_to_add_to(ANY); for (const front of fronts) { gen_action_front(front); } for (const bonus of bonuses) { if (game.bonuses[bonus] === OFF) { gen_action_bonus(bonus); } } }, bonus(b: number) { update_bonus(b, ON); // TODO: insert action in case other bonus is OFF and AP left resolve_active_and_proceed(); }, front(f: FrontId) { const s: number = get_active_node_args().strength; update_front(f, s, get_active_faction_id()); resolve_active_and_proceed(); }, standee(track_id: number) { insert_after_active_node({ t: leaf_node, p: get_active_faction_id(), s: 'move_track_up_or_down', a: { track_id, strength: get_active_node()?.a.strength, }, }); resolve_active_and_proceed(); }, }; states.change_bonus = { inactive: 'gain select Bonus', prompt() { const args = get_active_node_args(); if ( (args.v === ON && game.bonuses[TEAMWORK_BONUS] === ON && game.bonuses[MORALE_BONUS] === ON) || (args.v === OFF && game.bonuses[args.t] === OFF) ) { gen_action('skip'); } if (args.t === ANY && args.v === ON) { view.prompt = 'Turn on a Bonus'; for (const bonus of bonuses) { if (game.bonuses[bonus] === OFF) { gen_action_bonus(bonus); } } } else if (args.v === OFF) { view.prompt = `Turn off ${bonus_names[args.t]}`; gen_action_bonus(args.t); } }, bonus(b: number) { const value = get_active_node_args().v; update_bonus(b, value); resolve_active_and_proceed(); }, skip() { resolve_active_and_proceed(); }, }; states.choose_card = { inactive: 'choose a card', prompt() { view.prompt = 'Choose a card to play this turn'; const hand = game.hands[player_faction_map[game.active]]; for (let c of hand) gen_action_card(c); }, card(c: CardId) { game.chosen_cards[player_faction_map[game.active]] = c; resolve_active_and_proceed(); }, }; states.gain_hero_points = { inactive: 'gain Hero Points', prompt() { const value = get_active_node_args().v; view.prompt = value > 1 ? `Gain ${value} Hero Points` : 'Gain 1 Hero Point'; gen_action('gain_hp'); }, gain_hp() { const value = get_active_node_args().v; gain_hero_points(get_active_faction(), value); resolve_active_and_proceed(); }, }; states.game_over = { get inactive() { return game.victory; }, prompt() { view.prompt = game.victory; }, }; states.lose_hero_points = { inactive: 'choose a Player', prompt() { const args = get_active_node_args(); view.prompt = 'Choose player to lose Hero Points'; if (args.t === PLAYER_WITH_MOST_HERO_POINTS) { const factions = get_factions_with_most_hero_poins(); console.log('faction', factions); for (let faction_id of factions) { gen_action(faction_player_map[faction_id]); } } }, Anarchist() { const value = get_active_node_args().v; lose_hero_point(ANARCHISTS_ID, value); resolve_active_and_proceed(); }, Communist() { const value = get_active_node_args().v; lose_hero_point(ANARCHISTS_ID, value); resolve_active_and_proceed(); }, Moderate() { const value = get_active_node_args().v; lose_hero_point(ANARCHISTS_ID, value); resolve_active_and_proceed(); }, }; states.move_track = { inactive: 'move a Track', prompt() { const node = get_active_node(); const track = node.a.t; const value = node.a.v; const name = tracks[track].name; view.prompt = `Move ${name} ${value > 0 ? 'up' : 'down'}`; if (track === GOVERNMENT && value === TOWARDS_CENTER) { view.prompt = `Move ${name} towards center`; } else if (track === GOVERNMENT && value === AWAY_FROM_CENTER) { view.prompt = `Move ${name} away from center`; } // return 'Decrease ' + tracks[effect.target].name; gen_action_standee(track); }, standee(s: number) { const node = get_active_node(); let value = node.a.v; if ( s === GOVERNMENT && (value === TOWARDS_CENTER || value === AWAY_FROM_CENTER) ) { value = get_government_track_direction(value); } move_track(s, value); resolve_active_and_proceed(); }, }; states.move_track_up_or_down = { inactive: 'move a track', prompt() { const node = get_active_node(); view.prompt = `Move ${get_track_name(node.a.track_id)} up or down`; gen_action('up'); gen_action('down'); }, down() { const node = get_active_node(); move_track(node.a.track_id, -1 * node.a.strength); resolve_active_and_proceed(); }, up() { const node = get_active_node(); move_track(node.a.track_id, node.a.strength); resolve_active_and_proceed(); }, }; states.player_turn = { inactive: 'play their turn', prompt() { const faction_id = get_faction_id(game.active as Player); const can_spend_hp = game.hero_points[faction_id] > 0; const can_play_card = game.hands[faction_id].includes( game.chosen_cards[faction_id] ); view.prompt = 'Play a card or spend Hero points'; if (!(can_play_card || can_spend_hp)) { view.prompt = 'End turn'; } else if (!can_play_card && can_spend_hp) { view.prompt = 'Spend Hero Points or end turn'; } // const card = game.cards_in_play[faction_id]; if (can_play_card) { // gen_action_card(); gen_action('play_for_ap'); gen_action('play_for_event'); } else { gen_action('done'); } if (can_spend_hp) { gen_action('spend_hp'); } }, done() { resolve_active_and_proceed(); }, play_for_ap() { const faction_id = get_faction_id(game.active as Player); const card = game.chosen_cards[faction_id]; log_h3(`${game.active} plays ${cards[card].title} for the Action Points`); array_remove(game.hands[faction_id], game.hands[faction_id].indexOf(card)); game.tableaus[faction_id].push(card); insert_before_active_node( create_seq_node([ create_leaf_node('choose_area_ap', faction_id, { strength: (cards[card] as PlayerCard).strength, }), create_function_node('check_activate_icon'), ]) ); next(); }, play_for_event() { const faction_id = get_faction_id(game.active as Player); const card = game.chosen_cards[faction_id]; array_remove(game.hands[faction_id], game.hands[faction_id].indexOf(card)); game.trash[faction_id].push(card); log_h3(`${game.active} plays ${cards[card].title} for the Event`); insert_after_active_node(create_effects_node(cards[card].effects)); resolve_active_and_proceed(); }, spend_hp() { // insert spend hero points node before current node // so it will return to current node after resolving insert_before_active_node( create_leaf_node('spend_hero_points', get_active_faction()) ); log('Spends Hero Points'); next(); }, }; states.spend_hero_points = { inactive: 'spend Hero points', prompt() { view.prompt = 'Spend your Hero points'; gen_action('done'); const hero_points = game.hero_points[get_active_faction()]; if (hero_points === 0) { return; } gen_action('draw_card'); if (hero_points < 2) { return; } gen_action_standee(FOREIGN_AID); gen_action_standee(SOVIET_SUPPORT); for (const bonus of bonuses) { if (game.bonuses[bonus] === OFF) { gen_action_bonus(bonus); } } if (hero_points < 3) { return; } gen_action_standee(COLLECTIVIZATION); gen_action_standee(LIBERTY); if (hero_points < 4) { return; } gen_action_standee(GOVERNMENT); }, done() { resolve_active_and_proceed(); }, bonus(b: number) { update_bonus(b, ON); pay_hero_points(get_active_faction(), 2); }, draw_card() { const faction = get_active_faction(); pay_hero_points(faction, 1); draw_hand_cards(faction, 1); }, standee(track_id: number) { let amount = 2; if (track_id === LIBERTY || track_id === COLLECTIVIZATION) { amount = 3; } else if (track_id === GOVERNMENT) { amount = 4; } const faction = get_active_faction(); pay_hero_points(faction, amount); insert_after_active_node( create_seq_node([ create_leaf_node('move_track_up_or_down', faction, { track_id, strength: 1, }), create_leaf_node('spend_hero_points', faction), ]) ); resolve_active_and_proceed(); }, }; // states.move_to = { // inactive: 'move', // prompt() { // view.prompt = 'Move the piece to a space.'; // for (let s = 0; s < 64; ++s) gen_action_space(s); // }, // // space(to) { // // game.location[game.selected] = to // // game.state = "move" // // if (game.active === PLAYER1) // // game.active = PLAYER2 // // else // // game.active = PLAYER1 // // }, // }; // #endrregion function pop_undo() { const save_log = game.log; const save_undo = game.undo; game = save_undo.pop()!; (save_log as string[]).length = game.log as unknown as number; game.log = save_log; game.undo = save_undo; } // #region GAME FUNCTIONS // Check if Morale bonus is on so player can activate icon function check_activate_icon() { if (game.bonuses[MORALE_BONUS] === ON) { insert_after_active_node( create_leaf_node('activate_icon', get_active_faction_id()) ); } resolve_active_and_proceed(); } function check_initiative() { let initiative: FactionId; if (game.tracks[LIBERTY] >= 6 && game.tracks[COLLECTIVIZATION] >= 6) { initiative = ANARCHISTS_ID; } else if (game.tracks[GOVERNMENT] <= 5) { initiative = COMMUNISTS_ID; } else { initiative = MODERATES_ID; } if (game.initiative === initiative) { return; } game.initiative = initiative; logi(`${faction_player_map[initiative]} claims the Initiative`); } function end_of_turn() { Object.keys(game.fronts).forEach((front_id) => { game.fronts[front_id].contributions = []; }); // log_h2('End of turn'); if (game.turn === 4) { end_of_year(); } else { game.turn++; start_turn(); } } function end_of_year() { const gloryToDraw = [0, 1, 2, 5]; for (let i = 0; i < gloryToDraw[game.year]; ++i) { const index = random(game.bag_of_glory.length); game.glory.push(game.bag_of_glory[index]); array_remove(game.bag_of_glory, index); } game.year++; start_year(); } function gain_hero_points_in_player_order(factions: FactionId[], value) { for (const f of get_player_order()) { if (factions.includes(f)) { gain_hero_points(f, value); } } } function gain_hero_points(faction_id: FactionId, value: number) { if (game.hero_points.pool === 0) { return; } const gain = Math.min(game.hero_points.pool, value); game.hero_points.pool -= gain; game.hero_points[faction_id] += gain; logi( `${get_player(faction_id)} +${gain} ${ gain === 1 ? 'Hero Point' : 'Hero Points' }` ); } function game_over(result: Player | 'None', victory: string) { insert_after_active_node(create_leaf_node('game_over', 'None')); // game.state = 'game_over'; // game.active = 'None'; game.result = result; game.victory = victory; log_br(); log(game.victory); } function resolve_fascist_test() { log_h2('Fascist Test', 'fascist'); const test = get_current_event().test; const status = game.fronts[test.front].status; const test_passed = status === VICTORY || (status !== DEFEAT && game.fronts[test.front].value >= test.value); if (test_passed) { log('The Test is passed'); } else { log('The Test is failed'); } const effect = test_passed ? test.pass : test.fail; const node = resolve_effect(effect); if (node !== null) { insert_after_active_node(node); } resolve_active_and_proceed(); } // TODO: check for defeated / won fronts function get_fronts_to_add_to(target: string): FrontId[] { console.log('get_fronts_to_add_to', target); if (target === CLOSEST_TO_DEFEAT || target === CLOSEST_TO_VICTORY) { return get_fronts_closest_to(target); } else if (target === ANY) { return FRONTS.filter((id) => game.fronts[id].status === null); } else { return [target as FrontId]; } } function get_max_value_for_track(track_id: number) { switch (track_id) { case LIBERTY: return game.tracks[COLLECTIVIZATION] >= 8 ? 10 : 7; case GOVERNMENT: return game.tracks[FOREIGN_AID] >= 8 ? 10 : 7; case COLLECTIVIZATION: case SOVIET_SUPPORT: case FOREIGN_AID: default: return 10; } } function get_min_value_for_track(track_id: number) { switch (track_id) { case GOVERNMENT: return game.tracks[SOVIET_SUPPORT] >= 8 ? 4 : 1; case LIBERTY: case COLLECTIVIZATION: case SOVIET_SUPPORT: case FOREIGN_AID: default: return 0; } } // TOWARDS_CENTER = 10; // AWAY_FROM_CENTER = 11; function get_government_track_direction(direction: 10 | 11): -1 | 1 { const value = game.tracks[GOVERNMENT]; if ( (direction === TOWARDS_CENTER && value >= 6) || (direction === AWAY_FROM_CENTER && value <= 5) ) { return -1; } else { return 1; } } function move_track(track_id: number, change: number) { const current_value = game.tracks[track_id]; let new_value = current_value + change; new_value = Math.max(new_value, get_min_value_for_track(track_id)); new_value = Math.min(new_value, get_max_value_for_track(track_id)); game.tracks[track_id] = new_value; logi(`${get_track_name(track_id)} to ${new_value}`); check_initiative(); const triggered_spaces = change > 0 ? make_list(current_value + 1, new_value).reverse() : make_list(new_value, current_value - 1); triggered_spaces.forEach((space_id) => { const trigger = tracks[track_id].triggers[space_id]; if ( trigger !== null && !game.triggered_track_effects[track_id].includes(space_id) ) { if (space_id !== 0) { game.triggered_track_effects[track_id].push(space_id); } const node = resolve_effect(trigger); if (node !== null) { insert_after_active_node(node); } } }); } function pay_hero_points(faction: FactionId, amount: number) { game.hero_points[faction] -= amount; game.hero_points.pool += amount; } function update_bonus(bonus_id: number, status: number) { if (game.bonuses[bonus_id] === status) { return; } game.bonuses[bonus_id] = status; logi(`${bonus_names[bonus_id]} ${status === ON ? 'on' : 'off'}`); } // TODO: acccount for victory / defeat of front function update_front( // f: string, front_id: FrontId, change: number, faction_id: FactionId | null = null ) { // Check teamwork bonus const player_token_on_front = faction_id !== null && game.fronts[front_id].contributions.includes(faction_id); if ( game.bonuses[TEAMWORK_BONUS] === ON && change > 0 && faction_id !== null && !player_token_on_front && game.fronts[front_id].contributions.length > 0 ) { change += 1; } const value_before = game.fronts[front_id].value; game.fronts[front_id].value += change; logi(`${front_names[front_id]}: ${change > 0 ? '+' : ''}${change}`); if ( faction_id !== null && value_before <= 0 && game.fronts[front_id].value > 0 ) { gain_hero_points(faction_id, 1); } // Add token to front if player contributed if ( faction_id !== null && !game.fronts[front_id].contributions.includes(faction_id) ) { game.fronts[front_id].contributions.push(faction_id); } // Check victory / defeat on a front if (game.fronts[front_id].value >= 10) { victory_on_a_front(front_id); } else if (game.fronts[front_id].value <= -10) { defeat_on_a_front(front_id); } } function defeat_on_a_front(front_id: FrontId) { game.fronts[front_id].status = DEFEAT; log('Defeat on ' + get_front_name(front_id)); // Check game end if (front_id === 'm' || get_defeated_front_count() == 2) { game_over('None', 'All players lose the game!'); return; } insert_after_active_node( create_effects_node([ create_effect('bonus', MORALE_BONUS, OFF), create_effect('track', COLLECTIVIZATION, -1), create_effect('track', SOVIET_SUPPORT, -1), create_effect('track', FOREIGN_AID, -1), ]) ); } function victory_on_a_front(front_id: FrontId) { game.fronts[front_id].status = VICTORY; log('Victory on ' + get_front_name(front_id)); gain_hero_points_in_player_order(game.fronts[front_id].contributions, 3); } function create_effects_node(effects: Effect[]): EngineNode { const nodes = effects.reduce((accrued: EngineNode[], current: Effect) => { const node = resolve_effect(current); if (node !== null) { accrued.push(node); } return accrued; }, []); return { t: seq_node, c: nodes, }; } const effect_type_state_map: Record = { attack: 'attack_front', bonus: 'change_bonus', front: 'add_to_front', track: 'move_track', }; function resolve_effect( effect: Effect, faction: FactionId = get_active_faction() ): EngineNode | null { const args = { t: effect.target, v: effect.value, }; let state = effect_type_state_map[effect.type]; if (state !== undefined) { return create_leaf_node(state, faction, args); } if ( effect.type === 'hero_points' && effect.target === PLAYER_WITH_MOST_HERO_POINTS ) { state = 'lose_hero_points'; } if (effect.type === 'hero_points' && effect.target === SELF) { state = 'gain_hero_points'; } return state === undefined ? null : create_leaf_node(state, faction, args); } // #endregion // #region CARDS // function draw_faction_card(faction: Player): CardId { // return draw_faction_cards(faction, 1)[0]; // } // function draw_faction_cards(faction: Player, count: number = 1): CardId[] { // const drawnCards = []; // } function draw_card(deck: CardId[]): CardId { clear_undo(); let i = random(deck.length); let c = deck[i] as CardId; set_delete(deck, c); return c; } function draw_item(ids: number[]): number { let i = random(ids.length); let r = ids[i] as CardId; set_delete(ids, r); return r; } function lose_hero_point(faction: FactionId, value: number) { const points_lost = Math.min(game.hero_points[faction], Math.abs(value)); game.hero_points.pool += points_lost; game.hero_points[faction] -= points_lost; if (points_lost === 0) { return; } if (points_lost === 1) { log(`${get_player(faction)}: -1 Hero Point`); } else { log(`${get_player(faction)}: -${points_lost} Hero Points`); } } // #endregion // #region FRONTS function get_fronts_closest_to(target: 'd' | 'v'): FrontId[] { const values = Object.values(game.fronts).reduce( (accrued: number[], current: Front) => { if (current.status === null) { accrued.push(current.value); } return accrued; }, [] ); const targetValue = target === 'd' ? Math.min(...values) : Math.max(...values); return Object.keys(game.fronts).filter( (frontId) => game.fronts[frontId].value === targetValue ) as FrontId[]; } // #endregion // #region LOGGING function log_br() { if (game.log.length > 0 && game.log[game.log.length - 1] !== '') game.log.push(''); } function log(msg: string) { game.log.push(msg); } // function logevent(cap: Card) { // game.log.push(`E${cap}.`) // } // function logcap(cap: Card) { // game.log.push(`C${cap}.`) // } function logi(msg: string) { log('>' + msg); } // function logii(msg: string) { // log('>>' + msg); // } function log_h1(msg: string) { log_br(); log('.h1 ' + msg); log_br(); } function log_h2(msg: string, player?: Player | 'fascist') { log_br(); log(`.h2${player ? `.${player}` : ''} ${msg}`); log_br(); } log; // function log_h2_active(msg: string) { // log_br(); // log('.h2 ' + msg); // log_br(); // } // function log_h2_common(msg: string) { // log_br(); // log('.h2 ' + msg); // log_br(); // } function log_h3(msg: string) { log_br(); log('.h3 ' + msg); } // function log_h4(msg: string) { // log_br(); // log('.h4 ' + msg); // } // #endregion LOGGING // #region UTILITY function clear_undo() { console.log('game clear undo', game?.undo); if (game?.undo && game.undo.length > 0) game.undo = []; } function get_active_faction(): FactionId { return player_faction_map[game.active]; } function get_active_faction_id(): FactionId { return player_faction_map[game.active]; } function get_faction_id(player: Player): FactionId { return player_faction_map[player]; } function get_front_name(id: FrontId | 'd' | 'v') { return front_names[id]; } function get_current_event(): EventCard { return cards[ game.current_events[game.current_events.length - 1] ] as EventCard; } function get_defeated_front_count() { let count = 0; for (const front_id of FRONTS) { if (game.fronts[front_id].status === DEFEAT) { count++; } } return count; } function get_icon_count_in_tableau( icon: Icon, faction: FactionId = get_active_faction_id() ) { let count = 0; for (const c of game.tableaus[faction]) { const card = cards[c] as PlayerCard; for (const i of card.icons) { if (i === icon) { ++count; } } } return count; } function get_player(faction_id: FactionId) { return faction_player_map[faction_id]; } function get_player_order(first_player = game.initiative): FactionId[] { const order = []; let faction = first_player; for (let i = 0; i < 3; ++i) { order.push(faction); faction = game.year === 2 ? get_previous_faction(faction) : get_next_faction(faction); } return order; } function get_previous_faction(faction_id: FactionId): FactionId { const index = role_ids.indexOf(faction_id); if (index === 0) { return role_ids[2]; } return role_ids[index - 1]; } function get_next_faction(faction_id: FactionId): FactionId { const index = role_ids.indexOf(faction_id); if (index === 2) { return role_ids[0]; } return role_ids[index + 1]; } function get_factions_with_most_hero_poins(): FactionId[] { let most_hero_points = null; let faction_ids = []; Object.entries(game.hero_points).forEach(([id, value]) => { if (id === 'pool') { return; } if (most_hero_points === null || value > most_hero_points) { most_hero_points = value; faction_ids = [id]; } else if (most_hero_points === value) { faction_ids.push(id); } }); return faction_ids; } function get_track_name(track_id: number): string { return tracks[track_id].name; } function make_list(first: number, last: number): number[] { let list = []; for (let i = first; i <= last; i++) list.push(i); return list; } function random(range: number): number { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = (game.seed * 200105) % 34359738337) % range; } function set_delete(set: T[], item: T) { let a = 0; let b = set.length - 1; while (a <= b) { let m = (a + b) >> 1; let x = set[m]; if (item < x) b = m - 1; else if (item > x) a = m + 1; else { array_remove(set, m); return; } } } function list_deck(id: FactionId | 'fascist') { const deck = []; const card_list = id === 'fascist' ? fascist_decks[game.year] : faction_cards[id]; card_list.forEach((card) => { if (id === 'fascist' && game.discard.f.includes(card)) { return; } else if ( id !== 'fascist' && (game.hands[id].includes(card) || game.discard[id].includes(card)) ) { return; } deck.push(card); }); return deck; } // #endregion // #region ARRAY function array_remove(array: T[], index: number) { let n = array.length; for (let i = index + 1; i < n; ++i) array[i - 1] = array[i]; array.length = n - 1; } function array_insert(array: T[], index: number, item: T) { for (let i = array.length; i > index; --i) array[i] = array[i - 1]; array[index] = item; } // function array_remove_pair(array: T[], index: number) { // let n = array.length; // for (let i = index + 2; i < n; ++i) array[i - 2] = array[i]; // array.length = n - 2; // } // function array_insert_pair( // array: (K | V)[], // index: number, // key: K, // value: V // ) { // for (let i = array.length; i > index; i -= 2) { // array[i] = array[i - 2]; // array[i + 1] = array[i - 1]; // } // array[index] = key; // array[index + 1] = value; // } // #endregion