From 629206f773d5fd4c9247db03e3a705c4dcdc77c4 Mon Sep 17 00:00:00 2001 From: Frans Bongers Date: Thu, 28 Nov 2024 22:36:10 +0100 Subject: setup game engine --- land-and-freedom.css | 34 +++ land-and-freedom.scss | 32 +++ play.js | 71 ++++-- play.ts | 84 +++++-- rules.js | 584 ++++++++++++++++++++++++++++++++++-------- rules.ts | 685 ++++++++++++++++++++++++++++++++++++++++++-------- tsconfig.json | 2 +- 7 files changed, 1255 insertions(+), 237 deletions(-) diff --git a/land-and-freedom.css b/land-and-freedom.css index 6b4db57..e4c95a9 100644 --- a/land-and-freedom.css +++ b/land-and-freedom.css @@ -328,6 +328,21 @@ main { background-repeat: no-repeat; border-radius: 4px; box-shadow: 0 0 0 1px #333; + transition-property: top, left; + transition-duration: 700ms; + transition-timing-function: ease; +} + +.card.action, +.front.action, +.standee.action { + box-shadow: 0 0 0 3px white; +} + +.card.action:hover, +.front.action:hover, +.standee.action:hover { + box-shadow: 0 0 0 3px yellow; } .standee[data-standee-id="0"] { @@ -349,3 +364,22 @@ main { .standee[data-standee-id="4"] { background-image: url("images/standees/standee_4.png"); } + +#log { + background-color: floralwhite; +} + +#log .h1, #log .h2 { + font-size: 10px; + padding-top: 2px; + padding-bottom: 2px; + text-align: center; +} + +#log .h1 { + background-color: hsl(4, 40%, 73%); +} + +#log .h2 { + background-color: hsl(250, 40%, 83%); +} diff --git a/land-and-freedom.scss b/land-and-freedom.scss index 42f75e2..fc8e962 100644 --- a/land-and-freedom.scss +++ b/land-and-freedom.scss @@ -1,6 +1,9 @@ // @use "sass:math"; @use 'sass:map'; +$selectable-color: white; // yellow; +$selected-color: yellow; //blue; + main { // background-color: rgb(213, 196, 131); background-color: darkolivegreen; @@ -106,11 +109,40 @@ main { background-repeat: no-repeat; border-radius: 4px; box-shadow: 0 0 0 1px #333; + transition-property: top, left; + transition-duration: 700ms; + transition-timing-function: ease; // opacity: 0.6; } +.card.action, +.front.action, +.standee.action { + box-shadow: 0 0 0 3px $selectable-color; +} + +.card.action:hover, +.front.action:hover, +.standee.action:hover { + box-shadow: 0 0 0 3px $selected-color; +} + +// .standee.action:hover { +// box-shadow: 0 0 0 2px blue; +// } + @for $i from 0 through 4 { .standee[data-standee-id='#{$i}'] { background-image: url('images/standees/standee_#{$i}.png'); } } + +#log { background-color: floralwhite; } +#log .h1, #log .h2 { + font-size: 10px; + padding-top: 2px; + padding-bottom: 2px; + text-align: center; +} +#log .h1 { background-color: hsl(4, 40%, 73%); } +#log .h2 { background-color: hsl(250, 40%, 83%); } \ No newline at end of file diff --git a/play.js b/play.js index af1eccb..9c1b6f2 100644 --- a/play.js +++ b/play.js @@ -46,30 +46,30 @@ const LAYOUT_TRACKS = [ spaces.appendChild(element); const frontValueElement = (ui.frontValues[front.id] = document.createElement('span')); frontValueElement.classList.add('value'); + register_action(element, 'front', id); element.appendChild(frontValueElement); }); })(); console.log('ui', ui); -// @ts-ignore -function register_action(e, _action, _id) { - // e.my_action = action - // e.my_id = id +function register_action(e, action, id) { + e.my_action = action; + e.my_id = id; e.onmousedown = on_click_action; action_register.push(e); } function on_click_action(evt) { + console.log('on_click_action', evt); if (evt.button === 0) if (send_action(evt.target.my_action, evt.target.my_id)) evt.stopPropagation(); } -// function is_action(action, arg) { -// if (arg === undefined) return !!(view.actions && view.actions[action] === 1); -// return !!( -// view.actions && -// view.actions[action] && -// view.actions[action].includes(arg) -// ); -// } +function is_action(action, arg) { + if (arg === undefined) + return !!(view.actions && view.actions[action] === 1); + return !!(view.actions && + view.actions[action] && + view.actions[action].includes(arg)); +} let on_init_once = false; function on_init() { console.log('on_init'); @@ -97,6 +97,7 @@ function on_init() { e.className = 'standee'; e.setAttribute('data-standee-id', '' + s); register_action(e, 'standee', s); + ui.tracks.appendChild(ui.standees[s]); } console.log('standees', ui.standees); // create card elements @@ -106,6 +107,7 @@ function on_init() { e.setAttribute('data-card-id', '' + data.cards[c].id); register_action(e, 'card', c); } + console.log('action_register', action_register[0]); } // @ts-ignore function on_update() { @@ -128,15 +130,54 @@ function on_update() { for (let c of view.hand) ui.hand.appendChild(ui.cards[c]); for (let i = 0; i < view.tracks.length; i++) { - ui.tracks.appendChild(ui.standees[i]); + // ui.tracks.appendChild(ui.standees[i]); ui.standees[i].style.left = LAYOUT_TRACKS[i][view.tracks[i]][0] + 'px'; ui.standees[i].style.top = LAYOUT_TRACKS[i][view.tracks[i]][1] + 'px'; } for (let frontId of Object.keys(view.fronts)) { ui.frontValues[frontId].replaceChildren(view.fronts[frontId]); } - // for (let e of action_register) - // e.classList.toggle('action', is_action(e.my_action, e.my_id)); + for (let e of action_register) + e.classList.toggle('action', is_action(e.my_action, e.my_id)); action_button('next', 'Next'); action_button('undo', 'Undo'); + action_button('add_glory', 'Add Glory'); +} +// @ts-ignore +function on_log(text) { + let p = document.createElement("div"); + if (text.match(/^>/)) { + text = text.substring(1); + p.className = 'i'; + } + text = text.replace(/&/g, "&"); + text = text.replace(//g, ">"); + // 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) + // TODO dice icons + // text = text.replace(/\bD\d\b/g, sub_icon) + if (text.match(/^\.h1/)) { + text = text.substring(4); + p.className = 'h1'; + } + else if (text.match(/^\.h2/)) { + text = text.substring(4); + p.className = 'h2'; + } + else if (text.match(/^\.h3\.allies/)) { + text = text.substring(10); + p.className = 'h3 allies'; + } + else if (text.match(/^\.h3\.germans/)) { + text = text.substring(11); + p.className = 'h3 germans'; + } + else if (text.match(/^\.h3/)) { + text = text.substring(4); + p.className = 'h3'; + } + p.innerHTML = text; + return p; } diff --git a/play.ts b/play.ts index e941c05..26e942c 100644 --- a/play.ts +++ b/play.ts @@ -63,38 +63,40 @@ const LAYOUT_TRACKS = [ spaces.appendChild(element); const frontValueElement = (ui.frontValues[front.id] = document.createElement('span')); frontValueElement.classList.add('value'); + register_action(element, 'front', id); element.appendChild(frontValueElement); }); })(); console.log('ui', ui); -// @ts-ignore + function register_action( - e: HTMLElement, - _action: string, - _id: string | number + e: HTMLElement & {my_action?: string; my_id?: string | number}, + action: string, + id: string | number ) { - // e.my_action = action - // e.my_id = id + e.my_action = action + e.my_id = id e.onmousedown = on_click_action; action_register.push(e); } function on_click_action(evt) { + console.log('on_click_action', evt); if (evt.button === 0) if (send_action(evt.target.my_action, evt.target.my_id)) evt.stopPropagation(); } -// function is_action(action, arg) { -// if (arg === undefined) return !!(view.actions && view.actions[action] === 1); -// return !!( -// view.actions && -// view.actions[action] && -// view.actions[action].includes(arg) -// ); -// } +function is_action(action, arg) { + if (arg === undefined) return !!(view.actions && view.actions[action] === 1); + return !!( + view.actions && + view.actions[action] && + view.actions[action].includes(arg) + ); +} let on_init_once = false; @@ -128,6 +130,7 @@ function on_init() { e.className = 'standee'; e.setAttribute('data-standee-id', '' + s) register_action(e, 'standee', s); + ui.tracks.appendChild(ui.standees[s]); } console.log('standees', ui.standees); @@ -139,6 +142,8 @@ function on_init() { e.setAttribute('data-card-id', '' + data.cards[c].id) register_action(e, 'card', c); } + + console.log('action_register',action_register[0]); } // @ts-ignore @@ -165,7 +170,7 @@ function on_update() { for (let c of view.hand) ui.hand.appendChild(ui.cards[c]); for (let i = 0; i < view.tracks.length; i++) { - ui.tracks.appendChild(ui.standees[i]); + // ui.tracks.appendChild(ui.standees[i]); ui.standees[i].style.left = LAYOUT_TRACKS[i][view.tracks[i]][0] + 'px'; ui.standees[i].style.top = LAYOUT_TRACKS[i][view.tracks[i]][1] + 'px'; } @@ -174,9 +179,54 @@ function on_update() { ui.frontValues[frontId].replaceChildren(view.fronts[frontId]); } - // for (let e of action_register) - // e.classList.toggle('action', is_action(e.my_action, e.my_id)); + for (let e of action_register) + e.classList.toggle('action', is_action(e.my_action, e.my_id)); action_button('next', 'Next'); action_button('undo', 'Undo'); + action_button('add_glory', 'Add Glory'); } + +// @ts-ignore +function on_log(text) { + let p = document.createElement("div") + + if (text.match(/^>/)) { + text = text.substring(1) + p.className = 'i' + } + + text = text.replace(/&/g, "&") + text = text.replace(//g, ">") + // 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) + + // TODO dice icons + // text = text.replace(/\bD\d\b/g, sub_icon) + + if (text.match(/^\.h1/)) { + text = text.substring(4) + p.className = 'h1' + } + else if (text.match(/^\.h2/)) { + text = text.substring(4) + p.className = 'h2' + } + else if (text.match(/^\.h3\.allies/)) { + text = text.substring(10) + p.className = 'h3 allies' + } + else if (text.match(/^\.h3\.germans/)) { + text = text.substring(11) + p.className = 'h3 germans' + } + else if (text.match(/^\.h3/)) { + text = text.substring(4) + p.className = 'h3' + } + + p.innerHTML = text + return p +} \ No newline at end of file diff --git a/rules.js b/rules.js index 1704761..99af9ab 100644 --- a/rules.js +++ b/rules.js @@ -14,13 +14,39 @@ const MODERATE = 'Moderate'; const ANARCHISTS_ID = 'a'; const COMMUNISTS_ID = 'c'; const MODERATES_ID = 'm'; +const role_ids = [ANARCHISTS_ID, COMMUNISTS_ID, MODERATES_ID]; +const faction_player_map = { + [ANARCHISTS_ID]: ANARCHIST, + [COMMUNISTS_ID]: COMMUNIST, + [MODERATES_ID]: MODERATE, +}; +const player_faction_map = { + [ANARCHIST]: ANARCHISTS_ID, + [COMMUNIST]: COMMUNISTS_ID, + [MODERATE]: MODERATES_ID, +}; +const front_names = { + 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 = { + [data_1.LIBERTY]: 'Liberty', + [data_1.COLLECTIVIZATION]: 'Collectivization', + [data_1.GOVERNMENT]: 'Government', + [data_1.SOVIET_SUPPORT]: 'Soviet Support', + [data_1.FOREIGN_AID]: 'Foreign Aid', +}; const { cards, // fronts } = data_1.default; const faction_cards = { - [ANARCHIST]: make_list(37, 54), - [COMMUNIST]: make_list(19, 36), - [MODERATE]: make_list(1, 18), + [ANARCHISTS_ID]: make_list(37, 54), + [COMMUNISTS_ID]: make_list(19, 36), + [MODERATES_ID]: make_list(1, 18), }; const fascist_decks = { 1: make_list(55, 62), @@ -28,20 +54,29 @@ const fascist_decks = { exports.scenarios = ['Standard']; exports.roles = [ANARCHIST, COMMUNIST, MODERATE]; function gen_action(action, argument) { - if (!(action in view.actions)) - view.actions[action] = []; - view.actions[action].push(argument); + 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) { + 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); // } function action(state, player, action, arg) { + console.log('action', state, player, action, arg); game = state; let S = states[game.state]; if (action in S) @@ -52,12 +87,132 @@ function action(state, player, action, arg) { throw new Error('Invalid action: ' + 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 = exports.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 = { + end_of_turn, + end_of_year, + setup_bag_of_glory, + setup_choose_card, + setup_player_turn, + resolve_fascist_test, +}; +function get_active_node(engine) { + 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() { + 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(); +} function game_view(state, 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, @@ -65,7 +220,7 @@ function game_view(state, player) { current_events: game.current_events, fronts: game.fronts, hand: game.hands[faction_id], - tracks: game.tracks + tracks: game.tracks, }; if (player !== game.active) { let inactive = states[game.state].inactive || game.state; @@ -82,8 +237,9 @@ function game_view(state, player) { return view; } // #endregion -// #region setup +// #region SETUP function setup(seed, _scenario, _options) { + // game.seed = seed; game = { seed: seed, state: null, @@ -95,80 +251,196 @@ function setup(seed, _scenario, _options) { }, bonuses: [data_1.ON, data_1.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]; + 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 = 'Move a piece.'; - for (let p = 0; p < 32; ++p) - gen_action_piece(p); + 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); + const effect = 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) { + 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) { + 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 = '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) { + 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) { + 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; @@ -177,6 +449,25 @@ function pop_undo() { 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]; @@ -185,61 +476,156 @@ function pop_undo() { // const drawnCards = []; // } function draw_card(deck) { + console.log('draw_card_deck', deck); clear_undo(); let i = random(deck.length); + console.log('random ', i); let c = deck[i]; + console.log('draw_card_id', c); set_delete(deck, c); return c; } // #endregion // #region EVENTS -function resolve_event_attack(target, value) { - switch (target) { - case 'v': +function get_current_event() { + return cards[game.current_events[game.current_events.length - 1]]; +} +// 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) { + let prompt = ''; + switch (effect.type) { + case 'attack': + return 'Attack ' + front_names[effect.target]; + 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); - const card = cards[cardId]; - 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; - } - }); +// function resolve_event_attack(target: string | number, value: number) { +// switch (target) { +// case 'v': +// break; +// case 'd': +// break; +// default: +// game.fronts[target] += value; +// } +// } +// 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; +// } +// }); +// } +// #endregion +// #region FRONTS +function get_fronts_closest_to(target) { + 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) { + 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) { + log_br(); + log('.h1 ' + msg); + log_br(); +} +function log_h2(msg) { + 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) { + 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) + console.log('game clear undo', game?.undo); + if (game?.undo && game.undo.length > 0) game.undo = []; } -function get_faction_id(player) { - switch (player) { - case ANARCHIST: - return ANARCHISTS_ID; - case COMMUNIST: - return COMMUNISTS_ID; - case MODERATE: - return MODERATES_ID; - default: - throw new Error('Unknown player'); +function get_active_faction() { + return player_faction_map[game.active]; +} +function get_next_faction(faction_id) { + 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, last) { let list = []; 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; 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; bonuses: number[]; + cards_in_play: Record; current_events: CardId[]; + engine: EngineNode[]; fronts: { a: number; m: number; @@ -46,6 +54,7 @@ interface Game { hands: Record; hero_points: Record; initiative: FactionId; + tableaus: Record; 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 = { + [ANARCHISTS_ID]: ANARCHIST, + [COMMUNISTS_ID]: COMMUNIST, + [MODERATES_ID]: MODERATE, +}; + +const player_faction_map: Record = { + [ANARCHIST]: ANARCHISTS_ID, + [COMMUNIST]: COMMUNISTS_ID, + [MODERATE]: MODERATES_ID, +}; + +const front_names: Record = { + 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 = { + [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 = { + 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) { diff --git a/tsconfig.json b/tsconfig.json index 825d119..6517e5c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2021", "declaration": false, "sourceMap": false, "module": "commonjs", -- cgit v1.2.3