summaryrefslogtreecommitdiff
path: root/rules.ts
diff options
context:
space:
mode:
authorFrans Bongers <fransbongers@franss-mbp.home>2024-11-28 22:36:10 +0100
committerFrans Bongers <fransbongers@franss-mbp.home>2024-11-28 22:36:10 +0100
commit629206f773d5fd4c9247db03e3a705c4dcdc77c4 (patch)
tree692b809c4bfc01a452b3ae110310a9103a720b44 /rules.ts
parent9414fe91218a00fe9e44b48fdf40e51de5cb4479 (diff)
downloadland-and-freedom-629206f773d5fd4c9247db03e3a705c4dcdc77c4.tar.gz
setup game engine
Diffstat (limited to 'rules.ts')
-rw-r--r--rules.ts685
1 files changed, 580 insertions, 105 deletions
diff --git a/rules.ts b/rules.ts
index 389900d..49f4687 100644
--- a/rules.ts
+++ b/rules.ts
@@ -11,6 +11,12 @@ import data, {
// OFF,
ON,
EventCard,
+ CardEffect,
+ LIBERTY,
+ COLLECTIVIZATION,
+ GOVERNMENT,
+ SOVIET_SUPPORT,
+ FOREIGN_AID,
// StaticData,
// PLAYER_WITH_MOST_HERO_POINTS,
} from './data';
@@ -28,7 +34,7 @@ export type FactionId = Brand<string, 'FactionId'>;
interface Game {
[index: number]: any;
seed: number;
- log: number | string[];
+ log: string[];
undo: Game[];
turn: number;
year: number;
@@ -36,7 +42,9 @@ interface Game {
state: string | null;
bag_of_glory: Record<FactionId, number>;
bonuses: number[];
+ cards_in_play: Record<FactionId, CardId>;
current_events: CardId[];
+ engine: EngineNode[];
fronts: {
a: number;
m: number;
@@ -46,6 +54,7 @@ interface Game {
hands: Record<FactionId, CardId[]>;
hero_points: Record<FactionId, number>;
initiative: FactionId;
+ tableaus: Record<FactionId, CardId[]>;
tracks: number[];
result?: string;
@@ -54,12 +63,14 @@ interface Game {
location?: string;
selected?: string;
+ state_data: any;
// played_card: CardId
// turn: Turn
}
export interface View {
+ engine: Game['engine'];
log: number | string[];
active?: string | null;
prompt: string | null;
@@ -94,15 +105,46 @@ const ANARCHISTS_ID = 'a' as FactionId;
const COMMUNISTS_ID = 'c' as FactionId;
const MODERATES_ID = 'm' as FactionId;
+const role_ids = [ANARCHISTS_ID, COMMUNISTS_ID, MODERATES_ID];
+
+const faction_player_map: Record<FactionId, Player> = {
+ [ANARCHISTS_ID]: ANARCHIST,
+ [COMMUNISTS_ID]: COMMUNIST,
+ [MODERATES_ID]: MODERATE,
+};
+
+const player_faction_map: Record<Player, FactionId> = {
+ [ANARCHIST]: ANARCHISTS_ID,
+ [COMMUNIST]: COMMUNISTS_ID,
+ [MODERATE]: MODERATES_ID,
+};
+
+const front_names: Record<string, string> = {
+ a: 'the Aragon Front',
+ m: 'the Madrid Front',
+ n: 'the Nothern Front',
+ s: 'the Southern Front',
+ d: 'the Front closest to Defeat',
+ v: 'the Front closest to Victory',
+};
+
+const track_names: Record<number, string> = {
+ [LIBERTY]: 'Liberty',
+ [COLLECTIVIZATION]: 'Collectivization',
+ [GOVERNMENT]: 'Government',
+ [SOVIET_SUPPORT]: 'Soviet Support',
+ [FOREIGN_AID]: 'Foreign Aid',
+};
+
const {
cards,
// fronts
} = data;
const faction_cards = {
- [ANARCHIST]: make_list(37, 54) as CardId[],
- [COMMUNIST]: make_list(19, 36) as CardId[],
- [MODERATE]: make_list(1, 18) as CardId[],
+ [ANARCHISTS_ID]: make_list(37, 54) as CardId[],
+ [COMMUNISTS_ID]: make_list(19, 36) as CardId[],
+ [MODERATES_ID]: make_list(1, 18) as CardId[],
};
const fascist_decks = {
@@ -113,18 +155,26 @@ export const scenarios = ['Standard'];
export const roles: Player[] = [ANARCHIST, COMMUNIST, MODERATE];
-function gen_action(action, argument) {
- if (!(action in view.actions)) view.actions[action] = [];
- view.actions[action].push(argument);
+function gen_action(action: string, argument?: number | string) {
+ if (argument === undefined) {
+ view.actions![action] = 1;
+ } else {
+ if (!(action in view.actions)) view.actions[action] = [];
+ view.actions[action].push(argument);
+ }
+}
+
+function gen_action_card(card_id: CardId) {
+ gen_action('card', card_id);
}
function gen_action_space(space) {
gen_action('space', space);
}
-function gen_action_piece(piece) {
- gen_action('piece', piece);
-}
+// function gen_action_piece(piece) {
+// gen_action('piece', piece);
+// }
// function gen_action_card(card) {
// gen_action('card', card);
@@ -136,6 +186,7 @@ export function action(
action: string,
arg: unknown
) {
+ console.log('action', state, player, action, arg);
game = state;
let S = states[game.state];
if (action in S) S[action](arg, player);
@@ -144,18 +195,171 @@ export function action(
return game;
}
+// #region ENGINE
+
+const leaf_node = 'l';
+const seq_node = 's';
+const function_node = 'f';
+const resolved = 1;
+
+function setup_bag_of_glory() {
+ console.log('setup_bag_of_glory');
+ game.engine = [
+ {
+ t: leaf_node,
+ p: game.initiative,
+ s: 'add_glory',
+ },
+ {
+ t: function_node,
+ f: 'end_of_turn',
+ },
+ ];
+ next();
+}
+
+function setup_choose_card() {
+ console.log('setup_choose_card');
+ game.engine = roles.map((role) => ({
+ t: leaf_node,
+ p: player_faction_map[role],
+ s: 'choose_card',
+ }));
+ game.engine.push({
+ t: function_node,
+ f: 'setup_player_turn',
+ });
+ next();
+}
+
+function setup_player_turn() {
+ console.log('setup_player_turn');
+ // TODO: reverse order in second round
+ const first = game.initiative;
+ const second = get_next_faction(first);
+ const third = get_next_faction(second);
+ game.engine = [first, second, third].map((faction_id) => ({
+ t: seq_node,
+ c: [
+ {
+ t: leaf_node,
+ s: 'player_turn',
+ p: faction_id,
+ },
+ ],
+ }));
+ game.engine.push({
+ t: function_node,
+ f: 'resolve_fascist_test',
+ });
+ game.engine.push({
+ t: function_node,
+ f: 'setup_bag_of_glory',
+ });
+ next();
+}
+
+const engine_functions: Record<string, Function> = {
+ end_of_turn,
+ end_of_year,
+ setup_bag_of_glory,
+ setup_choose_card,
+ setup_player_turn,
+ resolve_fascist_test,
+};
+
+type EngineNode = FunctionNode | LeafNode | SeqNode;
+
+interface FunctionNode {
+ t: 'f';
+ f: string; // function to be triggered
+ a?: any; // args
+ r?: 0 | 1; // 1 if resolved
+}
+
+interface SeqNode {
+ t: 's'; // Type
+ c: EngineNode[];
+}
+
+interface LeafNode {
+ t: 'l';
+ s: string; // State
+ p: FactionId; // Player
+ a?: any; // args
+ r?: 0 | 1; // 1 if resolved
+}
+
+function get_active_node(engine: EngineNode[]): FunctionNode | LeafNode | null {
+ for (let i of engine) {
+ if (i.t === leaf_node && i.r !== resolved) {
+ return i;
+ }
+ if (i.t === function_node && i.r !== resolved) {
+ return i;
+ }
+ if (i.t === seq_node) {
+ const next_child = get_active_node(i.c);
+ if (next_child !== null) {
+ return next_child;
+ }
+ }
+ }
+ return null;
+}
+
+function get_active_node_args(): any {
+ const node = get_active_node(game.engine);
+ if (node.t === leaf_node) {
+ return node.a;
+ }
+ return null;
+}
+
+function next() {
+ console.log('next');
+ const node = get_active_node(game.engine);
+ console.log('node', node);
+ if (node.t === function_node && engine_functions[node.f]) {
+ resolve_active_node();
+ const args = node.a;
+ if (args) {
+ engine_functions[node.f](args);
+ } else {
+ engine_functions[node.f]();
+ }
+ } else if (node.t === 'l') {
+ game.state = node.s;
+ game.active = faction_player_map[node.p];
+ }
+}
+
+function resolve_active_node() {
+ const next_node = get_active_node(game.engine);
+ console.log('resolve_active_node', next_node);
+ if (next_node !== null) {
+ next_node.r = resolved;
+ }
+}
+
+function resolve_active_and_proceed() {
+ resolve_active_node();
+ next();
+}
+
+// #endregion
+
// #region VIEW
export { game_view as view };
function game_view(state: Game, player: Player) {
- console.log('game_view', state);
- console.log('player', player);
game = state;
- const faction_id = get_faction_id(player);
+ const faction_id = player_faction_map[player];
view = {
+ engine: game.engine,
log: game.log,
prompt: null,
location: game.location,
@@ -163,7 +367,7 @@ function game_view(state: Game, player: Player) {
current_events: game.current_events,
fronts: game.fronts,
hand: game.hands[faction_id],
- tracks: game.tracks
+ tracks: game.tracks,
};
if (player !== game.active) {
@@ -181,9 +385,10 @@ function game_view(state: Game, player: Player) {
// #endregion
-// #region setup
+// #region SETUP
export function setup(seed: number, _scenario: string, _options: unknown) {
+ // game.seed = seed;
game = {
seed: seed,
state: null,
@@ -195,94 +400,243 @@ export function setup(seed: number, _scenario: string, _options: unknown) {
},
bonuses: [ON, ON],
current_events: [],
+ engine: [],
fronts: {
- a: 2,
- m: 2,
- n: 2,
- s: 2,
+ a: -2,
+ m: -2,
+ n: -2,
+ s: -2,
},
hands: {
- [ANARCHISTS_ID]: [
- draw_card(faction_cards[ANARCHIST]),
- draw_card(faction_cards[ANARCHIST]),
- draw_card(faction_cards[ANARCHIST]),
- draw_card(faction_cards[ANARCHIST]),
- draw_card(faction_cards[ANARCHIST]),
- ],
- [COMMUNISTS_ID]: [
- draw_card(faction_cards[COMMUNIST]),
- draw_card(faction_cards[COMMUNIST]),
- draw_card(faction_cards[COMMUNIST]),
- draw_card(faction_cards[COMMUNIST]),
- draw_card(faction_cards[COMMUNIST]),
- ],
- [MODERATES_ID]: [
- draw_card(faction_cards[MODERATE]),
- draw_card(faction_cards[MODERATE]),
- draw_card(faction_cards[MODERATE]),
- draw_card(faction_cards[MODERATE]),
- draw_card(faction_cards[MODERATE]),
- ],
+ [ANARCHISTS_ID]: [],
+ [COMMUNISTS_ID]: [],
+ [MODERATES_ID]: [],
},
hero_points: {
[ANARCHISTS_ID]: 2,
[COMMUNISTS_ID]: 2,
[MODERATES_ID]: 0,
},
+ cards_in_play: {
+ [ANARCHISTS_ID]: null,
+ [COMMUNISTS_ID]: null,
+ [MODERATES_ID]: null,
+ },
initiative: MODERATES_ID,
+ tableaus: {
+ [ANARCHISTS_ID]: [],
+ [COMMUNISTS_ID]: [],
+ [MODERATES_ID]: [],
+ },
tracks: [5, 5, 6, 3, 3],
log: [],
undo: [],
turn: 1,
year: 1,
+ state_data: null,
};
- fascist_event();
-
- game.state = 'move';
-
+ start_year();
return game;
}
+function draw_hand_cards() {
+ role_ids.forEach((role) => {
+ const deck = faction_cards[role];
+ for (let i = 0; i < 5; i++) {
+ game.hands[role];
+ game.hands[role].push(draw_card(deck));
+ }
+ });
+}
+
// #endregion
-states.move = {
- inactive: 'move',
+function start_year() {
+ log_h1('Year ' + game.year);
+ draw_hand_cards();
+ start_turn();
+}
+
+function start_turn() {
+ log_h2('Turn ' + game.turn);
+
+ const deck = fascist_decks[game.year];
+ const cardId = draw_card(deck);
+ game.current_events.push(cardId);
+
+ const card = cards[cardId] as EventCard;
+ log_h3('Fascist Event: ' + card.title);
+
+ game.engine = card.effects.map((_effect, index) => ({
+ t: leaf_node,
+ s: 'resolve_event',
+ p: game.initiative,
+ a: index,
+ }));
+ game.engine.push({
+ t: 'f',
+ f: 'setup_choose_card',
+ });
+ next();
+ // game.state = 'resolve_event';
+ // game.active = faction_player_map[game.initiative];
+ // game.state_data = {
+ // current_effect: 0,
+ // };
+}
+
+// region STATES
+
+states.add_glory = {
+ inactive: 'add tokens to the Bag of Glory',
+ prompt() {
+ view.prompt = 'Add tokens to the Bag of Glory';
+ gen_action('add_glory');
+ },
+ add_glory() {
+ console.log('add_glory');
+ let number = 1;
+ if (game.turn === 4) {
+ number++;
+ }
+ game.bag_of_glory[get_active_faction()] += number;
+ if (number === 1) {
+ log_h3(`${game.active} adds 1 token to the Bag of Glory`);
+ } else {
+ log_h3(`${game.active} adds ${number} tokens to the Bag of Glory`);
+ }
+ resolve_active_and_proceed();
+ },
+};
+
+states.resolve_event = {
+ inactive: 'resolve Fascist Event',
+ prompt() {
+ const card = get_current_event();
+ const node = get_active_node(game.engine) as LeafNode;
+ const effect: CardEffect = card.effects[node.a];
+ view.prompt = get_event_prompt(effect);
+
+ if (
+ effect.type === 'attack' &&
+ (effect.target === 'd' || effect.target === 'v')
+ ) {
+ const fronts = get_fronts_closest_to(effect.target);
+ fronts.forEach((id) => gen_action('front', id));
+ } else if (effect.type === 'attack') {
+ gen_action('front', effect.target);
+ } else if (effect.type === 'track') {
+ gen_action('standee', effect.target);
+ }
+ // for (let p = 0; p < 5; ++p) gen_action('standee', p);
+ },
+ front(f: string) {
+ const card = get_current_event();
+ const value = card.effects[get_active_node_args()].value;
+ game.fronts[f] -= value;
+ log_h3(`${value} attacks added to ${front_names[f]}`);
+ resolve_active_and_proceed();
+ },
+ standee(s: string) {
+ const effect = get_current_event().effects[get_active_node_args()];
+ const value = effect.value;
+ game.tracks[s] += value;
+ log_h3(`${track_names[effect.target]} decreased by ${Math.abs(value)}`);
+ resolve_active_and_proceed();
+ },
+};
+
+states.choose_card = {
+ inactive: 'choose a card',
prompt() {
- view.prompt = 'Move a piece.';
- for (let p = 0; p < 32; ++p) gen_action_piece(p);
+ view.prompt = 'Choose a card to play this turn';
+ const hand = game.hands[player_faction_map[game.active]];
+ for (let c of hand) gen_action_card(c);
+ },
+ card(c: CardId) {
+ log_h3(`${game.active} chooses a card`);
+ if (!game.cards_in_play) {
+ game.cards_in_play = {};
+ }
+ game.cards_in_play[player_faction_map[game.active]] = c;
+ resolve_active_and_proceed();
},
- // piece(p) {
- // game.selected = p
- // game.state = "move_to"
- // },
};
-states.move_to = {
- inactive: 'move',
+states.player_turn = {
+ inactive: 'play their turn',
prompt() {
- view.prompt = 'Move the piece to a space.';
- for (let s = 0; s < 64; ++s) gen_action_space(s);
+ view.prompt = 'Play your card or spend Hero points';
+ gen_action_card(game.cards_in_play[player_faction_map[game.active]]);
+ },
+ card(c: CardId) {
+ const faction = get_active_faction();
+ log_h3(`${game.active} plays ${cards[c].title} to their tableau`);
+ if (!game.tableaus) {
+ game.tableaus = {
+ [ANARCHISTS_ID]: [],
+ [COMMUNISTS_ID]: [],
+ [MODERATES_ID]: [],
+ };
+ }
+ game.cards_in_play[faction] = null;
+ game.tableaus[faction].push(c);
+ array_remove(game.hands[faction], game.hands[faction].indexOf(c));
+ resolve_active_and_proceed();
},
- // space(to) {
- // game.location[game.selected] = to
- // game.state = "move"
- // if (game.active === PLAYER1)
- // game.active = PLAYER2
- // else
- // game.active = PLAYER1
- // },
};
+// states.move_to = {
+// inactive: 'move',
+// prompt() {
+// view.prompt = 'Move the piece to a space.';
+// for (let s = 0; s < 64; ++s) gen_action_space(s);
+// },
+// // space(to) {
+// // game.location[game.selected] = to
+// // game.state = "move"
+// // if (game.active === PLAYER1)
+// // game.active = PLAYER2
+// // else
+// // game.active = PLAYER1
+// // },
+// };
+
+// #endrregion
+
function pop_undo() {
const save_log = game.log;
const save_undo = game.undo;
game = save_undo.pop()!;
- (save_log as string[]).length = game.log as number;
+ (save_log as string[]).length = game.log as unknown as number;
game.log = save_log;
game.undo = save_undo;
}
+// #region GAME FUNCTIONS
+
+function end_of_turn() {
+ // REMOVE playre tplems from the Fronts;
+ log_h2('End of turn');
+ if (game.turn === 4) {
+ end_of_year();
+ } else {
+ game.turn++;
+ start_turn();
+ }
+}
+
+function end_of_year() {}
+
+function resolve_fascist_test() {
+ console.log('resolve fascist test');
+ log_h2('Fascist test is resolved');
+ next();
+}
+
+// #endregion
+
// #region CARDS
// function draw_faction_card(faction: Player): CardId {
// return draw_faction_cards(faction, 1)[0];
@@ -293,10 +647,13 @@ function pop_undo() {
// }
-function draw_card(deck: CardId[]) {
+function draw_card(deck: CardId[]): CardId {
+ console.log('draw_card_deck', deck);
clear_undo();
let i = random(deck.length);
- let c = deck[i];
+ console.log('random ', i);
+ let c = deck[i] as CardId;
+ console.log('draw_card_id', c);
set_delete(deck, c);
return c;
}
@@ -305,59 +662,177 @@ function draw_card(deck: CardId[]) {
// #region EVENTS
-function resolve_event_attack(target: string | number, value: number) {
- switch (target) {
- case 'v':
+function get_current_event(): EventCard {
+ return cards[
+ game.current_events[game.current_events.length - 1]
+ ] as EventCard;
+}
+
+// function get_front_name(frontId: string) {
+// switch (frontId) {
+// case 'a':
+// return 'the Aragon Front';
+// case 'm':
+// return 'the Madrid Front';
+// case 'n':
+// return 'the Nothern Front';
+// case 's':
+// return 'the Southern Front';
+// case 'd':
+// return 'the Front closest to Defeat';
+// case 'v':
+// return 'the Front closest to Victory';
+// default:
+// return '';
+// }
+// }
+
+function get_event_prompt(effect: CardEffect) {
+ let prompt = '';
+ switch (effect.type) {
+ case 'attack':
+ return 'Attack ' + front_names[effect.target as string];
+ case 'bonus':
break;
- case 'd':
+ case 'hero_points':
break;
- default:
- game.fronts[target] += value;
+ case 'track':
+ return 'Decrease ' + track_names[effect.target];
}
+ return prompt;
}
-function fascist_event() {
- const deck = fascist_decks[game.year];
- const cardId = draw_card(deck);
- game.current_events.push(cardId);
+// function resolve_event_attack(target: string | number, value: number) {
+// switch (target) {
+// case 'v':
+// break;
+// case 'd':
+// break;
+// default:
+// game.fronts[target] += value;
+// }
+// }
- const card = cards[cardId] as EventCard;
+// function fascist_event() {
+// // log_h1('Year ' + game.year);
+// const deck = fascist_decks[game.year];
+// const cardId = draw_card(deck);
+// game.current_events.push(cardId);
+
+// const card = cards[cardId] as EventCard;
+
+// card.effects.forEach((effect) => {
+// switch (effect.type) {
+// case 'attack':
+// resolve_event_attack(effect.target, effect.value);
+// break;
+// case 'bonus':
+// break;
+// case 'hero_points':
+// break;
+// case 'track':
+// game.tracks[effect.target] += effect.value;
+// break;
+// }
+// });
+// }
- card.effects.forEach((effect) => {
- switch (effect.type) {
- case 'attack':
- resolve_event_attack(effect.target, effect.value);
- break;
- case 'bonus':
- break;
- case 'hero_points':
- break;
- case 'track':
- game.tracks[effect.target] += effect.value;
- break;
- }
- });
+// #endregion
+
+// #region FRONTS
+
+function get_fronts_closest_to(target: 'd' | 'v') {
+ const values = Object.values(game.fronts);
+ const targetValue =
+ target === 'd' ? Math.min(...values) : Math.max(...values);
+ return Object.keys(game.fronts).filter(
+ (frontId) => game.fronts[frontId] === targetValue
+ );
}
// #endregion
+// #region LOGGING
+
+function log_br() {
+ if (game.log.length > 0 && game.log[game.log.length - 1] !== '')
+ game.log.push('');
+}
+
+function log(msg: string) {
+ game.log.push(msg);
+}
+
+// function logevent(cap: Card) {
+// game.log.push(`E${cap}.`)
+// }
+
+// function logcap(cap: Card) {
+// game.log.push(`C${cap}.`)
+// }
+
+// function logi(msg: string) {
+// game.log.push('>' + msg);
+// }
+
+// function logii(msg: string) {
+// game.log.push('>>' + msg);
+// }
+
+function log_h1(msg: string) {
+ log_br();
+ log('.h1 ' + msg);
+ log_br();
+}
+
+function log_h2(msg: string) {
+ log_br();
+ log('.h2 ' + msg);
+ log_br();
+}
+
+// function log_h2_active(msg: string) {
+// log_br();
+// log('.h2 ' + msg);
+// log_br();
+// }
+
+// function log_h2_common(msg: string) {
+// log_br();
+// log('.h2 ' + msg);
+// log_br();
+// }
+
+function log_h3(msg: string) {
+ log_br();
+ log('.h3 ' + msg);
+}
+
+// function log_h4(msg: string) {
+// log_br();
+// log('.h4 ' + msg);
+// }
+
+// #endregion LOGGING
+
// #region UTILITY
function clear_undo() {
- if (game.undo.length > 0) game.undo = [];
-}
-
-function get_faction_id(player: Player) {
- switch (player) {
- case ANARCHIST:
- return ANARCHISTS_ID;
- case COMMUNIST:
- return COMMUNISTS_ID;
- case MODERATE:
- return MODERATES_ID;
- default:
- throw new Error('Unknown player');
+ console.log('game clear undo', game?.undo);
+ if (game?.undo && game.undo.length > 0) game.undo = [];
+}
+
+function get_active_faction(): FactionId {
+ return player_faction_map[game.active];
+}
+
+function get_next_faction(faction_id: FactionId): FactionId {
+ const index = role_ids.indexOf(faction_id);
+ let next_index = index + 1;
+ if (next_index === role_ids.length) {
+ next_index = 0;
}
+ return role_ids[next_index];
}
function make_list(first: number, last: number) {