'use strict'; import { CardId, EngineNode, FactionId, FunctionNode, Game, LeafNode, Player, States, View, } from './types'; import data, { // LIBERTY, // COLLECTIVIZATION, // GOVERNMENT, // SOVIET_SUPPORT, // FOREIGN_AID, // MORALE_BONUS, // TEAMWORK_BONUS, // OFF, ON, EventCard, CardEffect, LIBERTY, COLLECTIVIZATION, GOVERNMENT, SOVIET_SUPPORT, FOREIGN_AID, PLAYER_WITH_MOST_HERO_POINTS, PlayerCard, // 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 ANARCHISTS_ID = 'a' as FactionId; const COMMUNISTS_ID = 'c' as FactionId; const MODERATES_ID = 'm' as FactionId; 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: 'the Aragon Front', m: 'the Madrid Front', n: 'the Nothern Front', s: 'the Southern Front', d: 'the Front closest to Defeat', v: 'the Front closest to Victory', }; const tracks = [ LIBERTY, COLLECTIVIZATION, GOVERNMENT, SOVIET_SUPPORT, FOREIGN_AID, ]; const track_names: Record = { [LIBERTY]: 'Liberty', [COLLECTIVIZATION]: 'Collectivization', [GOVERNMENT]: 'Government', [SOVIET_SUPPORT]: 'Soviet Support', [FOREIGN_AID]: 'Foreign Aid', }; const { cards, // fronts } = data; 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, 62), }; 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_card(card_id: CardId) { gen_action('card', card_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 setup_bag_of_glory() { console.log('setup_bag_of_glory'); game.engine = [ { t: leaf_node, p: game.initiative, s: 'add_glory', }, { t: function_node, f: 'end_of_turn', }, ]; next(); } function setup_choose_card() { console.log('setup_choose_card'); const player_order = get_player_order(); game.engine = player_order.map((faction_id) => ({ t: leaf_node, p: faction_id, s: 'choose_card', })); game.engine.push({ t: function_node, f: 'setup_player_turn', }); next(); } function setup_player_turn() { console.log('setup_player_turn'); const player_order = get_player_order(); game.engine = player_order.map((faction_id) => ({ t: seq_node, c: [ { t: leaf_node, s: 'player_turn', p: faction_id, }, ], })); game.engine.push({ t: function_node, f: 'resolve_fascist_test', }); game.engine.push({ t: function_node, f: 'setup_bag_of_glory', }); next(); } const engine_functions: Record = { end_of_turn, end_of_year, setup_bag_of_glory, setup_choose_card, setup_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_parent(engine: EngineNode[]): EngineNode[] | null { // for (let i of engine) { // if ((i.t === leaf_node || i.t === function_node) && i.r !== resolved) { // return engine; // } // if (i.t === seq_node) { // const next_child = get_active_node_parent(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; // for (let i of engine) { // if ((i.t === leaf_node || i.t === function_node) && i.r !== resolved) { // return i; // } // // if (i.t === function_node && i.r !== resolved) { // // return i; // // } // if (i.t === seq_node) { // const next_child = get_active_node(i.c); // if (next_child !== null) { // return next_child; // } // } // } // return null; } function get_active_node_args(): any { const node = get_active_node(game.engine); if (node.t === leaf_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() { console.log('next'); const node = get_active_node(game.engine); console.log('node', node); if (node.t === function_node && engine_functions[node.f]) { resolve_active_node(); 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, bonuses: game.bonuses, current_events: game.current_events, fronts: game.fronts, hand: game.hands[faction_id], medaillons: game.medaillons, selected_card: game.cards_in_play[faction_id], tracks: game.tracks, }; 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]: 1, [COMMUNISTS_ID]: 1, [MODERATES_ID]: 1, }, blank_markers: [[], [], [], [], []], bonuses: [ON, ON], current_events: [], discard: { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], f: [], }, engine: [], fronts: { a: -2, m: -2, n: -2, s: -2, }, hands: { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], }, hero_points: { [ANARCHISTS_ID]: 2, [COMMUNISTS_ID]: 2, [MODERATES_ID]: 0, pool: 14, }, cards_in_play: { [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], log: [], undo: [], turn: 1, year: 1, state_data: null, }; start_year(); return game; } function draw_hand_cards() { role_ids.forEach((role) => { const deck = faction_cards[role]; for (let i = 0; i < 5; i++) { game.hands[role]; game.hands[role].push(draw_card(deck)); } }); } // #endregion function start_year() { log_h1('Year ' + game.year); draw_hand_cards(); start_turn(); } function start_turn() { log_h2('Turn ' + game.turn); const deck = fascist_decks[game.year]; const cardId = draw_card(deck); game.current_events.push(cardId); const card = cards[cardId] as EventCard; log_h3('Fascist Event: ' + card.title); game.engine = card.effects.map((_effect, index) => ({ t: leaf_node, s: 'resolve_event', p: game.initiative, a: index, })); 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.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[get_active_faction()] += number; 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.choose_area_ap = { inactive: 'choose area to use Action Points', prompt() { view.prompt = 'Choose area of the board to affect'; for (let track_id of tracks) { gen_action_standee(track_id); } }, standee(track_id: number) { console.log('standee', track_id); 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.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.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) { log_h3(`${game.active} chooses a card`); if (!game.cards_in_play) { game.cards_in_play = {}; } game.cards_in_play[player_faction_map[game.active]] = c; resolve_active_and_proceed(); }, }; states.player_turn = { inactive: 'play their turn', prompt() { const faction_id = get_faction_id(game.active); const hero_points = game.hero_points[faction_id]; view.prompt = hero_points === 0 ? 'Play your card' : 'Play your card or spend Hero points'; // const card = game.cards_in_play[faction_id]; if (game.cards_in_play[faction_id] !== null) { // gen_action_card(); gen_action('play_for_ap'); gen_action('play_for_event'); } else { gen_action('done'); } if (hero_points > 0) { gen_action('spend_hp'); } }, done() { resolve_active_and_proceed(); }, play_for_ap() { const faction_id = get_faction_id(game.active); const card = game.cards_in_play[faction_id]; log_h3(`${game.active} plays ${cards[card].title} for the Action Points`); if (!game.discard) { game.discard = { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], f: [], }; } array_remove(game.hands[faction_id], game.hands[faction_id].indexOf(card)); game.discard[faction_id].push(card); insert_before_active_node({ t: leaf_node, p: faction_id, s: 'choose_area_ap', a: { strength: (cards[card] as PlayerCard).strength, }, }); next(); }, play_for_event() { const faction_id = get_faction_id(game.active); const card = game.cards_in_play[faction_id]; log_h3(`${game.active} plays ${cards[card].title} for the Event`); game.cards_in_play[faction_id] = null; game.tableaus[faction_id].push(card); resolve_active_and_proceed(); }, spend_hp() { insert_before_active_node({ t: leaf_node, p: get_faction_id(game.active), s: 'spend_hero_points', }); // insert spend hero points node before current node next(); }, card(c: CardId) { const faction = get_active_faction(); log_h3(`${game.active} plays ${cards[c].title} to their tableau`); if (!game.tableaus) { game.tableaus = { [ANARCHISTS_ID]: [], [COMMUNISTS_ID]: [], [MODERATES_ID]: [], }; } game.cards_in_play[faction] = null; game.tableaus[faction].push(c); array_remove(game.hands[faction], game.hands[faction].indexOf(c)); resolve_active_and_proceed(); }, }; states.resolve_event = { inactive: 'resolve Fascist Event', prompt() { const card = get_current_event(); const node = get_active_node(game.engine) as LeafNode; const effect: CardEffect = card.effects[node.a]; view.prompt = get_event_prompt(effect); if ( effect.type === 'attack' && (effect.target === 'd' || effect.target === 'v') ) { const fronts = get_fronts_closest_to(effect.target); fronts.forEach((id) => gen_action('front', id)); } else if (effect.type === 'attack') { gen_action('front', effect.target); } else if (effect.type === 'track') { gen_action('standee', effect.target); } else if ( effect.type === 'hero_points' && effect.target === PLAYER_WITH_MOST_HERO_POINTS ) { const factions = get_factions_with_most_hero_poins(); for (let faction_id of factions) { gen_action(get_player(faction_id)); } } // for (let p = 0; p < 5; ++p) gen_action('standee', p); }, front(f: string) { const card = get_current_event(); const value = card.effects[get_active_node_args()].value; game.fronts[f] -= value; log_h3(`${value} attacks added to ${front_names[f]}`); resolve_active_and_proceed(); }, standee(s: string) { const effect = get_current_event().effects[get_active_node_args()]; const value = effect.value; game.tracks[s] += value; log_h3(`${track_names[effect.target]} decreased by ${Math.abs(value)}`); resolve_active_and_proceed(); }, Anarchist() { lose_hero_point(ANARCHISTS_ID, 1); resolve_active_and_proceed(); }, Communist() { lose_hero_point(ANARCHISTS_ID, 1); resolve_active_and_proceed(); }, Moderate() { lose_hero_point(ANARCHISTS_ID, 1); resolve_active_and_proceed(); }, }; states.spend_hero_points = { inactive: 'spend Hero points', prompt() { view.prompt = 'Spend your Hero points'; gen_action('done'); gen_action('draw_card'); }, done() { resolve_active_and_proceed(); }, draw_card() { game.hero_points[get_active_faction_id()]--; log(`${game.active} draws a card`); // TODO: Draw card if (game.hero_points[get_active_faction_id()] === 0) { 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 function end_of_turn() { // REMOVE playre tplems from the Fronts; log_h2('End of turn'); if (game.turn === 4) { end_of_year(); } else { game.turn++; start_turn(); } } function end_of_year() {} function resolve_fascist_test() { console.log('resolve fascist test'); log_h2('Fascist test is resolved'); next(); } function move_track(track: number, change: number) { } // #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], value); game.hero_points.pool += points_lost; game.hero_points[faction] -= points_lost; if (points_lost === 1) { log(`${get_player(faction)} loses 1 Hero Point`); } else { log(`${get_player(faction)} loses ${points_lost} Hero Points`); } } // #endregion // #region EVENTS function get_current_event(): EventCard { return cards[ game.current_events[game.current_events.length - 1] ] as EventCard; } // function get_front_name(frontId: string) { // switch (frontId) { // case 'a': // return 'the Aragon Front'; // case 'm': // return 'the Madrid Front'; // case 'n': // return 'the Nothern Front'; // case 's': // return 'the Southern Front'; // case 'd': // return 'the Front closest to Defeat'; // case 'v': // return 'the Front closest to Victory'; // default: // return ''; // } // } function get_event_prompt(effect: CardEffect) { let prompt = ''; switch (effect.type) { case 'attack': return 'Attack ' + front_names[effect.target as string]; case 'bonus': break; case 'hero_points': return 'Select player with most Hero points'; case 'track': return 'Decrease ' + track_names[effect.target]; } return prompt; } // function resolve_event_attack(target: string | number, value: number) { // switch (target) { // case 'v': // break; // case 'd': // break; // default: // game.fronts[target] += value; // } // } // function fascist_event() { // // log_h1('Year ' + game.year); // const deck = fascist_decks[game.year]; // const cardId = draw_card(deck); // game.current_events.push(cardId); // const card = cards[cardId] as EventCard; // card.effects.forEach((effect) => { // switch (effect.type) { // case 'attack': // resolve_event_attack(effect.target, effect.value); // break; // case 'bonus': // break; // case 'hero_points': // break; // case 'track': // game.tracks[effect.target] += effect.value; // break; // } // }); // } // #endregion // #region FRONTS function get_fronts_closest_to(target: 'd' | 'v') { const values = Object.values(game.fronts); const targetValue = target === 'd' ? Math.min(...values) : Math.max(...values); return Object.keys(game.fronts).filter( (frontId) => game.fronts[frontId] === targetValue ); } // #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) { // game.log.push('>' + msg); // } // function logii(msg: string) { // game.log.push('>>' + msg); // } function log_h1(msg: string) { log_br(); log('.h1 ' + msg); log_br(); } function log_h2(msg: string) { log_br(); log('.h2 ' + msg); log_br(); } // 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_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() { const most_hero_points = Math.max(...Object.values(game.hero_points)); const faction_ids = []; Object.entries(game.hero_points).forEach(([faction, hero_points]) => { if (hero_points === most_hero_points) { faction_ids.push(faction); } }); return faction_ids; } function get_track_name(track_id: number): string { return track_names[track_id]; } function make_list(first: number, last: 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; } } } // #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