summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--play.js4
-rw-r--r--play.ts4
-rw-r--r--rules.js207
-rw-r--r--rules.ts245
-rw-r--r--types.d.ts1
5 files changed, 372 insertions, 89 deletions
diff --git a/play.js b/play.js
index fa7dcb4..aeea368 100644
--- a/play.js
+++ b/play.js
@@ -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');
diff --git a/play.ts b/play.ts
index 5fbdd4e..83c0f19 100644
--- a/play.ts
+++ b/play.ts
@@ -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');
diff --git a/rules.js b/rules.js
index 3b085a9..f68af28 100644
--- a/rules.js
+++ b/rules.js
@@ -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;
+}
diff --git a/rules.ts b/rules.ts
index 3f0a2cf..6831f5e 100644
--- a/rules.ts
+++ b/rules.ts
@@ -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
diff --git a/types.d.ts b/types.d.ts
index 859fa79..06f4125 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -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;
}