diff options
Diffstat (limited to 'rules.ts')
-rw-r--r-- | rules.ts | 685 |
1 files changed, 580 insertions, 105 deletions
@@ -11,6 +11,12 @@ import data, { // OFF, ON, EventCard, + CardEffect, + LIBERTY, + COLLECTIVIZATION, + GOVERNMENT, + SOVIET_SUPPORT, + FOREIGN_AID, // StaticData, // PLAYER_WITH_MOST_HERO_POINTS, } from './data'; @@ -28,7 +34,7 @@ export type FactionId = Brand<string, 'FactionId'>; interface Game { [index: number]: any; seed: number; - log: number | string[]; + log: string[]; undo: Game[]; turn: number; year: number; @@ -36,7 +42,9 @@ interface Game { state: string | null; bag_of_glory: Record<FactionId, number>; bonuses: number[]; + cards_in_play: Record<FactionId, CardId>; current_events: CardId[]; + engine: EngineNode[]; fronts: { a: number; m: number; @@ -46,6 +54,7 @@ interface Game { hands: Record<FactionId, CardId[]>; hero_points: Record<FactionId, number>; initiative: FactionId; + tableaus: Record<FactionId, CardId[]>; tracks: number[]; result?: string; @@ -54,12 +63,14 @@ interface Game { location?: string; selected?: string; + state_data: any; // played_card: CardId // turn: Turn } export interface View { + engine: Game['engine']; log: number | string[]; active?: string | null; prompt: string | null; @@ -94,15 +105,46 @@ 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<FactionId, Player> = { + [ANARCHISTS_ID]: ANARCHIST, + [COMMUNISTS_ID]: COMMUNIST, + [MODERATES_ID]: MODERATE, +}; + +const player_faction_map: Record<Player, FactionId> = { + [ANARCHIST]: ANARCHISTS_ID, + [COMMUNIST]: COMMUNISTS_ID, + [MODERATE]: MODERATES_ID, +}; + +const front_names: Record<string, string> = { + 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 track_names: Record<number, string> = { + [LIBERTY]: 'Liberty', + [COLLECTIVIZATION]: 'Collectivization', + [GOVERNMENT]: 'Government', + [SOVIET_SUPPORT]: 'Soviet Support', + [FOREIGN_AID]: 'Foreign Aid', +}; + const { cards, // fronts } = data; const faction_cards = { - [ANARCHIST]: make_list(37, 54) as CardId[], - [COMMUNIST]: make_list(19, 36) as CardId[], - [MODERATE]: make_list(1, 18) as CardId[], + [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 fascist_decks = { @@ -113,18 +155,26 @@ export const scenarios = ['Standard']; export const roles: Player[] = [ANARCHIST, COMMUNIST, MODERATE]; -function gen_action(action, argument) { - if (!(action in view.actions)) view.actions[action] = []; - view.actions[action].push(argument); +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_space(space) { gen_action('space', space); } -function gen_action_piece(piece) { - gen_action('piece', piece); -} +// function gen_action_piece(piece) { +// gen_action('piece', piece); +// } // function gen_action_card(card) { // gen_action('card', card); @@ -136,6 +186,7 @@ export function action( 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); @@ -144,18 +195,171 @@ export function 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'); + game.engine = roles.map((role) => ({ + t: leaf_node, + p: player_faction_map[role], + s: 'choose_card', + })); + game.engine.push({ + t: function_node, + f: 'setup_player_turn', + }); + next(); +} + +function setup_player_turn() { + console.log('setup_player_turn'); + // TODO: reverse order in second round + const first = game.initiative; + const second = get_next_faction(first); + const third = get_next_faction(second); + game.engine = [first, second, third].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<string, Function> = { + end_of_turn, + end_of_year, + setup_bag_of_glory, + setup_choose_card, + setup_player_turn, + resolve_fascist_test, +}; + +type EngineNode = FunctionNode | LeafNode | SeqNode; + +interface FunctionNode { + t: 'f'; + f: string; // function to be triggered + a?: any; // args + r?: 0 | 1; // 1 if resolved +} + +interface SeqNode { + t: 's'; // Type + c: EngineNode[]; +} + +interface LeafNode { + t: 'l'; + s: string; // State + p: FactionId; // Player + a?: any; // args + r?: 0 | 1; // 1 if resolved +} + +function get_active_node(engine: EngineNode[]): FunctionNode | LeafNode | null { + for (let i of engine) { + if (i.t === leaf_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 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) { - console.log('game_view', state); - console.log('player', player); game = state; - const faction_id = get_faction_id(player); + const faction_id = player_faction_map[player]; view = { + engine: game.engine, log: game.log, prompt: null, location: game.location, @@ -163,7 +367,7 @@ function game_view(state: Game, player: Player) { current_events: game.current_events, fronts: game.fronts, hand: game.hands[faction_id], - tracks: game.tracks + tracks: game.tracks, }; if (player !== game.active) { @@ -181,9 +385,10 @@ function game_view(state: Game, player: Player) { // #endregion -// #region setup +// #region SETUP export function setup(seed: number, _scenario: string, _options: unknown) { + // game.seed = seed; game = { seed: seed, state: null, @@ -195,94 +400,243 @@ export function setup(seed: number, _scenario: string, _options: unknown) { }, bonuses: [ON, ON], current_events: [], + engine: [], fronts: { - a: 2, - m: 2, - n: 2, - s: 2, + a: -2, + m: -2, + n: -2, + s: -2, }, hands: { - [ANARCHISTS_ID]: [ - draw_card(faction_cards[ANARCHIST]), - draw_card(faction_cards[ANARCHIST]), - draw_card(faction_cards[ANARCHIST]), - draw_card(faction_cards[ANARCHIST]), - draw_card(faction_cards[ANARCHIST]), - ], - [COMMUNISTS_ID]: [ - draw_card(faction_cards[COMMUNIST]), - draw_card(faction_cards[COMMUNIST]), - draw_card(faction_cards[COMMUNIST]), - draw_card(faction_cards[COMMUNIST]), - draw_card(faction_cards[COMMUNIST]), - ], - [MODERATES_ID]: [ - draw_card(faction_cards[MODERATE]), - draw_card(faction_cards[MODERATE]), - draw_card(faction_cards[MODERATE]), - draw_card(faction_cards[MODERATE]), - draw_card(faction_cards[MODERATE]), - ], + [ANARCHISTS_ID]: [], + [COMMUNISTS_ID]: [], + [MODERATES_ID]: [], }, hero_points: { [ANARCHISTS_ID]: 2, [COMMUNISTS_ID]: 2, [MODERATES_ID]: 0, }, + cards_in_play: { + [ANARCHISTS_ID]: null, + [COMMUNISTS_ID]: null, + [MODERATES_ID]: null, + }, initiative: MODERATES_ID, + tableaus: { + [ANARCHISTS_ID]: [], + [COMMUNISTS_ID]: [], + [MODERATES_ID]: [], + }, tracks: [5, 5, 6, 3, 3], log: [], undo: [], turn: 1, year: 1, + state_data: null, }; - fascist_event(); - - game.state = 'move'; - + 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 -states.move = { - inactive: 'move', +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.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); + } + // 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(); + }, +}; + +states.choose_card = { + inactive: 'choose a card', prompt() { - view.prompt = 'Move a piece.'; - for (let p = 0; p < 32; ++p) gen_action_piece(p); + 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(); }, - // piece(p) { - // game.selected = p - // game.state = "move_to" - // }, }; -states.move_to = { - inactive: 'move', +states.player_turn = { + inactive: 'play their turn', prompt() { - view.prompt = 'Move the piece to a space.'; - for (let s = 0; s < 64; ++s) gen_action_space(s); + view.prompt = 'Play your card or spend Hero points'; + gen_action_card(game.cards_in_play[player_faction_map[game.active]]); + }, + 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(); }, - // space(to) { - // game.location[game.selected] = to - // game.state = "move" - // if (game.active === PLAYER1) - // game.active = PLAYER2 - // else - // game.active = PLAYER1 - // }, }; +// 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 number; + (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(); +} + +// #endregion + // #region CARDS // function draw_faction_card(faction: Player): CardId { // return draw_faction_cards(faction, 1)[0]; @@ -293,10 +647,13 @@ function pop_undo() { // } -function draw_card(deck: CardId[]) { +function draw_card(deck: CardId[]): CardId { + console.log('draw_card_deck', deck); clear_undo(); let i = random(deck.length); - let c = deck[i]; + console.log('random ', i); + let c = deck[i] as CardId; + console.log('draw_card_id', c); set_delete(deck, c); return c; } @@ -305,59 +662,177 @@ function draw_card(deck: CardId[]) { // #region EVENTS -function resolve_event_attack(target: string | number, value: number) { - switch (target) { - case 'v': +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 'd': + case 'hero_points': break; - default: - game.fronts[target] += value; + case 'track': + return 'Decrease ' + track_names[effect.target]; } + return prompt; } -function fascist_event() { - const deck = fascist_decks[game.year]; - const cardId = draw_card(deck); - game.current_events.push(cardId); +// function resolve_event_attack(target: string | number, value: number) { +// switch (target) { +// case 'v': +// break; +// case 'd': +// break; +// default: +// game.fronts[target] += value; +// } +// } - const card = cards[cardId] as EventCard; +// 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; +// } +// }); +// } - 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() { - if (game.undo.length > 0) game.undo = []; -} - -function get_faction_id(player: Player) { - switch (player) { - case ANARCHIST: - return ANARCHISTS_ID; - case COMMUNIST: - return COMMUNISTS_ID; - case MODERATE: - return MODERATES_ID; - default: - throw new Error('Unknown player'); + 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_next_faction(faction_id: FactionId): FactionId { + const index = role_ids.indexOf(faction_id); + let next_index = index + 1; + if (next_index === role_ids.length) { + next_index = 0; } + return role_ids[next_index]; } function make_list(first: number, last: number) { |