summaryrefslogtreecommitdiff
path: root/rules.ts
diff options
context:
space:
mode:
Diffstat (limited to 'rules.ts')
-rw-r--r--rules.ts408
1 files changed, 330 insertions, 78 deletions
diff --git a/rules.ts b/rules.ts
index 18bb5d3..2cee300 100644
--- a/rules.ts
+++ b/rules.ts
@@ -2,7 +2,9 @@
import {
CardId,
+ ChooseCardArgs,
Effect,
+ EffectSource,
EngineNode,
EventCard,
FactionId,
@@ -14,6 +16,7 @@ import {
LeafNode,
Player,
PlayerCard,
+ PlayerTurnArgs,
SeqNode,
States,
View,
@@ -219,10 +222,10 @@ const seq_node = 's';
const function_node = 'f';
const resolved = 1;
-function create_leaf_node(
+function create_leaf_node<T = any>(
state: string,
faction: FactionId | 'None',
- args?: any
+ args?: T
): LeafNode {
return {
t: leaf_node,
@@ -260,6 +263,7 @@ function checkpoint() {
}
function setup_bag_of_glory() {
+ log_h1('Bag of Glory');
game.engine = [
create_leaf_node('add_glory', game.initiative),
create_function_node('end_of_turn'),
@@ -339,6 +343,7 @@ function start_of_player_turn() {
const player = faction_player_map[args.f];
log_h2(player, player);
game.faction_turn = args.f;
+ game.played_card = game.selected_cards[args.f][0];
resolve_active_and_proceed();
}
@@ -347,6 +352,7 @@ const engine_functions: Record<string, Function> = {
checkpoint,
end_of_player_turn,
end_of_turn,
+ end_resolving_event_effects,
setup_bag_of_glory,
setup_choose_card,
setup_final_bid,
@@ -400,6 +406,22 @@ function get_active_node(
return a === null ? null : a.node;
}
+function get_nodes_for_state(
+ state: string,
+ engine: EngineNode[] = game.engine
+): LeafNode[] {
+ let nodes = [];
+ for (let i of engine) {
+ if (i.t === leaf_node && i.s === state) {
+ nodes.push(i);
+ }
+ if (i.t === seq_node) {
+ nodes = nodes.concat(get_nodes_for_state(state, i.c));
+ }
+ }
+ return nodes;
+}
+
function get_active_node_args<T = any>(): T {
const node = get_active_node(game.engine);
if (node.t === leaf_node || node.t === function_node) {
@@ -408,7 +430,7 @@ function get_active_node_args<T = any>(): T {
return null;
}
-function update_active_node_args<T = any>(args: T) {
+function update_active_node_args<T = any>(args: Partial<T>) {
const node = get_active_node(game.engine);
if (node.t === leaf_node || node.t === function_node) {
node.a = {
@@ -471,6 +493,9 @@ function next() {
return;
}
game.active = next_active;
+ if (states[game.state].auto_resolve && states[game.state].auto_resolve()) {
+ resolve_active_and_proceed();
+ }
}
}
@@ -505,14 +530,20 @@ function game_view(state: Game, current: Player | 'Observer') {
engine: game.engine, // TODO: remove
log: game.log,
prompt: null,
+ state: game.state,
bag_of_glory: game.bag_of_glory,
+ bag_of_glory_count: game.bag_of_glory.length,
bonuses: game.bonuses,
current,
+ current_player_faction: faction,
current_events: game.current_events,
first_player: game.first_player,
fronts: game.fronts,
glory: game.glory,
hand: faction === null ? [] : game.hands[faction],
+ discard: faction === null ? [] : game.discard[faction],
+ trash: faction === null ? [] : game.trash[faction],
+ deck: faction === null ? [] : list_deck(faction),
hero_points: game.hero_points,
initiative: game.initiative,
medallions: game.medallions,
@@ -700,7 +731,9 @@ function start_turn() {
log_h2('Fascist Event', 'fascist');
log(card.title);
- game.engine = card.effects.map((effect) => resolve_effect(effect));
+ game.engine = card.effects.map((effect) =>
+ resolve_effect(effect, 'fascist_event')
+ );
if (game.year === 3 && game.turn === 4) {
game.engine.push(create_function_node('setup_final_bid'));
} else {
@@ -914,6 +947,9 @@ states.add_to_front = {
for (let f of possible_fronts) {
gen_action_front(f);
}
+ if (args.src) {
+ view.prompt = add_prompt_prefix(view.prompt, get_source_name(args.src));
+ }
},
spend_hp() {
resolve_spend_hp();
@@ -929,7 +965,7 @@ states.attack_front = {
inactive: 'attack a Front',
prompt() {
gen_spend_hero_points();
- const { t: target, n } = get_active_node_args();
+ const { t: target, n, src } = get_active_node_args();
let fronts: Array<FrontId> = [];
@@ -949,6 +985,18 @@ states.attack_front = {
? `Attack ${front_names[fronts[0]]}`
: 'Attack a front';
+ let prefix = '';
+ if (src) {
+ prefix = `${get_source_name(src)}: `;
+ }
+ if (fronts.length === 1) {
+ view.prompt = prefix
+ ? `${prefix}attack ${front_names[fronts[0]]}`
+ : `Attack ${front_names[fronts[0]]}`;
+ } else {
+ view.prompt = prefix ? `${prefix}attack a front` : `Attack a front`;
+ }
+
fronts.forEach((id) => gen_action('front', id));
},
spend_hp() {
@@ -1109,7 +1157,12 @@ states.choose_card = {
inactive: 'choose a card',
prompt() {
gen_spend_hero_points();
+ const { src } = get_active_node_args<ChooseCardArgs>();
view.prompt = 'Choose a card to play this turn';
+ if (src === 'momentum') {
+ view.prompt = 'Choose a card to play';
+ }
+
const faction = get_active_faction();
const hand = game.hands[faction];
for (let c of hand) {
@@ -1124,6 +1177,10 @@ states.choose_card = {
card(c: CardId) {
const faction = get_active_faction();
game.selected_cards[faction].push(c);
+ const { src } = get_active_node_args<ChooseCardArgs>();
+ if (src === 'momentum') {
+ game.played_card = game.selected_cards[faction][0];
+ }
resolve_active_and_proceed();
},
};
@@ -1160,6 +1217,50 @@ states.choose_final_bid = {
},
};
+function setup_momentum() {
+ const faction = get_active_faction();
+
+ // Player received medallion outside of their normal turn
+ // and must be resolved
+ if (game.faction_turn !== faction) {
+ insert_after_active_node(
+ resolve_effect(create_effect('play_card', faction, 1), 'momentum')
+ );
+ return;
+ }
+
+ // Player gets medallion during their turn. Need to check if it can be player
+ // right away or not. Depends on whether card for this turn has been fully resolved or not
+
+ // Get player turn node
+ const node: LeafNode<PlayerTurnArgs> = get_nodes_for_state('player_turn')[0];
+ console.log('node', node);
+
+ const player_needs_to_play_card = game.selected_cards[faction].length > 0;
+ const { use_ap, use_morale_bonus, resolving_event } =
+ node.a ?? ({} as PlayerTurnArgs);
+ // TO CHECK: or event needs to be fulle resolved
+ if (
+ player_needs_to_play_card ||
+ use_ap ||
+ use_morale_bonus ||
+ resolving_event
+ ) {
+ // Player hasn't fully resolved this turns card. Update args to enable button
+ node.a = {
+ ...(node.a || {}),
+ use_momentum: true,
+ };
+ } else {
+ // Player can resolve choosing a new card
+ insert_before_active_node(
+ create_leaf_node<ChooseCardArgs>('choose_card', faction, {
+ src: 'momentum',
+ })
+ );
+ }
+}
+
states.choose_medallion = {
inactive: 'choose a medallion',
prompt() {
@@ -1193,7 +1294,7 @@ states.choose_medallion = {
gain_hero_points(faction, 7);
break;
case 2:
- insert_after_active_node(create_leaf_node('choose_card', faction));
+ setup_momentum();
break;
default:
game.medallions[faction].push(m);
@@ -1394,6 +1495,10 @@ states.move_track = {
view.prompt = `Move ${name} away from center`;
}
+ if (node.a.src) {
+ view.prompt = add_prompt_prefix(view.prompt, get_source_name(node.a.src));
+ }
+
if (track === LIBERTY_OR_COLLECTIVIZATION) {
gen_action_standee(LIBERTY);
gen_action_standee(COLLECTIVIZATION);
@@ -1544,43 +1649,82 @@ function resolve_spend_hp() {
next();
}
+function set_player_turn_prompt({
+ can_play_card,
+ can_spend_hp,
+ use_ap,
+ use_momentum,
+ use_morale_bonus,
+}: PlayerTurnArgs & { can_spend_hp: boolean; can_play_card: boolean }) {
+ console.log('set_player_turn_prompt', {
+ can_play_card,
+ use_ap,
+ use_morale_bonus,
+ use_momentum,
+ });
+ if (can_play_card && can_spend_hp) {
+ view.prompt = 'Play a card or spend Hero points';
+ } else if (can_play_card && !can_spend_hp) {
+ view.prompt = 'Play a card';
+ } else if (use_ap || use_morale_bonus || use_momentum) {
+ const text_options = [];
+ if (use_ap) {
+ text_options.push('Action Points');
+ }
+ if (use_morale_bonus) {
+ text_options.push('Morale Bonus');
+ }
+
+ if (can_spend_hp) {
+ text_options.push('spend Hero points');
+ }
+
+ if (use_momentum) {
+ view.prompt = can_spend_hp
+ ? 'Play second card or spend Hero Points'
+ : 'Play second card';
+ } else {
+ view.prompt = `Use ${text_options.join(', ')} or end turn`;
+ }
+ } else if (can_spend_hp) {
+ view.prompt = 'Spend Hero Points or end turn';
+ } else {
+ view.prompt = 'End turn';
+ }
+}
+
states.player_turn = {
inactive: 'play their turn',
prompt() {
gen_spend_hero_points();
const faction_id = get_active_faction();
- const { use_ap, use_morale_bonus } = get_active_node_args();
+ let { use_ap, use_morale_bonus, use_momentum } =
+ get_active_node_args<PlayerTurnArgs>();
+
+ use_morale_bonus = use_morale_bonus && game.bonuses[MORALE_BONUS] === ON;
const can_spend_hp =
game.faction_turn === faction_id && game.hero_points[faction_id] > 0;
const can_play_card = game.selected_cards[faction_id].length > 0;
-
- if (can_play_card && can_spend_hp) {
- view.prompt = 'Play a card or spend Hero points';
- } else if (can_play_card && !can_spend_hp) {
- view.prompt = 'Play a card';
- } else if (use_ap || use_morale_bonus) {
- const text_options = [];
- if (use_ap) {
- text_options.push('Action Points');
- }
- if (use_morale_bonus) {
- text_options.push('Morale Bonus');
+ console.log('can_play_card', can_play_card);
+ if (use_momentum) {
+ gen_action('use_momentum');
+ if (use_ap || use_morale_bonus || can_play_card) {
+ view.actions['use_momentum'] = 0;
}
- if (can_spend_hp) {
- text_options.push('spend Hero points');
- }
-
- view.prompt = `Use ${text_options.join(', ')} or end turn`;
- } else if (can_spend_hp) {
- view.prompt = 'Spend Hero Points or end turn';
- } else {
- view.prompt = 'End turn';
}
+ set_player_turn_prompt({
+ can_play_card,
+ can_spend_hp,
+ use_ap,
+ use_momentum,
+ use_morale_bonus,
+ });
+
if (can_play_card) {
- gen_action('play_for_ap');
+ gen_action('play_to_tableau');
gen_action('play_for_event');
}
if (use_ap) {
@@ -1589,22 +1733,22 @@ states.player_turn = {
if (use_morale_bonus && game.bonuses[MORALE_BONUS] === ON) {
gen_action('use_morale_bonus');
}
- if (!can_play_card && !use_ap) {
- gen_action('done');
+ if (!(can_play_card || use_ap || use_morale_bonus || use_momentum)) {
+ gen_action('end_turn');
}
},
spend_hp() {
resolve_spend_hp();
},
- done() {
+ end_turn() {
game.faction_turn = null;
game.played_card = null;
resolve_active_and_proceed(true);
},
- play_for_ap() {
+ play_to_tableau() {
const faction = get_active_faction();
- const { strength } = play_card(faction, 'play_for_ap');
- update_active_node_args({
+ const { strength } = play_card(faction, 'play_to_tableau');
+ update_active_node_args<PlayerTurnArgs>({
use_morale_bonus: true,
use_ap: true,
strength,
@@ -1614,8 +1758,13 @@ states.player_turn = {
play_for_event() {
const faction = get_active_faction();
const { effects } = play_card(faction, 'play_for_event');
+ update_active_node_args<PlayerTurnArgs>({
+ resolving_event: true,
+ });
- insert_before_active_node(create_effects_node(effects));
+ const node = create_effects_node(effects);
+ node.c.push(create_function_node('end_resolving_event_effects'));
+ insert_before_active_node(node);
next();
},
@@ -1632,6 +1781,17 @@ states.player_turn = {
);
next();
},
+ use_momentum() {
+ // We need to update since there can be a case where
+ // morale bonus hasn't been used yet but is still set to true
+ // due to bonus being turned off.
+ update_active_node_args<PlayerTurnArgs>({
+ use_morale_bonus: false,
+ use_momentum: false,
+ });
+ setup_momentum();
+ next();
+ },
use_morale_bonus() {
// Update args before inserting node before current node,
// otherwise it will update args of inserted node
@@ -1722,7 +1882,7 @@ states.remove_attack_from_fronts = {
const removed_value =
card_id === 6 ? 1 : Math.min(3, Math.abs(game.fronts[id].value));
- update_front(id, removed_value);
+ update_front(id, removed_value, get_active_faction());
const fronts = f ?? {};
fronts[id] = removed_value;
@@ -1790,33 +1950,65 @@ states.return_card = {
states.spend_hero_points = {
inactive: 'spend Hero points',
+ auto_resolve() {
+ const hero_points = game.hero_points[get_active_faction()];
+ if (hero_points === 0) {
+ return true;
+ }
+ return false;
+ },
prompt() {
view.prompt = 'Spend your Hero points';
- gen_action('done');
+
const faction = get_active_faction();
+ const { move_track, turn_on_bonus } = get_active_node_args();
+
+ if (move_track) {
+ view.prompt = 'Spend Hero points: move a Track';
+ } else if (turn_on_bonus) {
+ view.prompt = 'Spend Hero points: turn on a Bonus';
+ }
+
+ if (!(move_track || turn_on_bonus)) {
+ gen_action('done');
+ }
const hero_points = game.hero_points[get_active_faction()];
if (hero_points === 0) {
return;
}
- gen_action('draw_card');
- if (can_use_medallion(ARCHIVES_MEDALLION_ID, faction)) {
- gen_action('remove_blank_marker');
- }
- if (can_use_medallion(VOLUNTEERS_MEDALLION_ID, faction)) {
- gen_action('add_to_front');
+ if (!(move_track || turn_on_bonus)) {
+ gen_action('draw_card');
+ if (can_use_medallion(ARCHIVES_MEDALLION_ID, faction)) {
+ gen_action('remove_blank_marker');
+ }
+ if (can_use_medallion(VOLUNTEERS_MEDALLION_ID, faction)) {
+ gen_action('add_to_front');
+ }
}
if (hero_points < 2) {
return;
}
- gen_action_standee(FOREIGN_AID);
- gen_action_standee(SOVIET_SUPPORT);
+ if (!(move_track || turn_on_bonus)) {
+ gen_action('move_track');
+ }
+
for (const bonus of bonuses) {
- if (game.bonuses[bonus] === OFF) {
+ let bonus_off = false;
+ if (!move_track && game.bonuses[bonus] === OFF) {
gen_action_bonus(bonus);
+ bonus_off = true;
+ }
+ if (bonus_off && !turn_on_bonus) {
+ gen_action('turn_on_bonus');
}
}
+ if (turn_on_bonus) {
+ return;
+ }
+ gen_action_standee(FOREIGN_AID);
+ gen_action_standee(SOVIET_SUPPORT);
if (hero_points < 3) {
return;
}
@@ -1846,13 +2038,24 @@ states.spend_hero_points = {
resolve_active_and_proceed();
},
bonus(b: number) {
+ update_active_node_args({
+ turn_on_bonus: false,
+ });
update_bonus(b, ON);
pay_hero_points(get_active_faction(), 2);
+ next();
},
draw_card() {
const faction = get_active_faction();
pay_hero_points(faction, 1);
draw_hand_cards(faction, 1);
+ next();
+ },
+ move_track() {
+ update_active_node_args({
+ move_track: true,
+ });
+ next();
},
remove_blank_marker() {
const faction = get_active_faction();
@@ -1870,6 +2073,9 @@ states.spend_hero_points = {
resolve_active_and_proceed();
},
standee(track_id: number) {
+ update_active_node_args({
+ move_track: false,
+ });
let amount = 2;
if (track_id === LIBERTY || track_id === COLLECTIVIZATION) {
amount = 3;
@@ -1889,6 +2095,12 @@ states.spend_hero_points = {
);
resolve_active_and_proceed();
},
+ turn_on_bonus() {
+ update_active_node_args({
+ turn_on_bonus: true,
+ });
+ next();
+ },
};
states.swap_card_tableau_hand = {
@@ -1981,11 +2193,19 @@ states.take_hero_points = {
? 'Choose a player to take a Hero Point from'
: `Choose a player to take ${v} Hero Points from`;
const active_faction = get_active_faction();
+ let target_exists = false;
for (const faction of role_ids) {
- if (faction !== active_faction) {
+ if (faction !== active_faction && game.hero_points[faction] > 0) {
gen_action(faction_player_map[faction]);
+ target_exists = true;
}
}
+
+ if (!target_exists) {
+ view.prompt =
+ 'Not possible to take Hero Points from another player. You must skip';
+ gen_action('skip');
+ }
},
spend_hp() {
resolve_spend_hp();
@@ -1999,6 +2219,9 @@ states.take_hero_points = {
Moderate() {
resolve_take_hero_points(MODERATES_ID);
},
+ skip() {
+ resolve_active_and_proceed();
+ },
};
states.use_organization_medallion = {
@@ -2213,14 +2436,13 @@ function add_glory(
amount: number,
indent: boolean = false
) {
+ let tokens_log = '';
for (let i = 0; i < amount; ++i) {
game.bag_of_glory.push(get_active_faction());
+ tokens_log += `<ft${faction}>`;
}
- let text =
- amount === 1
- ? `${faction_player_map[faction]} adds 1 token to the Bag of Glory`
- : `${faction_player_map[faction]} adds ${amount} tokens to the Bag of Glory`;
+ let text = `${faction_player_map[faction]} adds ${tokens_log} to the Bag of Glory`;
if (indent) {
logi(text);
@@ -2314,20 +2536,30 @@ function end_of_year() {
resolve_active_and_proceed();
return;
}
+ } else {
+ log_h1('End of year');
}
+
const glory_to_draw = [0, 1, 2, 5];
const glory_this_year: Record<FactionId, boolean> = {
a: false,
c: false,
m: false,
};
+ const drawn_glory = [];
for (let i = 0; i < glory_to_draw[game.year]; ++i) {
const index = random(game.bag_of_glory.length);
const faction = game.bag_of_glory[index];
game.glory.push(faction);
+ drawn_glory.push(faction);
glory_this_year[faction] = true;
array_remove(game.bag_of_glory, index);
}
+ log(
+ `Tokens pulled from the Bag of Glory: ${drawn_glory
+ .map((faction_id: string) => `<ft${faction_id}>`)
+ .join('')}`
+ );
if (game.year === 3) {
// end of game
@@ -2354,6 +2586,18 @@ function end_of_year() {
next();
}
+function end_resolving_event_effects() {
+ // Get player turn node
+ const node: LeafNode<PlayerTurnArgs> = get_nodes_for_state('player_turn')[0];
+
+ // Update args
+ node.a = {
+ ...(node.a || {}),
+ resolving_event: false,
+ };
+ resolve_active_and_proceed();
+}
+
function gain_hero_points_in_player_order(factions: FactionId[], value) {
for (const f of get_player_order()) {
if (factions.includes(f)) {
@@ -2422,22 +2666,20 @@ function get_hand_limit(faction: FactionId) {
function play_card(
faction: FactionId,
- type: 'play_for_event' | 'play_for_ap'
+ type: 'play_for_event' | 'play_to_tableau'
): PlayerCard {
const index = game.selected_cards[faction].length - 1;
const card_id = game.selected_cards[faction][index];
const card = cards[card_id];
game.played_card = card_id;
- log_h3(
- `${game.active} plays ${card.title} for the ${
- type === 'play_for_event' ? 'Event' : 'Action Points'
- }`
- );
+
array_remove(game.hands[faction], game.hands[faction].indexOf(card_id));
array_remove(game.selected_cards[faction], index);
if (type === 'play_for_event') {
+ log_h3(`${game.active} plays ${card.title} for the Event`);
game.trash[faction].push(card_id);
} else {
+ log_h3(`${game.active} plays ${card.title} to their Tableau`);
game.tableaus[faction].push(card_id);
}
return card as PlayerCard;
@@ -2471,7 +2713,7 @@ function resolve_fascist_test() {
}
const effect = test_passed ? test.pass : test.fail;
- const node = resolve_effect(effect);
+ const node = resolve_effect(effect, 'fascist_test');
if (node !== null) {
insert_after_active_node(node);
@@ -2600,7 +2842,7 @@ function move_track(track_id: number, change: number) {
get_blank_marker_id(track_id, space_id)
);
}
- const node = resolve_effect(trigger);
+ const node = resolve_effect(trigger, 'track_icon');
if (node !== null) {
insert_after_active_node(node);
}
@@ -2731,7 +2973,7 @@ function victory_on_a_front(front_id: FrontId) {
gain_hero_points_in_player_order(game.fronts[front_id].contributions, 3);
}
-function create_effects_node(effects: Effect[]): EngineNode {
+function create_effects_node(effects: Effect[]): SeqNode {
const nodes = effects.reduce((accrued: EngineNode[], current: Effect) => {
const node = resolve_effect(current);
if (node !== null) {
@@ -2765,13 +3007,11 @@ const effect_type_state_map: Record<string, string> = {
track: 'move_track',
};
-function resolve_effect(
- effect: Effect
- // faction: FactionId = get_active_faction() //
-): EngineNode {
+function resolve_effect(effect: Effect, source?: EffectSource): EngineNode {
const args = {
t: effect.target,
v: effect.value,
+ src: source,
};
const faction = get_faction_to_resolve_effect(effect);
@@ -2782,6 +3022,7 @@ function resolve_effect(
if (effect.type === 'state') {
return create_leaf_node(effect.target as string, faction, {
v: effect.value,
+ src: source,
});
}
// Default cases where effect type is mapped to a state
@@ -2809,9 +3050,7 @@ function resolve_effect(
resolve: () => {
return create_seq_node(
get_player_order().map((faction) =>
- create_leaf_node('hero_points', faction, {
- v: effect.value,
- })
+ create_leaf_node('hero_points', faction, args)
)
);
},
@@ -2867,9 +3106,7 @@ function resolve_effect(
resolve: () => {
return create_seq_node(
get_player_order(get_active_faction()).map((faction) =>
- create_leaf_node('draw_card', faction, {
- v: effect.value,
- })
+ create_leaf_node('draw_card', faction, args)
)
);
},
@@ -2878,12 +3115,9 @@ function resolve_effect(
condition: effect.type === 'draw_card' && effect.target === OTHER_PLAYERS,
resolve: () => {
const leaf_nodes = get_player_order(get_active_faction()).map(
- (faction) =>
- create_leaf_node('draw_card', faction, {
- v: effect.value,
- })
+ (faction) => create_leaf_node('draw_card', faction, args)
);
- array_remove(leaf_nodes, 0);
+ array_remove(leaf_nodes, 0); // Remove current player
return create_seq_node(leaf_nodes);
},
},
@@ -2891,8 +3125,8 @@ function resolve_effect(
condition: effect.type === 'play_card',
resolve: () => {
return create_seq_node([
- create_leaf_node('choose_card', faction),
- create_leaf_node('player_turn', faction),
+ create_leaf_node('choose_card', faction, { src: source }),
+ create_leaf_node('player_turn', faction, { src: source }),
]);
},
},
@@ -3049,6 +3283,14 @@ function log_h3(msg: string) {
// #region UTILITY
+function lowerCaseFirstLetter(val: string) {
+ return String(val).charAt(0).toLowerCase() + String(val).slice(1);
+}
+
+function add_prompt_prefix(prompt: string, prefix: string) {
+ return `${prefix}: ${lowerCaseFirstLetter(prompt)}`;
+}
+
function get_active_faction(): FactionId {
return player_faction_map[game.active];
}
@@ -3150,6 +3392,16 @@ function get_player_order_in_game(
return order;
}
+function get_source_name(source: EffectSource): string {
+ const prefix_map: Record<EffectSource, string> = {
+ fascist_event: 'Fascist Event',
+ fascist_test: 'Fascist Test',
+ track_icon: 'Track Trigger',
+ momentum: 'Momentum',
+ };
+ return prefix_map[source];
+}
+
function get_factions_with_most_hero_poins(): FactionId[] {
let most_hero_points = null;
let faction_ids = [];