summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data.js2
-rw-r--r--data.ts2
-rw-r--r--land-and-freedom.css46
-rw-r--r--land-and-freedom.scss52
-rw-r--r--play.html15
-rw-r--r--play.js68
-rw-r--r--play.ts82
-rw-r--r--rules.js333
-rw-r--r--rules.ts408
-rw-r--r--types.d.ts62
10 files changed, 898 insertions, 172 deletions
diff --git a/data.js b/data.js
index 4882c14..2a1d9bf 100644
--- a/data.js
+++ b/data.js
@@ -1686,7 +1686,7 @@ const data = {
test: {
front: MADRID,
value: 0,
- pass: create_effect('play_card', INITIATIVE_PLAYER, 1),
+ pass: create_effect('play_card', INITIATIVE_PLAYER, 1, INITIATIVE_PLAYER),
fail: create_effect('bonus', MORALE_BONUS, OFF),
},
title: 'MILITARY DICTATORSHIP',
diff --git a/data.ts b/data.ts
index d22a005..10c0856 100644
--- a/data.ts
+++ b/data.ts
@@ -1753,7 +1753,7 @@ const data: StaticData = {
test: {
front: MADRID,
value: 0,
- pass: create_effect('play_card', INITIATIVE_PLAYER, 1),
+ pass: create_effect('play_card', INITIATIVE_PLAYER, 1, INITIATIVE_PLAYER),
fail: create_effect('bonus', MORALE_BONUS, OFF),
},
title: 'MILITARY DICTATORSHIP',
diff --git a/land-and-freedom.css b/land-and-freedom.css
index 09c834b..0fb77dc 100644
--- a/land-and-freedom.css
+++ b/land-and-freedom.css
@@ -26,6 +26,10 @@ main {
background-image: url(images/map100.png);
}
}
+#current_events .card:last-child {
+ transform: scale(1.2);
+}
+
/* CURRENT CARD */
#turn_info {
border-bottom: 1px solid black;
@@ -133,6 +137,10 @@ main {
min-height: 260px;
}
+.panel_body[data-active=inactive] {
+ display: none;
+}
+
.panel_header {
color: floralwhite;
user-select: none;
@@ -142,18 +150,41 @@ main {
background-color: red;
}
-#hand_header {
+#player_area_header {
background-color: #273b09;
+ display: flex;
+ border-bottom: none;
+}
+#player_area_header .player_area_tab {
+ cursor: pointer;
+ width: 25%;
+ border-bottom: 2px solid #333;
+ border-left: 1px solid #333;
+ border-right: 1px solid #333;
+}
+#player_area_header .player_area_tab[data-active=active] {
+ width: 25%;
+ background-color: #58641d;
+ border-bottom: none;
+}
+#player_area_header #hand_tab {
+ border-left: none;
+}
+#player_area_header #trash_tab {
+ border-right: none;
}
+#player_area_header[data-faction-id=a],
.panel_header[data-faction-id=a] {
background-color: rgb(93, 89, 106);
}
+#player_area_header[data-faction-id=c],
.panel_header[data-faction-id=c] {
background-color: rgb(237, 36, 27);
}
+#player_area_header[data-faction-id=m],
.panel_header[data-faction-id=m] {
background-color: rgb(134, 44, 97);
}
@@ -661,6 +692,15 @@ main {
background-size: 100% 100%;
}
+#log .faction_token {
+ display: inline-block;
+ width: 25px;
+ height: 25px;
+ border-radius: 2px;
+ vertical-align: middle;
+ margin: 0px 1px;
+}
+
#glory .faction_token {
position: absolute;
width: 34px;
@@ -710,7 +750,7 @@ main {
.front.action,
.medallion.action,
.standee.action {
- box-shadow: 0 0 0 3px white;
+ box-shadow: 0 0 0 3px yellow;
}
.blank_marker.action {
@@ -724,7 +764,7 @@ main {
.front.action:hover,
.medallion.action:hover,
.standee.action:hover {
- box-shadow: 0 0 0 3px yellow;
+ box-shadow: 0 0 0 3px blue;
}
.blank_marker.action:hover {
diff --git a/land-and-freedom.scss b/land-and-freedom.scss
index 14ede73..07c55b2 100644
--- a/land-and-freedom.scss
+++ b/land-and-freedom.scss
@@ -1,8 +1,8 @@
// @use "sass:math";
@use 'sass:map';
-$selectable-color: white; // yellow;
-$selected-color: yellow; //blue;
+$selectable-color: yellow; // yellow;
+$selected-color: blue; //blue;
$anarchist-color: rgb(93, 89, 106);
$communist-color: rgb(237, 36, 27);
@@ -50,6 +50,12 @@ main {
}
}
+#current_events {
+ .card:last-child {
+ transform: scale(1.2);
+ }
+}
+
/* CURRENT CARD */
#turn_info {
@@ -172,6 +178,10 @@ main {
min-height: 260px;
}
+.panel_body[data-active="inactive"] {
+ display: none;
+}
+
.panel_header {
color: floralwhite;
user-select: none;
@@ -181,18 +191,45 @@ main {
background-color: red;
}
-#hand_header {
+#player_area_header {
background-color: #273b09;
+ display: flex;
+ border-bottom: none;
+
+ .player_area_tab {
+ cursor: pointer;
+ width: 25%;
+ border-bottom: 2px solid #333;
+ border-left: 1px solid #333;
+ border-right: 1px solid #333;
+ }
+
+ .player_area_tab[data-active="active"] {
+ width: 25%;
+ background-color: #58641d;
+ border-bottom: none;
+ }
+
+ #hand_tab {
+ border-left: none;
+ }
+
+ #trash_tab {
+ border-right: none;
+ }
}
+#player_area_header[data-faction-id='a'],
.panel_header[data-faction-id='a'] {
background-color: $anarchist-color;
}
+#player_area_header[data-faction-id='c'],
.panel_header[data-faction-id='c'] {
background-color: $communist-color;
}
+#player_area_header[data-faction-id='m'],
.panel_header[data-faction-id='m'] {
background-color: $moderate-color;
}
@@ -303,6 +340,15 @@ main {
// box-shadow: 0 0 0 1px #333;
}
+#log .faction_token {
+ display: inline-block;
+ width: 25px;
+ height: 25px;
+ border-radius: 2px;
+ vertical-align: middle;
+ margin: 0px 1px;
+}
+
#glory .faction_token {
position: absolute;
width: 34px;
diff --git a/play.html b/play.html
index c9fc7cd..b22fc41 100644
--- a/play.html
+++ b/play.html
@@ -35,6 +35,7 @@
<div class="game_info">
<span id="year"></span>
<span id="pool_hero_points"></span>
+ <span id="bag_of_glory"></span>
</div>
<div id="roles">
<div id="role_Anarchist" class="role">
@@ -96,9 +97,17 @@
</div>
</div>
<div id="selectable_cards"></div>
- <div id="hand_area" class="panel">
- <div id="hand_header" class="panel_header">Hand</div>
- <div id="hand" class="panel_body"></div>
+ <div id="player_area" class="panel">
+ <div id="player_area_header" class="panel_header">
+ <div id="hand_tab" data-active="active" class="player_area_tab">Hand</div>
+ <div id="deck_tab" data-active="inactive" class="player_area_tab">Deck</div>
+ <div id="discard_tab" data-active="inactive" class="player_area_tab">Discard Pile</div>
+ <div id="trash_tab" data-active="inactive" class="player_area_tab">Trash</div>
+ </div>
+ <div id="hand" data-active="active" class="panel_body"></div>
+ <div id="deck" data-active="inactive" class="panel_body"></div>
+ <div id="discard" data-active="inactive" class="panel_body"></div>
+ <div id="trash" data-active="inactive" class="panel_body"></div>
</div>
<div id="player_areas">
<div id="player_area_Anarchist" class="panel">
diff --git a/play.js b/play.js
index ec459e0..b55c6be 100644
--- a/play.js
+++ b/play.js
@@ -9,6 +9,7 @@ const TRACK_COUNT = 5;
const TRACK_LENGTH = 11;
const FACTIONS = ['a', 'c', 'm'];
const ui = {
+ bag_of_glory: document.getElementById('bag_of_glory'),
map: document.getElementById('map'),
medallions_container: document.getElementById('medallions'),
markers: document.getElementById('markers'),
@@ -36,7 +37,7 @@ const ui = {
},
glory_container: document.getElementById('glory'),
hand: document.getElementById('hand'),
- hand_area: document.getElementById('hand_area'),
+ player_area: document.getElementById('player_area'),
current_events: document.getElementById('current_events'),
roles: {
a: {
@@ -59,6 +60,18 @@ const ui = {
Moderate: document.getElementById('role_Moderate'),
container: document.getElementById('roles'),
},
+ player_area_tabs: {
+ hand_tab: document.getElementById('hand_tab'),
+ deck_tab: document.getElementById('deck_tab'),
+ discard_tab: document.getElementById('discard_tab'),
+ trash_tab: document.getElementById('trash_tab'),
+ },
+ player_area_cards: {
+ hand: document.getElementById('hand'),
+ deck: document.getElementById('deck'),
+ discard: document.getElementById('discard'),
+ trash: document.getElementById('trash'),
+ },
player_areas: {
container: document.getElementById('player_areas'),
Anarchist: document.getElementById('player_area_Anarchist'),
@@ -194,6 +207,21 @@ function on_click_action(evt) {
if (send_action(evt.target.my_action, evt.target.my_id))
evt.stopPropagation();
}
+function on_click_tab(evt) {
+ evt.stopPropagation();
+ const { id } = evt.target;
+ Object.entries(ui.player_area_tabs).forEach(([tab, elt]) => {
+ const cards_area = ui.player_area_cards[tab.split('_')[0]];
+ if (tab === id) {
+ elt.setAttribute('data-active', 'active');
+ cards_area.setAttribute('data-active', 'active');
+ }
+ else {
+ elt.setAttribute('data-active', 'inactive');
+ cards_area.setAttribute('data-active', 'inactive');
+ }
+ });
+}
function is_action(action, arg) {
if (arg === undefined)
return !!(view.actions && view.actions[action] === 1);
@@ -206,13 +234,21 @@ function on_init() {
if (on_init_once)
return;
on_init_once = true;
+ Object.values(ui.player_area_tabs).forEach((element) => {
+ element.addEventListener('click', on_click_tab);
+ });
for (const player of view.player_order) {
ui.player_areas.container.insertAdjacentElement('beforeend', ui.player_areas[player]);
ui.roles.container.insertAdjacentElement('beforeend', ui.roles[player]);
}
ui.roles.container.insertAdjacentElement('beforeend', ui.turn_info);
if (view.current === 'Observer') {
- ui.hand_area.style.display = 'none';
+ ui.player_area.style.display = 'none';
+ }
+ else {
+ document
+ .getElementById('player_area_header')
+ .setAttribute('data-faction-id', view.current_player_faction);
}
for (let t = 0; t < TRACK_COUNT; ++t) {
for (let s = 0; s < TRACK_LENGTH; ++s) {
@@ -289,6 +325,7 @@ function on_update() {
for (let key of Object.keys(view.hero_points)) {
ui.roles[key].hero_points.replaceChildren(`Hero Points: ${view.hero_points[key]}`);
}
+ ui.bag_of_glory.replaceChildren(`Bag of Glory: ${view.bag_of_glory_count}`);
ui.current_events.replaceChildren();
for (let i = 0; i < view.current_events.length; i++) {
const cardId = view.current_events[i];
@@ -309,6 +346,13 @@ function on_update() {
ui.bonuses[bonus_id].setAttribute('data-bonus-on', view.bonuses[bonus_id] + 0);
}
place_cards(ui.hand, view.hand);
+ ui.player_area_tabs.hand_tab.replaceChildren(`Hand (${view.hand.length})`);
+ place_cards(ui.player_area_cards.deck, view.deck);
+ ui.player_area_tabs.deck_tab.replaceChildren(`Deck (${view.deck.length})`);
+ place_cards(ui.player_area_cards.discard, view.discard);
+ ui.player_area_tabs.discard_tab.replaceChildren(`Discard (${view.discard.length})`);
+ place_cards(ui.player_area_cards.trash, view.trash);
+ ui.player_area_tabs.trash_tab.replaceChildren(`Trash (${view.trash.length})`);
place_cards(ui.selectable_cards, view.selectable_cards);
for (let faction_id of FACTIONS) {
place_cards(ui.tableaus[faction_id], view.tableaus[faction_id]);
@@ -349,6 +393,7 @@ function on_update() {
ui.turn_info.style.display = '';
ui.turn_info_card.setAttribute('data-card-id', view.played_card + '');
}
+ Object.values(ui.glory).forEach((elt) => elt.removeAttribute('data-faction-id'));
for (let g = 0; g < view.glory.length; ++g) {
ui.glory[g].setAttribute('data-faction-id', view.glory[g]);
}
@@ -375,10 +420,12 @@ function on_update() {
action_button('lose_hp', 'Lose Hero Points');
action_button('draw_card', 'Draw a card');
action_button('draw_cards', 'Draw cards');
- action_button('play_for_ap', 'Play card for Action Points');
+ action_button('play_to_tableau', 'Play card to Tableau');
action_button('play_for_event', 'Play card for Event');
action_button('use_ap', 'Use Action Points');
action_button('use_morale_bonus', 'Use Morale Bonus');
+ action_button('move_track', 'Move a Track');
+ action_button('turn_on_bonus', 'Turn on a Bonus');
action_button('add_glory', 'Add to Bag of Glory');
action_button('up', 'Up');
action_button('down', 'Down');
@@ -389,9 +436,14 @@ function on_update() {
action_button('no', 'No');
action_button('skip', 'Skip');
action_button('spend_hp', 'Spend Hero Points');
+ action_button('use_momentum', 'Play second card (Momentum)');
action_button('done', 'Done');
+ action_button('end_turn', 'End turn');
action_button('undo', 'Undo');
}
+const IMG_FTA = '<span class="faction_token" data-faction-id="a"></span>';
+const IMG_FTC = '<span class="faction_token" data-faction-id="c"></span>';
+const IMG_FTM = '<span class="faction_token" data-faction-id="m"></span>';
function on_log(text) {
let p = document.createElement('div');
if (text.match(/^>>/)) {
@@ -402,9 +454,6 @@ function on_log(text) {
text = text.substring(1);
p.className = 'i';
}
- text = text.replace(/&/g, '&amp;');
- text = text.replace(/</g, '&lt;');
- text = text.replace(/>/g, '&gt;');
if (text.match(/^\.h1/)) {
text = text.substring(4);
p.className = 'h1';
@@ -425,6 +474,10 @@ function on_log(text) {
text = text.substring(11);
p.className = 'h2 fascist';
}
+ else if (text.match(/^\.h2\.glory/)) {
+ text = text.substring(9);
+ p.className = 'h2 glory';
+ }
else if (text.match(/^\.h2/)) {
text = text.substring(4);
p.className = 'h2';
@@ -433,6 +486,9 @@ function on_log(text) {
text = text.substring(4);
p.className = 'h3';
}
+ text = text.replace(/<fta>/g, IMG_FTA);
+ text = text.replace(/<ftc>/g, IMG_FTC);
+ text = text.replace(/<ftm>/g, IMG_FTM);
p.innerHTML = text;
return p;
}
diff --git a/play.ts b/play.ts
index 31f9b65..adf9f9d 100644
--- a/play.ts
+++ b/play.ts
@@ -22,6 +22,7 @@ const FACTIONS = ['a', 'c', 'm'];
// const ROLES = ['Anarchist', 'Communist', 'Moderate'];
const ui = {
+ bag_of_glory: document.getElementById('bag_of_glory'),
map: document.getElementById('map'),
medallions_container: document.getElementById('medallions'),
markers: document.getElementById('markers'),
@@ -49,7 +50,7 @@ const ui = {
},
glory_container: document.getElementById('glory'),
hand: document.getElementById('hand'),
- hand_area: document.getElementById('hand_area'),
+ player_area: document.getElementById('player_area'),
current_events: document.getElementById('current_events'),
roles: {
a: {
@@ -72,6 +73,18 @@ const ui = {
Moderate: document.getElementById('role_Moderate'),
container: document.getElementById('roles'),
},
+ player_area_tabs: {
+ hand_tab: document.getElementById('hand_tab'),
+ deck_tab: document.getElementById('deck_tab'),
+ discard_tab: document.getElementById('discard_tab'),
+ trash_tab: document.getElementById('trash_tab'),
+ },
+ player_area_cards: {
+ hand: document.getElementById('hand'),
+ deck: document.getElementById('deck'),
+ discard: document.getElementById('discard'),
+ trash: document.getElementById('trash'),
+ },
player_areas: {
container: document.getElementById('player_areas'),
Anarchist: document.getElementById('player_area_Anarchist'),
@@ -244,6 +257,22 @@ function on_click_action(evt) {
evt.stopPropagation();
}
+function on_click_tab(evt) {
+ evt.stopPropagation();
+ const { id } = evt.target as HTMLElement;
+
+ Object.entries(ui.player_area_tabs).forEach(([tab, elt]) => {
+ const cards_area = ui.player_area_cards[tab.split('_')[0]];
+ if (tab === id) {
+ elt.setAttribute('data-active', 'active');
+ cards_area.setAttribute('data-active', 'active');
+ } else {
+ elt.setAttribute('data-active', 'inactive');
+ cards_area.setAttribute('data-active', 'inactive');
+ }
+ });
+}
+
function is_action(action, arg) {
if (arg === undefined) return !!(view.actions && view.actions[action] === 1);
return !!(
@@ -259,15 +288,26 @@ function on_init() {
if (on_init_once) return;
on_init_once = true;
+ Object.values(ui.player_area_tabs).forEach((element) => {
+ element.addEventListener('click', on_click_tab);
+ });
+
// Reorder tableaus and roles based on player order
for (const player of view.player_order) {
- ui.player_areas.container.insertAdjacentElement('beforeend', ui.player_areas[player]);
+ ui.player_areas.container.insertAdjacentElement(
+ 'beforeend',
+ ui.player_areas[player]
+ );
ui.roles.container.insertAdjacentElement('beforeend', ui.roles[player]);
}
ui.roles.container.insertAdjacentElement('beforeend', ui.turn_info);
if (view.current === 'Observer') {
- ui.hand_area.style.display = 'none';
+ ui.player_area.style.display = 'none';
+ } else {
+ document
+ .getElementById('player_area_header')
+ .setAttribute('data-faction-id', view.current_player_faction);
}
// Create blank_markers
@@ -346,7 +386,7 @@ function on_init() {
});
}
-function place_cards(e: HTMLElement, cards: CardId[]) {
+function place_cards(e: HTMLElement, cards: number[]) {
e.replaceChildren();
for (let c of cards) {
ui.cards[c].classList.remove('selected');
@@ -367,6 +407,7 @@ function on_update() {
`Hero Points: ${view.hero_points[key]}`
);
}
+ ui.bag_of_glory.replaceChildren(`Bag of Glory: ${view.bag_of_glory_count}`);
// for (let s = 0; s < SPACE_COUNT; ++s) ui.spaces[s].replaceChildren();
@@ -400,6 +441,14 @@ function on_update() {
}
place_cards(ui.hand, view.hand);
+ ui.player_area_tabs.hand_tab.replaceChildren(`Hand (${view.hand.length})`);
+ place_cards(ui.player_area_cards.deck, view.deck);
+ ui.player_area_tabs.deck_tab.replaceChildren(`Deck (${view.deck.length})`);
+ place_cards(ui.player_area_cards.discard, view.discard);
+ ui.player_area_tabs.discard_tab.replaceChildren(`Discard (${view.discard.length})`);
+ place_cards(ui.player_area_cards.trash, view.trash);
+ ui.player_area_tabs.trash_tab.replaceChildren(`Trash (${view.trash.length})`);
+
place_cards(ui.selectable_cards, view.selectable_cards);
for (let faction_id of FACTIONS) {
@@ -451,6 +500,7 @@ function on_update() {
ui.turn_info_card.setAttribute('data-card-id', view.played_card + '');
}
+ Object.values(ui.glory).forEach((elt: HTMLElement) => elt.removeAttribute('data-faction-id'));
for (let g = 0; g < view.glory.length; ++g) {
ui.glory[g].setAttribute('data-faction-id', view.glory[g]);
}
@@ -485,11 +535,14 @@ function on_update() {
action_button('draw_card', 'Draw a card');
action_button('draw_cards', 'Draw cards');
// action_button('draw_card', 'Draw card');
- action_button('play_for_ap', 'Play card for Action Points');
+ action_button('play_to_tableau', 'Play card to Tableau');
action_button('play_for_event', 'Play card for Event');
action_button('use_ap', 'Use Action Points');
action_button('use_morale_bonus', 'Use Morale Bonus');
+ action_button('move_track', 'Move a Track');
+ action_button('turn_on_bonus', 'Turn on a Bonus');
+
action_button('add_glory', 'Add to Bag of Glory');
action_button('up', 'Up');
action_button('down', 'Down');
@@ -500,10 +553,16 @@ function on_update() {
action_button('no', 'No');
action_button('skip', 'Skip');
action_button('spend_hp', 'Spend Hero Points');
+ action_button('use_momentum', 'Play second card (Momentum)');
action_button('done', 'Done');
+ action_button('end_turn', 'End turn');
action_button('undo', 'Undo');
}
+const IMG_FTA = '<span class="faction_token" data-faction-id="a"></span>';
+const IMG_FTC = '<span class="faction_token" data-faction-id="c"></span>';
+const IMG_FTM = '<span class="faction_token" data-faction-id="m"></span>';
+
// @ts-ignore
function on_log(text) {
let p = document.createElement('div');
@@ -518,9 +577,9 @@ function on_log(text) {
p.className = 'i';
}
- text = text.replace(/&/g, '&amp;');
- text = text.replace(/</g, '&lt;');
- text = text.replace(/>/g, '&gt;');
+ // text = text.replace(/&/g, '&amp;');
+ // text = text.replace(/</g, '&lt;');
+ // text = text.replace(/>/g, '&gt;');
// text = text.replace(/C(\d+)/g, sub_card_name)
// text = text.replace(/S(\d+)/g, sub_space_name)
// text = text.replace(/U(\d+)/g, sub_unit_name)
@@ -543,6 +602,9 @@ function on_log(text) {
} else if (text.match(/^\.h2\.fascist/)) {
text = text.substring(11);
p.className = 'h2 fascist';
+ } else if (text.match(/^\.h2\.glory/)) {
+ text = text.substring(9);
+ p.className = 'h2 glory';
} else if (text.match(/^\.h2/)) {
text = text.substring(4);
p.className = 'h2';
@@ -551,6 +613,10 @@ function on_log(text) {
p.className = 'h3';
}
+ text = text.replace(/<fta>/g, IMG_FTA)
+ text = text.replace(/<ftc>/g, IMG_FTC)
+ text = text.replace(/<ftm>/g, IMG_FTM)
+
p.innerHTML = text;
return p;
}
diff --git a/rules.js b/rules.js
index 4aa2491..6920a5e 100644
--- a/rules.js
+++ b/rules.js
@@ -126,6 +126,7 @@ function checkpoint() {
resolve_active_and_proceed();
}
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'),
@@ -187,6 +188,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();
}
const engine_functions = {
@@ -194,6 +196,7 @@ const engine_functions = {
checkpoint,
end_of_player_turn,
end_of_turn,
+ end_resolving_event_effects,
setup_bag_of_glory,
setup_choose_card,
setup_final_bid,
@@ -239,6 +242,18 @@ function get_active_node(engine = game.engine) {
const a = get_active(engine);
return a === null ? null : a.node;
}
+function get_nodes_for_state(state, engine = game.engine) {
+ 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() {
const node = get_active_node(game.engine);
if (node.t === leaf_node || node.t === function_node) {
@@ -292,6 +307,9 @@ function next() {
return;
}
game.active = next_active;
+ if (states[game.state].auto_resolve && states[game.state].auto_resolve()) {
+ resolve_active_and_proceed();
+ }
}
}
function resolve_active_node(checkpoint = false) {
@@ -314,14 +332,20 @@ function game_view(state, current) {
engine: game.engine,
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,
@@ -483,7 +507,7 @@ function start_turn() {
const card = cards[cardId];
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'));
}
@@ -687,6 +711,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();
@@ -701,7 +728,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 = [];
if (target === data_1.ANY) {
fronts = get_fronts_to_add_to(data_1.ANY, n);
@@ -722,6 +749,18 @@ states.attack_front = {
fronts.length === 1
? `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() {
@@ -869,7 +908,11 @@ states.choose_card = {
inactive: 'choose a card',
prompt() {
gen_spend_hero_points();
+ const { src } = get_active_node_args();
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) {
@@ -884,6 +927,10 @@ states.choose_card = {
card(c) {
const faction = get_active_faction();
game.selected_cards[faction].push(c);
+ const { src } = get_active_node_args();
+ if (src === 'momentum') {
+ game.played_card = game.selected_cards[faction][0];
+ }
resolve_active_and_proceed();
},
};
@@ -916,6 +963,31 @@ states.choose_final_bid = {
resolve_active_and_proceed(true);
},
};
+function setup_momentum() {
+ const faction = get_active_faction();
+ if (game.faction_turn !== faction) {
+ insert_after_active_node(resolve_effect((0, data_1.create_effect)('play_card', faction, 1), 'momentum'));
+ return;
+ }
+ const node = 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 ?? {};
+ if (player_needs_to_play_card ||
+ use_ap ||
+ use_morale_bonus ||
+ resolving_event) {
+ node.a = {
+ ...(node.a || {}),
+ use_momentum: true,
+ };
+ }
+ else {
+ insert_before_active_node(create_leaf_node('choose_card', faction, {
+ src: 'momentum',
+ }));
+ }
+}
states.choose_medallion = {
inactive: 'choose a medallion',
prompt() {
@@ -945,7 +1017,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);
@@ -1133,6 +1205,9 @@ states.move_track = {
else if (track === data_1.GOVERNMENT && value === data_1.AWAY_FROM_CENTER) {
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 === data_1.LIBERTY_OR_COLLECTIVIZATION) {
gen_action_standee(data_1.LIBERTY);
gen_action_standee(data_1.COLLECTIVIZATION);
@@ -1249,41 +1324,71 @@ function resolve_spend_hp() {
log('Spends Hero Points');
next();
}
+function set_player_turn_prompt({ can_play_card, can_spend_hp, use_ap, use_momentum, use_morale_bonus, }) {
+ 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();
+ use_morale_bonus = use_morale_bonus && game.bonuses[data_1.MORALE_BONUS] === data_1.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');
- }
- if (can_spend_hp) {
- text_options.push('spend Hero points');
+ 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;
}
- 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) {
@@ -1292,21 +1397,21 @@ states.player_turn = {
if (use_morale_bonus && game.bonuses[data_1.MORALE_BONUS] === data_1.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');
+ const { strength } = play_card(faction, 'play_to_tableau');
update_active_node_args({
use_morale_bonus: true,
use_ap: true,
@@ -1317,7 +1422,12 @@ states.player_turn = {
play_for_event() {
const faction = get_active_faction();
const { effects } = play_card(faction, 'play_for_event');
- insert_before_active_node(create_effects_node(effects));
+ update_active_node_args({
+ resolving_event: true,
+ });
+ const node = create_effects_node(effects);
+ node.c.push(create_function_node('end_resolving_event_effects'));
+ insert_before_active_node(node);
next();
},
use_ap() {
@@ -1331,6 +1441,14 @@ states.player_turn = {
}));
next();
},
+ use_momentum() {
+ update_active_node_args({
+ use_morale_bonus: false,
+ use_momentum: false,
+ });
+ setup_momentum();
+ next();
+ },
use_morale_bonus() {
update_active_node_args({
use_morale_bonus: false,
@@ -1394,7 +1512,7 @@ states.remove_attack_from_fronts = {
front(id) {
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);
+ update_front(id, removed_value, get_active_faction());
const fronts = f ?? {};
fronts[id] = removed_value;
update_active_node_args({ f: fronts });
@@ -1453,31 +1571,60 @@ 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(data_1.ARCHIVES_MEDALLION_ID, faction)) {
- gen_action('remove_blank_marker');
- }
- if (can_use_medallion(data_1.VOLUNTEERS_MEDALLION_ID, faction)) {
- gen_action('add_to_front');
+ if (!(move_track || turn_on_bonus)) {
+ gen_action('draw_card');
+ if (can_use_medallion(data_1.ARCHIVES_MEDALLION_ID, faction)) {
+ gen_action('remove_blank_marker');
+ }
+ if (can_use_medallion(data_1.VOLUNTEERS_MEDALLION_ID, faction)) {
+ gen_action('add_to_front');
+ }
}
if (hero_points < 2) {
return;
}
- gen_action_standee(data_1.FOREIGN_AID);
- gen_action_standee(data_1.SOVIET_SUPPORT);
+ if (!(move_track || turn_on_bonus)) {
+ gen_action('move_track');
+ }
for (const bonus of bonuses) {
- if (game.bonuses[bonus] === data_1.OFF) {
+ let bonus_off = false;
+ if (!move_track && game.bonuses[bonus] === data_1.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(data_1.FOREIGN_AID);
+ gen_action_standee(data_1.SOVIET_SUPPORT);
if (hero_points < 3) {
return;
}
@@ -1504,13 +1651,24 @@ states.spend_hero_points = {
resolve_active_and_proceed();
},
bonus(b) {
+ update_active_node_args({
+ turn_on_bonus: false,
+ });
update_bonus(b, data_1.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();
@@ -1527,6 +1685,9 @@ states.spend_hero_points = {
resolve_active_and_proceed();
},
standee(track_id) {
+ update_active_node_args({
+ move_track: false,
+ });
let amount = 2;
if (track_id === data_1.LIBERTY || track_id === data_1.COLLECTIVIZATION) {
amount = 3;
@@ -1545,6 +1706,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 = {
inactive: 'swap cards',
@@ -1616,11 +1783,18 @@ 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();
@@ -1634,6 +1808,9 @@ states.take_hero_points = {
Moderate() {
resolve_take_hero_points(data_1.MODERATES_ID);
},
+ skip() {
+ resolve_active_and_proceed();
+ },
};
states.use_organization_medallion = {
inactive: 'use Organization Medallion',
@@ -1783,12 +1960,12 @@ function setup_return_card_from_trash() {
resolve_active_and_proceed();
}
function add_glory(faction, amount, indent = 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);
}
@@ -1881,19 +2058,27 @@ function end_of_year() {
return;
}
}
+ else {
+ log_h1('End of year');
+ }
const glory_to_draw = [0, 1, 2, 5];
const glory_this_year = {
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) => `<ft${faction_id}>`)
+ .join('')}`);
if (game.year === 3) {
determine_winner();
return;
@@ -1906,6 +2091,14 @@ function end_of_year() {
game.top_of_events_deck = null;
next();
}
+function end_resolving_event_effects() {
+ const node = get_nodes_for_state('player_turn')[0];
+ node.a = {
+ ...(node.a || {}),
+ resolving_event: false,
+ };
+ resolve_active_and_proceed();
+}
function gain_hero_points_in_player_order(factions, value) {
for (const f of get_player_order()) {
if (factions.includes(f)) {
@@ -1954,13 +2147,14 @@ function play_card(faction, type) {
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;
@@ -1989,7 +2183,7 @@ function resolve_fascist_test() {
log('The Test is failed');
}
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);
}
@@ -2093,7 +2287,7 @@ function move_track(track_id, change) {
if (space_id !== 0) {
game.triggered_track_effects.push(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);
}
@@ -2216,10 +2410,11 @@ const effect_type_state_map = {
take_hero_points: 'take_hero_points',
track: 'move_track',
};
-function resolve_effect(effect) {
+function resolve_effect(effect, source) {
const args = {
t: effect.target,
v: effect.value,
+ src: source,
};
const faction = get_faction_to_resolve_effect(effect);
if (effect.type === 'function') {
@@ -2228,6 +2423,7 @@ function resolve_effect(effect) {
if (effect.type === 'state') {
return create_leaf_node(effect.target, faction, {
v: effect.value,
+ src: source,
});
}
let state = effect_type_state_map[effect.type];
@@ -2245,9 +2441,7 @@ function resolve_effect(effect) {
{
condition: effect.type === 'hero_points' && effect.target === data_1.ALL_PLAYERS,
resolve: () => {
- return create_seq_node(get_player_order().map((faction) => create_leaf_node('hero_points', faction, {
- v: effect.value,
- })));
+ return create_seq_node(get_player_order().map((faction) => create_leaf_node('hero_points', faction, args)));
},
},
{
@@ -2291,17 +2485,13 @@ function resolve_effect(effect) {
{
condition: effect.type === 'draw_card' && effect.target === data_1.ALL_PLAYERS,
resolve: () => {
- return create_seq_node(get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, {
- v: effect.value,
- })));
+ return create_seq_node(get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, args)));
},
},
{
condition: effect.type === 'draw_card' && effect.target === data_1.OTHER_PLAYERS,
resolve: () => {
- const leaf_nodes = get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, {
- v: effect.value,
- }));
+ const leaf_nodes = get_player_order(get_active_faction()).map((faction) => create_leaf_node('draw_card', faction, args));
array_remove(leaf_nodes, 0);
return create_seq_node(leaf_nodes);
},
@@ -2310,8 +2500,8 @@ function resolve_effect(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 }),
]);
},
},
@@ -2396,6 +2586,12 @@ function log_h3(msg) {
log_br();
log('.h3 ' + msg);
}
+function lowerCaseFirstLetter(val) {
+ return String(val).charAt(0).toLowerCase() + String(val).slice(1);
+}
+function add_prompt_prefix(prompt, prefix) {
+ return `${prefix}: ${lowerCaseFirstLetter(prompt)}`;
+}
function get_active_faction() {
return player_faction_map[game.active];
}
@@ -2470,6 +2666,15 @@ function get_player_order_in_game(first_player = game.initiative) {
}
return order;
}
+function get_source_name(source) {
+ const prefix_map = {
+ 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() {
let most_hero_points = null;
let faction_ids = [];
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 = [];
diff --git a/types.d.ts b/types.d.ts
index d68a7e9..d55f03e 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -79,17 +79,23 @@ export interface View {
log: number | string[];
active?: string | null;
prompt: string | null;
+ state: Game['state'];
actions?: any;
victory?: string;
current: Player | 'Observer';
- selected_cards: CardId[];
- bag_of_glory: Game['bag_of_glory'];
+ current_player_faction: FactionId | null;
+ selected_cards: number[];
+ bag_of_glory: Game['bag_of_glory']; // TODO: remove
+ bag_of_glory_count: number;
bonuses: Game['bonuses'];
current_events: CardId[];
first_player: Game['first_player'];
fronts: Game['fronts'];
glory: Game['glory'];
- hand: CardId[];
+ hand: number[];
+ discard: number[];
+ deck: number[];
+ trash: number[];
hero_points: Game['hero_points'];
initiative: Game['initiative'];
medallions: Game['medallions'];
@@ -121,11 +127,11 @@ export interface SeqNode {
c: EngineNode[];
}
-export interface LeafNode {
+export interface LeafNode<T = any> {
t: 'l';
s: string; // State
p: FactionId | 'None'; // Player
- a?: any; // args
+ a?: T; // args
r?: 0 | 1; // 1 if resolved
}
@@ -171,6 +177,8 @@ export interface PlayerCard extends CardBase {
icons: Icon[];
}
+export type EffectSource = 'fascist_event' | 'fascist_test' | 'track_icon' | 'momentum';
+
export interface Effect {
type:
| 'attack'
@@ -211,3 +219,47 @@ export interface StaticData {
triggers: Array<null | Effect>;
}>;
}
+
+// #region engine node args
+
+export interface EngineNodeArgsBase {
+ /**
+ * If set, node was added to engine as result of given effect.
+ * Used for prompts
+ */
+ src?: EffectSource;
+}
+
+export interface ChooseCardArgs extends EngineNodeArgsBase {
+}
+
+export interface PlayerTurnArgs extends EngineNodeArgsBase {
+ /**
+ * When set to true, player can use current card
+ * for action points (using for ap sets it to false)
+ */
+ use_ap?: boolean;
+ /**
+ * When set to true, player can use current card for
+ * morale bonus if that is active (using morale bonus sets it to dalse)
+ */
+ use_morale_bonus?: boolean;
+ /**
+ * Set when starting to resolve event effect from card. Will be set to false
+ * after all effects have been resolved. Used to determine if momentum medallion
+ * can be used.
+ */
+ resolving_event?: boolean;
+ /**
+ * Strength of the current card
+ */
+ strength?: number;
+ /**
+ * Set to true if player got momentum medallion,
+ * but was not able to resolve directly as they were still
+ * resolving their current card
+ */
+ use_momentum?: boolean;
+}
+
+// #endregion \ No newline at end of file