diff options
-rw-r--r-- | data.js | 5 | ||||
-rw-r--r-- | data.ts | 3 | ||||
-rw-r--r-- | land-and-freedom.css | 17 | ||||
-rw-r--r-- | land-and-freedom.scss | 17 | ||||
-rw-r--r-- | play.html | 11 | ||||
-rw-r--r-- | play.js | 29 | ||||
-rw-r--r-- | play.ts | 34 | ||||
-rw-r--r-- | rules.js | 45 | ||||
-rw-r--r-- | rules.ts | 63 | ||||
-rw-r--r-- | types.d.ts | 17 |
10 files changed, 186 insertions, 55 deletions
@@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.LIBERTY_OR_COLLECTIVIZATION = exports.COMMUNIST_EXTRA_HERO_POINT = exports.ANARCHIST_EXTRA_HERO_POINT = exports.NORTHERN = exports.SOUTHERN = exports.ARAGON = exports.MADRID = exports.ORGANIZATION_MEDALLION_ID = exports.ARCHIVES_MEDALLION_ID = exports.VOLUNTEERS_MEDALLION_ID = exports.INTELLIGENCE_MEDALLION_ID = exports.PROPAGANDA_MEDALLION_ID = exports.STRATEGY_MEDALLION_ID = exports.FRONTS = exports.DEFEAT = exports.VICTORY = exports.TRASH = exports.TOWARDS_CENTER = exports.AWAY_FROM_CENTER = exports.SELF = exports.PLAYER_WITH_MOST_HERO_POINTS = exports.OTHER_PLAYERS = exports.ON = exports.OFF = exports.TEAMWORK_BONUS = exports.MORALE_BONUS = exports.FOREIGN_AID = exports.SOVIET_SUPPORT = exports.INITIATIVE_PLAYER = exports.GOVERNMENT = exports.COLLECTIVIZATION = exports.CLOSEST_TO_VICTORY = exports.CLOSEST_TO_DEFEAT = exports.LIBERTY = exports.ANY = exports.MODERATES_ID = exports.COMMUNISTS_ID = exports.ANARCHISTS_ID = exports.MODERATE = exports.COMMUNIST = exports.ANARCHIST = exports.ALL_PLAYERS = void 0; +exports.LIBERTY_OR_COLLECTIVIZATION = exports.COMMUNIST_EXTRA_HERO_POINT = exports.ANARCHIST_EXTRA_HERO_POINT = exports.NORTHERN = exports.SOUTHERN = exports.ARAGON = exports.MADRID = exports.ORGANIZATION_MEDALLION_ID = exports.ARCHIVES_MEDALLION_ID = exports.VOLUNTEERS_MEDALLION_ID = exports.INTELLIGENCE_MEDALLION_ID = exports.PROPAGANDA_MEDALLION_ID = exports.STRATEGY_MEDALLION_ID = exports.FRONTS = exports.DEFEAT = exports.VICTORY = exports.TRASH = exports.TOWARDS_CENTER = exports.AWAY_FROM_CENTER = exports.SELF = exports.PLAYER_WITH_MOST_HERO_POINTS = exports.OTHER_PLAYERS = exports.ON = exports.OFF = exports.TEAMWORK_BONUS = exports.MORALE_BONUS = exports.FOREIGN_AID = exports.SOVIET_SUPPORT = exports.INITIATIVE_PLAYER = exports.GOVERNMENT = exports.COLLECTIVIZATION = exports.CLOSEST_TO_VICTORY = exports.CLOSEST_TO_DEFEAT = exports.LIBERTY = exports.ANY = exports.MODERATES_ID = exports.FASCIST_ID = exports.COMMUNISTS_ID = exports.ANARCHISTS_ID = exports.MODERATE = exports.COMMUNIST = exports.ANARCHIST = exports.ALL_PLAYERS = void 0; exports.create_effect = create_effect; const LIBERTY = 0; exports.LIBERTY = LIBERTY; @@ -58,6 +58,8 @@ const ANARCHISTS_ID = 'a'; exports.ANARCHISTS_ID = ANARCHISTS_ID; const COMMUNISTS_ID = 'c'; exports.COMMUNISTS_ID = COMMUNISTS_ID; +const FASCIST_ID = 'f'; +exports.FASCIST_ID = FASCIST_ID; const MODERATES_ID = 'm'; exports.MODERATES_ID = MODERATES_ID; const ANARCHIST = 'Anarchist'; @@ -682,6 +684,7 @@ const data = { effects: [ create_effect('bonus', ANY, ON), create_effect('track', LIBERTY, 2), + create_effect('function', 'card46_event3', 0), create_effect('state', 'peek_fascist_cards', 0), ], icons: ['teamwork_on', 'liberty', 'add_to_front'], @@ -35,6 +35,7 @@ const TRASH = 'trash'; const ANARCHISTS_ID: FactionId = 'a'; const COMMUNISTS_ID: FactionId = 'c'; +const FASCIST_ID = 'f'; const MODERATES_ID: FactionId = 'm'; const ANARCHIST = 'Anarchist' as Player; @@ -78,6 +79,7 @@ export { MODERATE, ANARCHISTS_ID, COMMUNISTS_ID, + FASCIST_ID, MODERATES_ID, ANY, LIBERTY, @@ -704,6 +706,7 @@ const data: StaticData = { effects: [ create_effect('bonus', ANY, ON), create_effect('track', LIBERTY, 2), + create_effect('function', 'card46_event3', 0), create_effect('state', 'peek_fascist_cards', 0), ], icons: ['teamwork_on', 'liberty', 'add_to_front'], diff --git a/land-and-freedom.css b/land-and-freedom.css index 4ef7c15..be49582 100644 --- a/land-and-freedom.css +++ b/land-and-freedom.css @@ -102,6 +102,19 @@ main { color: floralwhite; } +#selectable_cards { + display: flex; + justify-content: center; + flex-wrap: wrap; + padding: 12px; + gap: 12px; + min-height: 260px; +} + +#selectable_cards:empty { + display: none; +} + .panel { min-width: 1271px; max-width: 1271px; @@ -709,6 +722,10 @@ main { box-shadow: 0 0 0 3px yellow; } +.blank_marker.action:hover { + opacity: 0.5; +} + .standee[data-standee-id="0"] { background-image: url("images/standees/standee_0.png"); } diff --git a/land-and-freedom.scss b/land-and-freedom.scss index cae059a..5c0c760 100644 --- a/land-and-freedom.scss +++ b/land-and-freedom.scss @@ -140,6 +140,19 @@ main { } } +#selectable_cards { + display: flex; + justify-content: center; + flex-wrap: wrap; + padding: 12px; + gap: 12px; + min-height: 260px; +} + +#selectable_cards:empty { + display: none; +} + .panel { min-width: 1271px; max-width: 1271px; @@ -351,6 +364,10 @@ main { box-shadow: 0 0 0 3px $selected-color; } +.blank_marker.action:hover { + opacity: 0.5; +} + // .standee.action:hover { // box-shadow: 0 0 0 2px blue; // } @@ -87,8 +87,7 @@ </div> </div> <div id="glory"></div> - <div id="spaces"> - </div> + <div id="spaces"></div> <div id="pieces"></div> <div id="markers"></div> <div id="medallions"></div> @@ -96,20 +95,20 @@ <div id="current_events"></div> </div> </div> - + <div id="selectable_cards"></div> <div class="panel"> <div id="hand_header" class="panel_header">Hand</div> <div id="hand" class="panel_body"></div> </div> - <div class="panel"> + <div class="panel"> <div class="panel_header" data-faction-id="a">Anarchist</div> <div id="tableau_a" class="panel_body"></div> </div> - <div class="panel"> + <div class="panel"> <div class="panel_header" data-faction-id="c">Communist</div> <div id="tableau_c" class="panel_body"></div> </div> - <div class="panel"> + <div class="panel"> <div class="panel_header" data-faction-id="m">Moderate</div> <div id="tableau_m" class="panel_body"></div> </div> @@ -55,6 +55,7 @@ const ui = { hero_points: document.getElementById('pool_hero_points'), }, }, + selectable_cards: document.getElementById('selectable_cards'), tableaus: { a: document.getElementById('tableau_a'), c: document.getElementById('tableau_c'), @@ -257,6 +258,16 @@ function on_init() { register_action(ui.fronts[front_id].front, 'front', front_id); }); } +function place_cards(e, cards) { + e.replaceChildren(); + for (let c of cards) { + ui.cards[c].classList.remove('selected'); + e.appendChild(ui.cards[c]); + if (view.selected_cards.includes(c)) { + ui.cards[c].classList.add('selected'); + } + } +} function on_update() { console.log('on_update', view); on_init(); @@ -282,13 +293,10 @@ function on_update() { for (let bonus_id of Object.keys(view.bonuses)) { ui.bonuses[bonus_id].setAttribute('data-bonus-on', view.bonuses[bonus_id] + 0); } - ui.hand.replaceChildren(); - for (let c of view.hand) { - ui.cards[c].classList.remove('selected'); - ui.hand.appendChild(ui.cards[c]); - if (view.selected_cards.includes(c)) { - ui.cards[c].classList.add('selected'); - } + place_cards(ui.hand, view.hand); + place_cards(ui.selectable_cards, view.selectable_cards); + for (let faction_id of FACTIONS) { + place_cards(ui.tableaus[faction_id], view.tableaus[faction_id]); } for (let i = 0; i < view.tracks.length; ++i) { ui.standees[i].style.left = LAYOUT_TRACKS[i][view.tracks[i]][0] + 'px'; @@ -319,13 +327,6 @@ function on_update() { } ui.roles[view.initiative].medallions.appendChild(ui.initiative_token); ui.initiative_token.setAttribute('data-year', view.year); - for (let faction_id of FACTIONS) { - ui.tableaus[faction_id].replaceChildren(); - for (let c of view.tableaus[faction_id]) { - ui.cards[c].classList.remove('selected'); - ui.tableaus[faction_id].appendChild(ui.cards[c]); - } - } if (view.played_card === null) { ui.turn_info.style.display = 'none'; } @@ -1,6 +1,6 @@ 'use strict'; -import { StaticData, View } from './types'; +import { CardId, StaticData, View } from './types'; declare function action_button(action: string, text: string): void; // declare function register_action(element: HTMLElement, type: string, s: number): void; @@ -69,6 +69,7 @@ const ui = { hero_points: document.getElementById('pool_hero_points'), }, }, + selectable_cards: document.getElementById('selectable_cards'), tableaus: { a: document.getElementById('tableau_a'), c: document.getElementById('tableau_c'), @@ -329,6 +330,17 @@ function on_init() { }); } +function place_cards(e: HTMLElement, cards: CardId[]) { + e.replaceChildren(); + for (let c of cards) { + ui.cards[c].classList.remove('selected'); + e.appendChild(ui.cards[c]); + if (view.selected_cards.includes(c)) { + ui.cards[c].classList.add('selected'); + } + } +} + // @ts-ignore function on_update() { console.log('on_update', view); @@ -371,13 +383,11 @@ function on_update() { ); } - ui.hand.replaceChildren(); - for (let c of view.hand) { - ui.cards[c].classList.remove('selected'); - ui.hand.appendChild(ui.cards[c]); - if (view.selected_cards.includes(c)) { - ui.cards[c].classList.add('selected'); - } + place_cards(ui.hand, view.hand); + place_cards(ui.selectable_cards, view.selectable_cards); + + for (let faction_id of FACTIONS) { + place_cards(ui.tableaus[faction_id], view.tableaus[faction_id]); } for (let i = 0; i < view.tracks.length; ++i) { @@ -418,13 +428,7 @@ function on_update() { ui.roles[view.initiative].medallions.appendChild(ui.initiative_token); ui.initiative_token.setAttribute('data-year', view.year); - for (let faction_id of FACTIONS) { - ui.tableaus[faction_id].replaceChildren(); - for (let c of view.tableaus[faction_id]) { - ui.cards[c].classList.remove('selected'); - ui.tableaus[faction_id].appendChild(ui.cards[c]); - } - } + // ui.turn_info.replaceChildren(); // console.log('played_card', view.played_card); @@ -174,6 +174,7 @@ const engine_functions = { card35_event2, card42_event3, card45_event2, + card46_event3, card50_event2, card53_event2, card54_event1, @@ -272,6 +273,7 @@ function game_view(state, player) { initiative: game.initiative, medallions: game.medallions, played_card: game.played_card, + selectable_cards: game.selectable_cards, selected_cards: game.selected_cards[faction_id], tableaus: game.tableaus, tracks: game.tracks, @@ -357,6 +359,7 @@ function setup(seed, _scenario, _options) { pool: [], }, played_card: null, + selectable_cards: [], selected_cards: { a: [], c: [], @@ -377,6 +380,7 @@ function setup(seed, _scenario, _options) { log: [], undo: [], used_medallions: [], + top_of_events_deck: null, turn: 0, year: 0, }; @@ -415,7 +419,7 @@ function start_year() { } function start_turn() { log_h1(`Year ${game.year} - Turn ${game.turn}`); - const cardId = draw_card(list_deck('fascist')); + const cardId = draw_fascist_card(); game.current_events.push(cardId); const card = cards[cardId]; log_h2('Fascist Event', 'fascist'); @@ -1032,6 +1036,19 @@ states.peek_fascist_cards = { inactive: 'peek at Fascist cards', prompt() { view.prompt = 'Choose one card to return to the top of the deck'; + for (const c of game.selectable_cards) { + gen_action_card(c); + } + }, + card(c) { + game.top_of_events_deck = c; + for (const ec of game.selectable_cards) { + if (ec !== c) { + game.discard.f.push(ec); + } + } + game.selectable_cards = []; + resolve_active_and_proceed(); }, }; states.player_turn = { @@ -1368,6 +1385,12 @@ function card45_event2() { } resolve_active_and_proceed(); } +function card46_event3() { + for (let i = 0; i < 3; ++i) { + game.selectable_cards.push(draw_fascist_card()); + } + resolve_active_and_proceed(); +} function card50_event2() { const value = game.tracks[data_1.COLLECTIVIZATION] >= 8 ? 3 : 2; insert_after_active_node(resolve_effect((0, data_1.create_effect)('front', data_1.ARAGON, value))); @@ -1507,6 +1530,7 @@ function end_of_year() { gain_hero_points_in_player_order(players_to_gain_hero_points, game.year); game.engine = get_player_order().map((f) => create_leaf_node('end_of_year_discard', f)); game.engine.push(create_function_node('start_year')); + game.top_of_events_deck = null; next(); } function gain_hero_points_in_player_order(factions, value) { @@ -1934,6 +1958,14 @@ function draw_card(deck) { set_delete(deck, c); return c; } +function draw_fascist_card() { + if (game.top_of_events_deck !== null) { + const card_id = game.top_of_events_deck; + game.top_of_events_deck = null; + return card_id; + } + return draw_card(list_deck(data_1.FASCIST_ID)); +} function lose_hero_point(faction, value) { const points_lost = Math.min(game.hero_points[faction], Math.abs(value)); game.hero_points.pool += points_lost; @@ -2078,13 +2110,16 @@ function make_list(first, last) { } function list_deck(id) { const deck = []; - const card_list = id === 'fascist' ? fascist_decks[game.year] : faction_cards[id]; + const card_list = id === data_1.FASCIST_ID ? fascist_decks[game.year] : faction_cards[id]; card_list.forEach((card) => { - if (id === 'fascist' && - (game.discard.f.includes(card) || game.current_events.includes(card))) { + if (game.discard[id].includes(card) || + game.selectable_cards.includes(card)) { + return; + } + if (id === data_1.FASCIST_ID && game.current_events.includes(card)) { return; } - else if (id !== 'fascist' && + else if (id !== data_1.FASCIST_ID && (game.hands[id].includes(card) || game.discard[id].includes(card) || game.tableaus[id].includes(card) || @@ -70,6 +70,7 @@ import data, { LIBERTY_OR_COLLECTIVIZATION, ANARCHIST_EXTRA_HERO_POINT, ARAGON, + FASCIST_ID, // StaticData, // PLAYER_WITH_MOST_HERO_POINTS, } from './data'; @@ -128,9 +129,9 @@ const faction_cards = { }; const fascist_decks = { - 1: make_list(55, 72), - 2: make_list(73, 90), - 3: make_list(91, 108), + 1: make_list(55, 72) as CardId[], + 2: make_list(73, 90) as CardId[], + 3: make_list(91, 108) as CardId[], }; export const scenarios = ['Standard']; @@ -313,6 +314,7 @@ const engine_functions: Record<string, Function> = { card35_event2, card42_event3, card45_event2, + card46_event3, card50_event2, card53_event2, card54_event1, @@ -446,6 +448,7 @@ function game_view(state: Game, player: Player) { initiative: game.initiative, medallions: game.medallions, played_card: game.played_card, + selectable_cards: game.selectable_cards, selected_cards: game.selected_cards[faction_id], tableaus: game.tableaus, tracks: game.tracks, @@ -535,6 +538,7 @@ export function setup(seed: number, _scenario: string, _options: unknown) { pool: [], }, played_card: null, + selectable_cards: [], selected_cards: { a: [], c: [], @@ -555,6 +559,7 @@ export function setup(seed: number, _scenario: string, _options: unknown) { log: [], undo: [], used_medallions: [], + top_of_events_deck: null, turn: 0, year: 0, }; @@ -607,7 +612,7 @@ function start_year() { function start_turn() { log_h1(`Year ${game.year} - Turn ${game.turn}`); - const cardId = draw_card(list_deck('fascist')); + const cardId = draw_fascist_card(); game.current_events.push(cardId); const card = cards[cardId] as EventCard; @@ -1271,6 +1276,19 @@ states.peek_fascist_cards = { inactive: 'peek at Fascist cards', prompt() { view.prompt = 'Choose one card to return to the top of the deck'; + for (const c of game.selectable_cards) { + gen_action_card(c); + } + }, + card(c: CardId) { + game.top_of_events_deck = c; + for (const ec of game.selectable_cards) { + if (ec !== c) { + game.discard.f.push(ec); + } + } + game.selectable_cards = []; + resolve_active_and_proceed(); }, }; @@ -1698,6 +1716,13 @@ function card45_event2() { resolve_active_and_proceed(); } +function card46_event3() { + for (let i = 0; i < 3; ++i) { + game.selectable_cards.push(draw_fascist_card()); + } + resolve_active_and_proceed(); +} + function card50_event2() { const value = game.tracks[COLLECTIVIZATION] >= 8 ? 3 : 2; insert_after_active_node( @@ -1869,6 +1894,11 @@ function end_of_year() { create_leaf_node('end_of_year_discard', f) ); game.engine.push(create_function_node('start_year')); + + // New deck is used for next year so clear top card + // if it was set + game.top_of_events_deck = null; + next(); } @@ -2441,6 +2471,15 @@ function draw_card(deck: CardId[]): CardId { return c; } +function draw_fascist_card(): CardId { + if (game.top_of_events_deck !== null) { + const card_id = game.top_of_events_deck; + game.top_of_events_deck = null; + return card_id; + } + return draw_card(list_deck(FASCIST_ID)); +} + function lose_hero_point(faction: FactionId, value: number) { const points_lost = Math.min(game.hero_points[faction], Math.abs(value)); game.hero_points.pool += points_lost; @@ -2656,18 +2695,22 @@ function make_list(first: number, last: number): number[] { return list; } -function list_deck(id: FactionId | 'fascist') { +function list_deck(id: FactionId | 'f') { const deck = []; - const card_list = - id === 'fascist' ? fascist_decks[game.year] : faction_cards[id]; + const card_list: CardId[] = + id === FASCIST_ID ? fascist_decks[game.year] : faction_cards[id]; card_list.forEach((card) => { if ( - id === 'fascist' && - (game.discard.f.includes(card) || game.current_events.includes(card)) + game.discard[id].includes(card) || + game.selectable_cards.includes(card) ) { return; + } + + if (id === FASCIST_ID && game.current_events.includes(card)) { + return; } else if ( - id !== 'fascist' && + id !== FASCIST_ID && (game.hands[id].includes(card) || game.discard[id].includes(card) || game.tableaus[id].includes(card) || @@ -28,7 +28,7 @@ export interface Game { result?: string; victory?: string; // Game specific - active_abilities: number[]; + active_abilities: number[]; turn: number; year: number; bag_of_glory: FactionId[]; @@ -37,9 +37,11 @@ export interface Game { current_events: CardId[]; discard: Record<FactionId | 'f', number[]>; engine: EngineNode[]; - // Set to faction whos turn it is or null if not player turn - // Used to determine who can spend Hero Points. Could be the case - // a faction is active in another factions turn. + /** + * Set to faction whos turn it is or null if not player turn + * Used to determine who can spend Hero Points. Could be the case + * a player becomes active in another players turn. + */ faction_turn: FactionId | null; fronts: { a: Front; @@ -54,7 +56,13 @@ export interface Game { medallions: Record<FactionId, number[]> & { pool: Array<number | null> }; played_card: CardId | null; selected_cards: Record<FactionId, CardId[]>; + selectable_cards: CardId[]; // used for specific events tableaus: Record<FactionId, CardId[]>; + /** + * Used for event effect that allows Anarchist to put an event + * card on top of the deck + */ + top_of_events_deck: CardId | null; tracks: number[]; trash: Record<FactionId, CardId[]>; triggered_track_effects: number[]; @@ -80,6 +88,7 @@ export interface View { initiative: Game['initiative']; medallions: Game['medallions']; played_card: Game['played_card']; + selectable_cards: Game['selectable_cards']; tableaus: Game['tableaus']; tracks: number[]; triggered_track_effects: Game['triggered_track_effects']; |