summaryrefslogtreecommitdiff
path: root/rules.ts
diff options
context:
space:
mode:
Diffstat (limited to 'rules.ts')
-rw-r--r--rules.ts299
1 files changed, 255 insertions, 44 deletions
diff --git a/rules.ts b/rules.ts
index 0b63c87..2f80d49 100644
--- a/rules.ts
+++ b/rules.ts
@@ -239,6 +239,16 @@ function create_seq_node(children: EngineNode[]): SeqNode {
};
}
+// Use to force confirm question if possible to undo
+function checkpoint() {
+ if (game.undo.length > 0) {
+ insert_after_active_node(
+ create_leaf_node('confirm_turn', get_active_faction())
+ );
+ }
+ resolve_active_and_proceed();
+}
+
function setup_bag_of_glory() {
game.engine = [
create_leaf_node('add_glory', game.initiative),
@@ -262,6 +272,7 @@ function setup_final_bid() {
game.engine = player_order.map((faction_id) =>
create_leaf_node('choose_final_bid', faction_id)
);
+ game.engine.push(create_function_node('checkpoint'));
game.engine.push(create_function_node('resolve_final_bid'));
game.engine.push(create_function_node('setup_choose_card'));
next();
@@ -290,6 +301,7 @@ function start_of_player_turn() {
const engine_functions: Record<string, Function> = {
check_activate_icon,
+ checkpoint,
end_of_turn,
// end_of_year,
setup_bag_of_glory,
@@ -318,6 +330,7 @@ const engine_functions: Record<string, Function> = {
card50_event2,
card53_event2,
card54_event1,
+ setup_return_card_from_trash,
};
function get_active(
@@ -352,6 +365,16 @@ function get_active_node_args<T = any>(): T {
return null;
}
+function update_active_node_args<T = any>(args: T) {
+ const node = get_active_node(game.engine);
+ if (node.t === leaf_node || node.t === function_node) {
+ node.a = {
+ ...node.a,
+ ...args,
+ };
+ }
+}
+
function insert_before_or_after_active_node(
node: EngineNode,
position: 'before' | 'after',
@@ -453,7 +476,6 @@ 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,
};
@@ -765,15 +787,17 @@ states.activate_icon = {
},
};
-// TODO: implement
-// cards 10, 36, 54
states.add_card_to_tableau = {
inactive: 'add a card to their tableau',
prompt() {
view.prompt = 'Choose a card to add to your tableau';
- for (const c of game.hands[get_active_faction()]) {
+ const faction = get_active_faction();
+ for (const c of game.hands[faction]) {
gen_action_card(c);
}
+ if (game.hands[faction].length === 0) {
+ gen_action('skip');
+ }
},
card(c: CardId) {
const faction_id = get_active_faction();
@@ -785,6 +809,9 @@ states.add_card_to_tableau = {
);
resolve_active_and_proceed();
},
+ skip() {
+ resolve_active_and_proceed();
+ },
};
states.add_glory = {
@@ -826,26 +853,27 @@ states.add_to_front = {
states.attack_front = {
inactive: 'attack a Front',
prompt() {
- const node = get_active_node();
- const front = node.a.t;
-
- let targets: Array<FrontId> = [];
-
- 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);
+ const { t: target, n } = get_active_node_args();
+
+ let fronts: Array<FrontId> = [];
+
+ if (target === ANY) {
+ fronts = get_fronts_to_add_to(ANY, n);
+ } else if (target === 'd' || target === 'v') {
+ fronts = get_fronts_closest_to(target);
+ } else if (game.fronts[target].status === DEFEAT) {
+ fronts = get_fronts_closest_to('d');
+ } else if (game.fronts[target].status === VICTORY) {
+ fronts = get_fronts_to_add_to(ANY);
} else {
- targets.push(front);
+ fronts.push(target);
}
view.prompt =
- targets.length === 1
- ? `Attack ${front_names[targets[0]]}`
+ fronts.length === 1
+ ? `Attack ${front_names[fronts[0]]}`
: 'Attack a front';
- targets.forEach((id) => gen_action('front', id));
+ fronts.forEach((id) => gen_action('front', id));
},
front(f: FrontId) {
const node = get_active_node();
@@ -922,8 +950,17 @@ states.choose_area_ap = {
}
},
bonus(b: number) {
+ // Turn on bonus
update_bonus(b, ON);
- // TODO: insert action in case other bonus is OFF and AP left
+ const s: number = get_active_node_args().strength;
+
+ // Check if other bonus can be activated
+ const other_bonus = b === TEAMWORK_BONUS ? MORALE_BONUS : TEAMWORK_BONUS;
+ if (s > 1 && game.bonuses[other_bonus] === OFF) {
+ insert_after_active_node(
+ resolve_effect(create_effect('bonus', other_bonus, ON))
+ );
+ }
resolve_active_and_proceed();
},
front(f: FrontId) {
@@ -946,7 +983,7 @@ states.choose_area_ap = {
};
states.change_bonus = {
- inactive: 'gain select Bonus',
+ inactive: 'select Bonus',
prompt() {
const args = get_active_node_args();
if (
@@ -964,8 +1001,10 @@ states.change_bonus = {
gen_action_bonus(bonus);
}
}
- } else if (args.v === OFF) {
- view.prompt = `Turn off ${bonus_names[args.t]}`;
+ } else {
+ view.prompt = `Turn ${args.v === OFF ? 'off' : 'on'} ${
+ bonus_names[args.t]
+ }`;
gen_action_bonus(args.t);
}
},
@@ -1013,14 +1052,21 @@ states.choose_final_bid = {
card(c: CardId) {
const faction = get_active_faction();
game.selected_cards[faction].push(c);
- if (game.selected_cards[faction].length < 3) {
- next();
- } else {
+ const number_selected = game.selected_cards[faction].length;
+ console.log('number selected', number_selected);
+ const number_hand = game.hands[faction].length;
+ if (
+ number_selected === 3 ||
+ (number_hand < 4 && number_selected === number_hand - 1)
+ ) {
+ console.log('proceed');
resolve_active_and_proceed();
+ } else {
+ next();
}
},
done() {
- resolve_active_and_proceed();
+ resolve_active_and_proceed(true);
},
};
@@ -1132,10 +1178,19 @@ states.end_of_year_discard = {
},
};
+// TODO: rename to change_hero_points
states.gain_hero_points = {
inactive: 'gain Hero Points',
prompt() {
const value = get_active_node_args().v;
+ if (value < 0) {
+ view.prompt =
+ value < -1
+ ? `Lose ${Math.abs(value)} Hero Points`
+ : 'Lose 1 Hero Point';
+ gen_action('lose_hp');
+ return;
+ }
if (game.hero_points.pool > 0) {
view.prompt =
value > 1 ? `Gain ${value} Hero Points` : 'Gain 1 Hero Point';
@@ -1150,6 +1205,11 @@ states.gain_hero_points = {
gain_hero_points(get_active_faction(), value);
resolve_active_and_proceed();
},
+ lose_hp() {
+ const value = get_active_node_args().v;
+ lose_hero_point(get_active_faction(), value)
+ resolve_active_and_proceed();
+ },
skip() {
resolve_active_and_proceed();
},
@@ -1271,7 +1331,6 @@ states.move_track_up_or_down = {
},
};
-// TODO: implement card 46
states.peek_fascist_cards = {
inactive: 'peek at Fascist cards',
prompt() {
@@ -1344,7 +1403,7 @@ states.player_turn = {
},
play_for_event() {
const faction = get_active_faction();
- const { effects } = play_card(faction, 'play_for_ap');
+ const { effects } = play_card(faction, 'play_for_event');
insert_before_active_node(create_effects_node(effects));
@@ -1393,20 +1452,103 @@ states.remove_blank_marker = {
},
};
-// TOOD: implement from card 6
+/**
+ * Event cards 6 and 39
+ * 6: remove 1 attack from up to three fronts
+ * 16: Move up to 2 Attacks from a Front to another Front
+ * 39: remove up to 3 attacks from a Front; place 2 back(-2)
+ */
states.remove_attack_from_fronts = {
inactive: 'remove attacks',
prompt() {
- view.prompt = 'Choose a front to remove an attack';
+ const { f, v: card_id } = get_active_node_args();
+ view.prompt =
+ card_id === 6
+ ? 'Choose a Front to remove an attack from'
+ : 'Choose a Front to remove attacks from';
+ const front_data = f ?? {};
+
+ let is_front_with_attacks = false;
+ FRONTS.forEach((id) => {
+ if (game.fronts[id].value >= 0 || game.fronts[id].status !== null) {
+ return;
+ }
+ if (card_id === 6 && front_data[id]) {
+ return;
+ }
+ is_front_with_attacks = true;
+ gen_action_front(id);
+ });
+ if (!is_front_with_attacks) {
+ view.prompt = 'No valid Front to choose. You must skip';
+ gen_action('skip');
+ }
+ },
+ front(id: FrontId) {
+ const { f, v: card_id } = get_active_node_args();
+
+ const removed_value =
+ card_id === 6 ? 1 : Math.min(3, Math.abs(game.fronts[id].value));
+
+ update_front(id, removed_value);
+
+ const fronts = f ?? {};
+ fronts[id] = removed_value;
+
+ update_active_node_args({ f: fronts });
+
+ if (card_id === 6 && Object.keys(fronts).length === 3) {
+ resolve_active_and_proceed();
+ } else if (card_id === 39 || card_id === 16) {
+ insert_after_active_node(
+ create_leaf_node('attack_front', get_active_faction(), {
+ t: ANY,
+ v: card_id === 39 ? -2 : -1 * removed_value,
+ n: card_id === 16 ? id : undefined,
+ })
+ );
+ resolve_active_and_proceed();
+ }
+ },
+ skip() {
+ const { f, v: card_id } = get_active_node_args();
+ const values: number[] = Object.values(f ?? {});
+ if (card_id === 39 && values.length > 0) {
+ insert_after_active_node(
+ create_leaf_node('attack_front', get_active_faction(), {
+ t: ANY,
+ v: -2,
+ })
+ );
+ }
+ resolve_active_and_proceed();
},
};
-// TOOD: implement from card 15
states.return_card = {
- inactive: 'return a card',
+ inactive: 'return a card to their hand',
prompt() {
- view.prompt = 'Choose a card to return';
- // Return from trash
+ view.prompt = 'Choose a card to return to your hand';
+ if (game.selectable_cards.length === 0) {
+ view.prompt = 'No card in trash to return. You must skip';
+ gen_action('skip');
+ }
+ for (const c of game.selectable_cards) {
+ gen_action_card(c);
+ }
+ },
+ card(c: CardId) {
+ const faction = get_active_faction();
+ array_remove(game.trash[faction], game.trash[faction].indexOf(c));
+ game.hands[faction].push(c);
+ logi(`${faction_player_map[faction]} returns a card to their hand`);
+
+ game.selectable_cards = [];
+ resolve_active_and_proceed();
+ },
+ skip() {
+ game.selectable_cards = [];
+ resolve_active_and_proceed();
},
};
@@ -1513,17 +1655,73 @@ states.spend_hero_points = {
},
};
-// TODO: implement, card 33
states.swap_card_tableau_hand = {
inactive: 'swap cards',
prompt() {
- view.prompt = 'Choose cards';
+ view.prompt =
+ 'Choose a card in your tableau and a card in your hand to swap';
const faction = get_active_faction();
- for (const c of game.hands[faction]) {
- gen_action_card(c);
+ gen_action('skip');
+ if (game.tableaus[faction].length === 0) {
+ view.prompt = 'No card in your tableau to swap. You must skip';
+ return;
+ }
+ if (game.hands[faction].length === 0) {
+ view.prompt = 'No card in your hand to swap. You must skip';
+ return;
}
+ if (
+ !game.selected_cards[faction].some((card_id) =>
+ game.hands[faction].includes(card_id)
+ )
+ ) {
+ for (const c of game.hands[faction]) {
+ gen_action_card(c);
+ }
+ }
+ if (
+ !game.selected_cards[faction].some((card_id) =>
+ game.tableaus[faction].includes(card_id)
+ )
+ ) {
+ for (const c of game.tableaus[faction]) {
+ gen_action_card(c);
+ }
+ }
+ },
+ card(c: CardId) {
+ console.log('card', c);
+ const faction = get_active_faction();
+ const selected_cards = game.selected_cards[faction];
+
+ selected_cards.push(c);
+
+ if (selected_cards.length === 2) {
+ const hand_card_index = selected_cards.findIndex((card_id) =>
+ game.hands[faction].includes(card_id)
+ );
+
+ const tableau_card_index = hand_card_index === 0 ? 1 : 0;
+
+ const hand = game.hands[faction];
+ const tableau = game.tableaus[faction];
+
+ array_remove(hand, hand.indexOf(selected_cards[hand_card_index]));
+ array_remove(
+ tableau,
+ tableau.indexOf(selected_cards[tableau_card_index])
+ );
+ hand.push(selected_cards[tableau_card_index]);
+ tableau.push(selected_cards[hand_card_index]);
+ game.selected_cards[faction] = [];
+ resolve_active_and_proceed();
+ }
+ },
+ skip() {
+ const faction = get_active_faction();
+ game.selected_cards[faction] = [];
+ resolve_active_and_proceed();
},
- // card(c: CardId) {},
};
function resolve_take_hero_points(faction: FactionId) {
@@ -1685,6 +1883,7 @@ function card23_event1() {
function card26_event1() {
game.active_abilities.push(COMMUNIST_EXTRA_HERO_POINT);
+ resolve_active_and_proceed();
}
function card29_event2() {
@@ -1705,6 +1904,7 @@ function card35_event2() {
function card42_event3() {
game.active_abilities.push(ANARCHIST_EXTRA_HERO_POINT);
+ resolve_active_and_proceed();
}
function card45_event2() {
@@ -1745,6 +1945,14 @@ function card54_event1() {
resolve_active_and_proceed();
}
+function setup_return_card_from_trash() {
+ const faction = get_active_faction();
+ game.selectable_cards = game.trash[faction].filter(
+ (c) => c !== game.played_card
+ );
+ resolve_active_and_proceed();
+}
+
// #endregion
// #region GAME FUNCTIONS
@@ -1860,7 +2068,9 @@ function end_of_year() {
if (is_won) {
log('The war is won!');
} else {
+ console.log('The war is lost');
game_over('None', 'The war is lost. All Players lose the game!');
+ resolve_active_and_proceed();
return;
}
}
@@ -1943,6 +2153,7 @@ function game_over(result: Player | 'None', victory: string) {
// game.active = 'None';
game.result = result;
game.victory = victory;
+ game.undo = [];
log_br();
log(game.victory);
}
@@ -2048,12 +2259,13 @@ function resolve_final_bid() {
resolve_active_and_proceed();
}
-// TODO: check for defeated / won fronts
-function get_fronts_to_add_to(target: string): FrontId[] {
+function get_fronts_to_add_to(target: string, not: FrontId[] = []): FrontId[] {
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);
+ return FRONTS.filter(
+ (id) => game.fronts[id].status === null && !not.includes(id)
+ );
} else {
return [target as FrontId];
}
@@ -2180,7 +2392,6 @@ function update_bonus(bonus_id: number, status: number) {
logi(`${bonus_names[bonus_id]} ${status === ON ? 'on' : 'off'}`);
}
-// TODO: acccount for victory / defeat of front
function update_front(
// f: string,
front_id: FrontId,