diff options
-rw-r--r-- | play.html | 8 | ||||
-rw-r--r-- | play.js | 11 | ||||
-rw-r--r-- | rules.js | 349 |
3 files changed, 353 insertions, 15 deletions
@@ -154,6 +154,14 @@ body.Observer #set_aside_panel { display: none } drop-shadow(2px 0 0 white) } +.piece.selected { + filter: + drop-shadow(0 -2px 0 yellow) + drop-shadow(0 2px 0 yellow) + drop-shadow(-2px 0 0 yellow) + drop-shadow(2px 0 0 yellow) +} + .card.action { box-shadow: 0 0 0 3px white; } @@ -419,6 +419,7 @@ function on_update() { layout[view.pieces[i]].push(ui.cubes[i]) ui.cubes[i].classList.remove("hide") ui.cubes[i].classList.toggle("action", is_piece_action(i)) + ui.cubes[i].classList.toggle("selected", i === view.selected_cube) } else { ui.cubes[i].classList.add("hide") @@ -446,17 +447,23 @@ function on_update() { action_button("momentum", "Momentum") action_button("event", "Event") + action_button("political", "Political") action_button("military", "Military") + action_button("institutional", "Institutional") action_button("public_opinion", "Public Opinion") action_button("paris", "Paris") + action_button("forts", "Forts") + + action_button("de_escalate", "De-escalate") + action_button("spread_influence", "Spread Influence") + action_button("turncoat", "Turncoat") action_button("ops", "Operations") action_button("place", "Place") - action_button("remove", "Remove") action_button("replace", "Replace") + action_button("remove", "Remove") - action_button("end_remove", "End Remove") action_button("end_ops", "End Operations") action_button("end_event", "End Event") @@ -193,6 +193,22 @@ const PUBLIC_OPINION = [ PRESS, CATHOLIC_CHURCH, SOCIAL_MOVEMENTS ] const FORTS = [ MONT_VALERIEN, FORT_D_ISSY, CHATEAU_DE_VINCENNES ] const PARIS = [ BUTTE_MONTMARTRE, BUTTE_AUX_CAILLES, PERE_LACHAISE ] +const PIVOTAL = [ NATIONAL_ASSEMBLY, PRESS, MONT_VALERIEN, BUTTE_MONTMARTRE ] + +const DIMENSION_SPACES = [ + INSTITUTIONAL, INSTITUTIONAL, INSTITUTIONAL, + PUBLIC_OPINION, PUBLIC_OPINION, PUBLIC_OPINION, + FORTS, FORTS, FORTS, + PARIS, PARIS, PARIS, +] + +const DIMENSION_NAME = [ + "Institutional", "Institutional", "Institutional", + "Public Opinion", "Public Opinion", "Public Opinion", + "Forts", "Forts", "Forts", + "Paris", "Paris", "Paris", +] + const ADJACENT_TO = [ [ ROYALISTS, REPUBLICANS ], [ PRESS, CATHOLIC_CHURCH ], @@ -249,6 +265,41 @@ function is_military_space(s) { ) } +const OBJECTIVE_SPACE = [ + BUTTE_MONTMARTRE, + BUTTE_AUX_CAILLES, + PERE_LACHAISE, + FORT_D_ISSY, + MONT_VALERIEN, + CHATEAU_DE_VINCENNES, + PRESS, + CATHOLIC_CHURCH, + SOCIAL_MOVEMENTS, + ROYALISTS, + REPUBLICANS, + NATIONAL_ASSEMBLY, +] + +function objective_card_space(c) { + return OBJECTIVE_SPACE[c - 42] +} + +function commune_objective_card() { + return game.red_objective[0] +} + +function versailles_objective_card() { + return game.blue_objective[0] +} + +function commune_objective_space() { + return objective_card_space(commune_objective_card()) +} + +function versailles_objective_space() { + return objective_card_space(versailles_objective_card()) +} + // === GAME STATE === function discard_card(c) { @@ -612,6 +663,13 @@ function can_place_cube_in_any(list) { return false } +function can_replace_cube_in_any(list) { + for (let s of list) + if (can_replace_cube(s)) + return true + return false +} + function can_place_cube(s) { return find_available_cube() >= 0 && count_friendly_cubes(s) < 4 } @@ -661,6 +719,12 @@ function place_disc(s) { game.pieces[find_available_disc()] = s } +function replace_cube(p) { + let s = game.pieces[p] + remove_piece(p) + place_cube(s) +} + function find_available_cube() { let p = -1 if (game.active === COMMUNE) { @@ -877,12 +941,12 @@ states.strategy_phase = { for (let c of player_hand()) gen_action_card(c) - if (player_hand().length > 0) { + if (player_hand().length > 1) { let final = player_final_crisis_card() if (final > 0) gen_action_card(final) - if (game.discard > 0) + if (game.discard > 0 && !is_neutral_card(game.discard)) if (can_play_event(game.discard)) gen_action_card(game.discard) } @@ -1396,9 +1460,253 @@ function goto_set_aside_cards() { goto_pivotal_space_bonus_actions() } +// === PIVOTAL SPACE BONUS ACTIONS === + function goto_pivotal_space_bonus_actions() { - game.state = "pivotal_space_bonus_actions" + update_presence_and_control() + game.spaces = PIVOTAL.filter(s => is_commune_control(s) || is_versailles_control(s)) + resume_pivotal_space_bonus_actions() +} + +function resume_pivotal_space_bonus_actions() { + assess_crisis_breach_all() game.active = game.initiative + if (game.spaces.length > 0) + game.state = "pivotal_space_bonus_actions" + else + goto_objective_card_scoring() +} + +states.pivotal_space_bonus_actions = { + prompt() { + view.prompt = "Perform Pivotal Space bonus actions." + for (let s of game.spaces) + gen_action_space(s) + }, + space(s) { + goto_bonus_action(s) + }, +} + +function goto_bonus_action(s) { + log_h2(space_names[s] + " Bonus Action") + array_remove_item(game.spaces, s) + game.where = s + game.state = "bonus_action" + if (is_commune_control(s)) + game.active = COMMUNE + else + game.active = VERSAILLES +} + +states.bonus_action = { + prompt() { + let dimension = DIMENSION_SPACES[game.where] + view.prompt = "Bonus Action in " + DIMENSION_NAME[game.where] + "." + view.where = game.where + view.actions.de_escalate = 1 + view.actions.spread_influence = 1 + if (can_replace_cube_in_any(dimension)) + view.actions.turncoat = 1 + else + view.actions.turncoat = 0 + }, + de_escalate() { + push_undo() + game.state = "de_escalate_1" + }, + spread_influence() { + push_undo() + game.who = -1 + game.count = 2 + game.state = "spread_influence" + }, + turncoat() { + push_undo() + game.state = "turncoat" + }, +} + +states.de_escalate_1 = { + prompt() { + let dimension = DIMENSION_SPACES[game.where] + view.prompt = "De-escalate in " + DIMENSION_NAME[game.where] + ": Remove your own cube." + for (let s of dimension) + for_each_friendly_cube(s, gen_action_piece) + }, + piece(p) { + push_undo() + remove_piece(p) + game.state = "de_escalate_2" + }, +} + +states.de_escalate_2 = { + prompt() { + let dimension = DIMENSION_SPACES[game.where] + view.prompt = "De-escalate in " + DIMENSION_NAME[game.where] + ": Remove your own or opponent's cube." + for (let s of dimension) { + for_each_enemy_cube(s, gen_action_piece) + for_each_friendly_cube(s, gen_action_piece) + } + }, + piece(p) { + push_undo() + remove_piece(p) + resume_pivotal_space_bonus_actions() + }, +} + +states.spread_influence = { + prompt() { + let dimension = DIMENSION_SPACES[game.where] + view.prompt = "Spread Influence in " + DIMENSION_NAME[game.where] + "." + view.selected_cube = game.who + if (game.who < 0) { + // TODO: don't move same cube twice! + for (let s of dimension) + for_each_friendly_cube(s, gen_action_piece) + } else { + for (let s of dimension) + if (game.pieces[game.who] !== s) + gen_action_space(s) + } + view.actions.skip = 1 + }, + piece(p) { + push_undo() + game.who = p + }, + space(s) { + place_piece(game.who, s) + game.who = -1 + if (--game.count === 0) + resume_pivotal_space_bonus_actions() + }, + skip() { + resume_pivotal_space_bonus_actions() + } +} + +states.turncoat = { + prompt() { + let dimension = DIMENSION_SPACES[game.where] + view.prompt = "Turncoat in " + DIMENSION_NAME[game.where] + ": Replace an opponent's cube." + for (let s of dimension) + for_each_enemy_cube(s, gen_action_piece) + }, + piece(p) { + push_undo() + replace_cube(p) + resume_pivotal_space_bonus_actions() + }, +} + +// === OBJECTIVE CARD SCORING === + +function goto_objective_card_scoring() { + update_presence_and_control() + game.active = game.initiative + game.count = 3 + game.state = "objective_card_scoring" + log("Commune Objective:") + logi("C" + commune_objective_card()) + log("Versailles Objective:") + logi("C" + versailles_objective_card()) +} + +states.objective_card_scoring = { + prompt() { + view.prompt = "Objective Card Scoring!" + if (game.active === COMMUNE) + view.objective = [ commune_objective_card(), versailles_objective_card() ] + else + view.objective = [ versailles_objective_card(), commune_objective_card() ] + if (game.count & 1) + gen_action_space(commune_objective_space()) + if (game.count & 2) + gen_action_space(versailles_objective_space()) + }, + space(s) { + if (is_political_space(s)) { + if (is_commune_control(s)) + add_political_vp(COMMUNE, 1) + else if (is_versailles_control(s)) + add_political_vp(VERSAILLES, 1) + else + log("Nobody controlled S" + s + ".") + } else { + if (is_commune_control(s)) + add_military_vp(COMMUNE, 1) + else if (is_versailles_control(s)) + add_military_vp(VERSAILLES, 1) + else + log("Nobody controlled S" + s + ".") + } + if (s === commune_objective_space()) + game.count ^= 1 + if (s === versailles_objective_space()) + game.count ^= 2 + if (game.count === 0) + game.state = "objective_card_events" + }, +} + +function resume_objective_card_events() { + let c = commune_objective_card() + let v = versailles_objective_card() + if (c || v) + game.state = "objective_card_events" + else + start_round() +} + +states.objective_card_events = { + prompt() { + view.prompt = "Objective Card Events!" + + let c = commune_objective_card() + let v = versailles_objective_card() + if (c && v) { + if (game.active === COMMUNE) + view.objective = [ c, v ] + else + view.objective = [ v, c ] + } else if (c) + view.objective = [ c ] + else if (v) + view.objective = [ v ] + else + view.objective = [ ] + + if (c) + gen_action_card(c) + if (v) + gen_action_card(v) + }, + card(c) { + let s = objective_card_space(c) + if (c === commune_objective_card()) { + game.red_objective = [] + if (is_commune_control(s)) { + game.red_fulfilled += 1 + game.active = COMMUNE + goto_play_event(c) + } else { + resume_objective_card_events() + } + } + if (c === versailles_objective_card()) { + game.blue_objective = [] + if (is_versailles_control(s)) { + game.blue_fulfilled += 1 + game.active = VERSAILLES + goto_play_event(c) + } else { + resume_objective_card_events() + } + } + }, } // === EVENTS === @@ -1409,8 +1717,13 @@ function goto_play_event(c) { } function end_event() { - game.vm = null - end_card_play() + if (is_objective_card(game.vm.fp)) { + game.vm = null + resume_objective_card_events() + } else { + game.vm = null + end_card_play() + } } function goto_vm(proc) { @@ -1853,9 +2166,7 @@ states.vm_replace = { }, piece(p) { push_undo() - let s = game.pieces[p] - remove_piece(p) - place_cube(s) + replace_cube(p) if (--game.vm.count === 0 || !can_vm_replace()) vm_next() }, @@ -1917,10 +2228,10 @@ exports.setup = function (seed, scenario, options) { scenario: scenario, log: [], undo: [], - active: "Both", - state: "choose_objective_card", + active: null, + state: null, - round: 1, + round: 0, initiative: null, political_vp: 0, military_vp: 0, @@ -1935,11 +2246,13 @@ exports.setup = function (seed, scenario, options) { red_hand: [], red_set_aside: [], red_objective: [], + red_fulfilled: 0, blue_final: 17, blue_hand: [], blue_set_aside: [], blue_objective: [], + blue_fulfilled: 0, presence: 0, control: 0, @@ -1997,7 +2310,6 @@ exports.setup = function (seed, scenario, options) { } log_h1("Red Flag Over Paris") - log_h1("Round 1") for (let c = 1; c <= 41; ++c) if (c !== 17 && c !== 34) @@ -2009,6 +2321,16 @@ exports.setup = function (seed, scenario, options) { shuffle(game.strategy_deck) shuffle(game.objective_deck) + start_round() + + return game +} + +function start_round() { + game.round += 1 + + log_h1("Round " + game.round) + let n = 4 if (game.scenario === "Censorship") n = 5 @@ -2023,7 +2345,8 @@ exports.setup = function (seed, scenario, options) { game.blue_objective.push(game.objective_deck.pop()) } - return game + game.active = "Both" + game.state = "choose_objective_card" } // === VIEW === |