diff options
-rw-r--r-- | play.js | 4 | ||||
-rw-r--r-- | play.ts | 4 | ||||
-rw-r--r-- | rules.js | 207 | ||||
-rw-r--r-- | rules.ts | 245 | ||||
-rw-r--r-- | types.d.ts | 1 |
5 files changed, 372 insertions, 89 deletions
@@ -333,7 +333,8 @@ function on_update() { action_button('d_foreign_aid', 'Decrease Foreign Aid'); action_button('d_government', 'Decrease Government'); action_button('d_soviet_support', 'Decrease Soviet Support'); - action_button('draw_card', 'Draw a Card'); + action_button('draw_card', 'Draw a card'); + action_button('draw_cards', 'Draw cards'); action_button('foreign_aid', 'Foreign Aid'); action_button('government', 'Government'); action_button('liberty', 'Liberty'); @@ -351,6 +352,7 @@ function on_update() { action_button('down', 'Down'); action_button('next', 'Next'); action_button('remove_blank_marker', 'Remove Blank marker'); + action_button('confirm', 'Confirm'); action_button('yes', 'Yes'); action_button('no', 'No'); action_button('skip', 'Skip'); @@ -435,7 +435,8 @@ function on_update() { action_button('d_government', 'Decrease Government'); action_button('d_soviet_support', 'Decrease Soviet Support'); - action_button('draw_card', 'Draw a Card'); + action_button('draw_card', 'Draw a card'); + action_button('draw_cards', 'Draw cards'); action_button('foreign_aid', 'Foreign Aid'); action_button('government', 'Government'); action_button('liberty', 'Liberty'); @@ -455,6 +456,7 @@ function on_update() { action_button('down', 'Down'); action_button('next', 'Next'); action_button('remove_blank_marker', 'Remove Blank marker'); + action_button('confirm', 'Confirm'); action_button('yes', 'Yes'); action_button('no', 'No'); action_button('skip', 'Skip'); @@ -71,7 +71,10 @@ function gen_action_standee(track_id) { gen_action('standee', track_id); } function action(state, player, action, arg) { - console.log('action', state, player, action, arg); + console.log('action', player, action, arg); + if (action !== 'undo') { + state.undo = push_undo(); + } game = state; let S = states[game.state]; if (action in S) @@ -212,17 +215,27 @@ function next() { } else if (node.t === 'l') { game.state = node.s; - game.active = faction_player_map[node.p]; + const current_active = game.active; + const next_active = faction_player_map[node.p]; + if (next_active !== current_active && game.undo.length > 0) { + insert_before_active_node(create_leaf_node('confirm_turn', get_active_faction())); + game.state = 'confirm_turn'; + return; + } + game.active = next_active; } } -function resolve_active_node() { +function resolve_active_node(checkpoint = false) { const next_node = get_active_node(game.engine); if (next_node !== null) { next_node.r = resolved; } + if (checkpoint) { + clear_undo(); + } } -function resolve_active_and_proceed() { - resolve_active_node(); +function resolve_active_and_proceed(checkpoint = false) { + resolve_active_node(checkpoint); next(); } function game_view(state, player) { @@ -248,6 +261,7 @@ function game_view(state, player) { tableaus: game.tableaus, tracks: game.tracks, triggered_track_effects: game.triggered_track_effects, + undo: game.undo, used_medallions: game.used_medallions, year: game.year, }; @@ -791,6 +805,34 @@ states.choose_medallion = { resolve_active_and_proceed(); }, }; +states.confirm_turn = { + inactive: 'confirm their turn', + prompt() { + view.prompt = 'Confirm your actions or undo'; + gen_action('confirm'); + }, + confirm() { + resolve_active_and_proceed(true); + }, +}; +states.draw_card = { + inactive: 'draw a card', + prompt() { + const { v } = get_active_node_args(); + view.prompt = v === 1 ? 'Draw a card' : `Draw ${v} cards`; + gen_action(v === 1 ? 'draw_card' : 'draw_cards'); + }, + draw_card() { + const { v } = get_active_node_args(); + draw_hand_cards(get_active_faction(), v); + resolve_active_and_proceed(); + }, + draw_cards() { + const { v } = get_active_node_args(); + draw_hand_cards(get_active_faction(), v); + resolve_active_and_proceed(); + }, +}; states.end_of_year_discard = { inactive: 'discard cards from hand and tableau', prompt() { @@ -947,7 +989,7 @@ states.player_turn = { } }, done() { - resolve_active_and_proceed(); + resolve_active_and_proceed(true); }, play_for_ap() { const faction_id = get_faction_id(game.active); @@ -1137,14 +1179,6 @@ states.use_strategy_medallion = { resolve_active_and_proceed(); }, }; -function pop_undo() { - const save_log = game.log; - const save_undo = game.undo; - game = save_undo.pop(); - save_log.length = game.log; - game.log = save_log; - game.undo = save_undo; -} function add_glory(faction, amount, indent = false) { for (let i = 0; i < amount; ++i) { game.bag_of_glory.push(get_active_faction()); @@ -1560,7 +1594,14 @@ function resolve_effect(effect) { if (effect.type === 'hero_points' && effect.target === data_1.SELF) { state = 'gain_hero_points'; } - return state === undefined ? null : create_leaf_node(state, faction, args); + if (effect.type === 'draw_card' && effect.target === data_1.SELF) { + state = 'draw_card'; + } + if (state === undefined) { + console.log('----UNRESOLVED EFFECT----', effect); + return null; + } + return create_leaf_node(state, faction, args); } function win_final_bid(faction_id) { log_br(); @@ -1625,11 +1666,6 @@ function log_h3(msg) { log_br(); log('.h3 ' + msg); } -function clear_undo() { - console.log('game clear undo', game?.undo); - if (game?.undo && game.undo.length > 0) - game.undo = []; -} function get_active_faction() { return player_faction_map[game.active]; } @@ -1724,25 +1760,6 @@ function make_list(first, last) { list.push(i); return list; } -function random(range) { - return (game.seed = (game.seed * 200105) % 34359738337) % range; -} -function set_delete(set, item) { - 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) { const deck = []; const card_list = id === 'fascist' ? fascist_decks[game.year] : faction_cards[id]; @@ -1754,7 +1771,8 @@ function list_deck(id) { else if (id !== 'fascist' && (game.hands[id].includes(card) || game.discard[id].includes(card) || - game.tableaus[id].includes(card))) { + game.tableaus[id].includes(card) || + game.trash[id].includes(card))) { return; } deck.push(card); @@ -1770,6 +1788,67 @@ function draw_medallions() { game.medallions.pool.push(r); } } +function clear_undo() { + if (game.undo) { + game.undo.length = 0; + } +} +function push_undo() { + if (game.undo) { + let copy = {}; + for (let k in game) { + let v = game[k]; + if (k === 'undo') + continue; + else if (k === 'log') + v = v.length; + else if (typeof v === 'object' && v !== null) + v = object_copy(v); + copy[k] = v; + } + game.undo.push(copy); + } + return game.undo; +} +function pop_undo() { + if (game.undo) { + let save_log = game.log; + let save_undo = game.undo; + game = save_undo.pop(); + save_log.length = game.log; + game.log = save_log; + game.undo = save_undo; + } + next(); +} +function random(range) { + return (game.seed = (game.seed * 200105) % 34359738337) % range; +} +function object_copy(original) { + if (Array.isArray(original)) { + let n = original.length; + let copy = new Array(n); + for (let i = 0; i < n; ++i) { + let v = original[i]; + if (typeof v === 'object' && v !== null) + copy[i] = object_copy(v); + else + copy[i] = v; + } + return copy; + } + else { + let copy = {}; + for (let i in original) { + let v = original[i]; + if (typeof v === 'object' && v !== null) + copy[i] = object_copy(v); + else + copy[i] = v; + } + return copy; + } +} function array_remove(array, index) { let n = array.length; for (let i = index + 1; i < n; ++i) @@ -1781,3 +1860,51 @@ function array_insert(array, index, item) { array[i] = array[i - 1]; array[index] = item; } +function set_clear(set) { + set.length = 0; +} +function set_has(set, item) { + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) + b = m - 1; + else if (item > x) + a = m + 1; + else + return true; + } + return false; +} +function set_add(set, item) { + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) + b = m - 1; + else if (item > x) + a = m + 1; + else + return set; + } + return array_insert(set, a, item); +} +function set_delete(set, item) { + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) + b = m - 1; + else if (item > x) + a = m + 1; + else + return array_remove(set, m); + } + return set; +} @@ -175,12 +175,16 @@ function gen_action_standee(track_id: number) { // } export function action( - state: any, + state: Game, player: Player, action: string, arg: unknown ) { - console.log('action', state, player, action, arg); + console.log('action', player, action, arg); + if (action !== 'undo') { + state.undo = push_undo(); + } + game = state; let S = states[game.state]; if (action in S) S[action](arg, player); @@ -312,7 +316,7 @@ function get_active_node( return a === null ? null : a.node; } -function get_active_node_args(): any { +function get_active_node_args<T = any>(): T { const node = get_active_node(game.engine); if (node.t === leaf_node || node.t === function_node) { return node.a; @@ -360,19 +364,34 @@ function next() { } } else if (node.t === 'l') { game.state = node.s; - game.active = faction_player_map[node.p]; + + // Control switches to another player and player can undo + // so ask to confirm turn + const current_active = game.active; + const next_active = faction_player_map[node.p]; + if (next_active !== current_active && game.undo.length > 0) { + insert_before_active_node( + create_leaf_node('confirm_turn', get_active_faction()) + ); + game.state = 'confirm_turn'; + return; + } + game.active = next_active; } } -function resolve_active_node() { +function resolve_active_node(checkpoint = false) { const next_node = get_active_node(game.engine); if (next_node !== null) { next_node.r = resolved; } + if (checkpoint) { + clear_undo(); + } } -function resolve_active_and_proceed() { - resolve_active_node(); +function resolve_active_and_proceed(checkpoint = false) { + resolve_active_node(checkpoint); next(); } @@ -388,7 +407,7 @@ function game_view(state: Game, player: Player) { const faction_id = player_faction_map[player]; view = { - engine: game.engine, + engine: game.engine, // TODO: remove log: game.log, prompt: null, location: game.location, @@ -407,6 +426,7 @@ function game_view(state: Game, player: Player) { tableaus: game.tableaus, tracks: game.tracks, triggered_track_effects: game.triggered_track_effects, + undo: game.undo, // TODO: remove used_medallions: game.used_medallions, year: game.year, }; @@ -991,6 +1011,36 @@ states.choose_medallion = { }, }; +states.confirm_turn = { + inactive: 'confirm their turn', + prompt() { + view.prompt = 'Confirm your actions or undo'; + gen_action('confirm'); + }, + confirm() { + resolve_active_and_proceed(true); + }, +}; + +states.draw_card = { + inactive: 'draw a card', + prompt() { + const { v } = get_active_node_args(); + view.prompt = v === 1 ? 'Draw a card' : `Draw ${v} cards`; + gen_action(v === 1 ? 'draw_card' : 'draw_cards'); + }, + draw_card() { + const { v } = get_active_node_args(); + draw_hand_cards(get_active_faction(), v); + resolve_active_and_proceed(); + }, + draw_cards() { + const { v } = get_active_node_args(); + draw_hand_cards(get_active_faction(), v); + resolve_active_and_proceed(); + }, +}; + states.end_of_year_discard = { inactive: 'discard cards from hand and tableau', prompt() { @@ -1163,7 +1213,7 @@ states.player_turn = { } }, done() { - resolve_active_and_proceed(); + resolve_active_and_proceed(true); }, play_for_ap() { const faction_id = get_faction_id(game.active as Player); @@ -1174,7 +1224,7 @@ states.player_turn = { insert_before_active_node( create_seq_node([ create_leaf_node('choose_area_ap', faction_id, { - strength: (cards[card] as PlayerCard).strength, + strength: (cards[card] as PlayerCard).strength, }), create_function_node('check_activate_icon'), ]) @@ -1394,14 +1444,6 @@ states.use_strategy_medallion = { // #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 @@ -1932,7 +1974,14 @@ function resolve_effect( if (effect.type === 'hero_points' && effect.target === SELF) { state = 'gain_hero_points'; } - return state === undefined ? null : create_leaf_node(state, faction, args); + if (effect.type === 'draw_card' && effect.target === SELF) { + state = 'draw_card'; + } + if (state === undefined) { + console.log('----UNRESOLVED EFFECT----', effect); + return null; + } + return create_leaf_node(state, faction, args); } function win_final_bid(faction_id: FactionId) { @@ -2067,10 +2116,6 @@ function log_h3(msg: string) { // #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]; @@ -2185,28 +2230,6 @@ function make_list(first: number, last: number): number[] { 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<T>(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 = @@ -2221,7 +2244,8 @@ function list_deck(id: FactionId | 'fascist') { id !== 'fascist' && (game.hands[id].includes(card) || game.discard[id].includes(card) || - game.tableaus[id].includes(card)) + game.tableaus[id].includes(card) || + game.trash[id].includes(card)) ) { return; } @@ -2242,7 +2266,71 @@ function draw_medallions() { // #endregion -// #region ARRAY +// #region COMMON LIBRARY + +function clear_undo() { + if (game.undo) { + game.undo.length = 0; + } +} + +function push_undo() { + if (game.undo) { + let copy = {} as Game; + for (let k in game) { + let v = game[k]; + if (k === 'undo') continue; + else if (k === 'log') v = v.length; + else if (typeof v === 'object' && v !== null) v = object_copy(v); + copy[k] = v; + } + game.undo.push(copy); + } + return game.undo; +} + +function pop_undo() { + if (game.undo) { + let save_log = game.log; + let 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; + } + next(); +} + +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; +} + +// Fast deep copy for objects without cycles +function object_copy(original) { + if (Array.isArray(original)) { + let n = original.length; + let copy = new Array(n); + for (let i = 0; i < n; ++i) { + let v = original[i]; + if (typeof v === 'object' && v !== null) copy[i] = object_copy(v); + else copy[i] = v; + } + return copy; + } else { + let copy = {}; + for (let i in original) { + let v = original[i]; + if (typeof v === 'object' && v !== null) copy[i] = object_copy(v); + else copy[i] = v; + } + return copy; + } +} + +// Array remove and insert (faster than splice) function array_remove<T>(array: T[], index: number) { let n = array.length; @@ -2275,4 +2363,67 @@ function array_insert<T>(array: T[], index: number, item: T) { // array[index + 1] = value; // } +// Set as plain sorted array + +function set_clear<T>(set: T[]) { + // eslint-disable-line @typescript-eslint/no-unused-vars + set.length = 0; +} + +function set_has<T>(set: T[], item: T) { + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) b = m - 1; + else if (item > x) a = m + 1; + else return true; + } + return false; +} + +function set_add<T>(set: T[], item: T) { + // eslint-disable-line @typescript-eslint/no-unused-vars + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) b = m - 1; + else if (item > x) a = m + 1; + else return set; + } + return array_insert(set, a, item); +} + +// function set_delete<T>(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 set_delete<T>(set: T[], item: T) { + // eslint-disable-line @typescript-eslint/no-unused-vars + let a = 0; + let b = set.length - 1; + while (a <= b) { + const m = (a + b) >> 1; + const x = set[m]; + if (item < x) b = m - 1; + else if (item > x) a = m + 1; + else return array_remove(set, m); + } + return set; +} + // #endregion @@ -86,6 +86,7 @@ export interface View { tableaus: Game['tableaus']; tracks: number[]; triggered_track_effects: Game['triggered_track_effects']; + undo: Game['undo']; used_medallions: Game['used_medallions']; year: number; } |