summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-05-07 17:50:39 +0200
committerTor Andersson <tor@ccxvii.net>2023-05-24 21:06:18 +0200
commit2381a17e3af7ea6d2f899568dfae11b7cdf805c7 (patch)
treec93e066e95ed6239c2fbaaba4f7d18428a87e714
parent0df5b3971cf65d23258ae080e8c1163077920fe6 (diff)
downloadred-flag-over-paris-2381a17e3af7ea6d2f899568dfae11b7cdf805c7.tar.gz
Objective Scoring and Pivotal Bonus Actions.
-rw-r--r--play.html8
-rw-r--r--play.js11
-rw-r--r--rules.js349
3 files changed, 353 insertions, 15 deletions
diff --git a/play.html b/play.html
index ea94d46..398a94b 100644
--- a/play.html
+++ b/play.html
@@ -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;
}
diff --git a/play.js b/play.js
index 95112eb..91a9afb 100644
--- a/play.js
+++ b/play.js
@@ -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")
diff --git a/rules.js b/rules.js
index 49014ac..04754b2 100644
--- a/rules.js
+++ b/rules.js
@@ -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 ===