summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2024-05-28 13:14:04 +0200
committerTor Andersson <tor@ccxvii.net>2024-05-30 21:59:25 +0200
commitb4b2b86d439e6bac48ab119b23d683c33988772f (patch)
tree91058f9de6fe731777055b207288f78c0f6c21cc
parent5bd0eda4db476f29fd8f29532f2f9135fb9bc295 (diff)
downloadfriedrich-b4b2b86d439e6bac48ab119b23d683c33988772f.tar.gz
offensive option
-rw-r--r--play.js16
-rw-r--r--rules.js209
2 files changed, 199 insertions, 26 deletions
diff --git a/play.js b/play.js
index be86670..04a75c8 100644
--- a/play.js
+++ b/play.js
@@ -147,9 +147,14 @@ function has_eased_victory(power) {
}
function count_total_objectives(pow) {
- if (has_eased_victory(pow))
- return objective1[pow].length
- return objective1[pow].length + objective2[pow].length
+ let n = objective1[pow].length
+ if (!has_eased_victory(pow))
+ n += objective2[pow].length
+ if (pow === P_PRUSSIA && !view.oo)
+ n = 0
+ if (pow === P_AUSTRIA && view.oo)
+ n -= 4
+ return n
}
/* CARD TEXTS */
@@ -890,11 +895,12 @@ function on_update() {
ui.turns[i].classList.toggle("hide", (typeof view.fate === "object") || (i + 1 < view.fate))
for (let pow = 0; pow < 7; ++pow) {
- // let banner = `${power_name[pow]} \u2013 ${view.pt[pow]}/${max_power_troops[pow]} troops`
let banner = `${power_name[pow]} \u2014 ${view.pt[pow]} troops`
let m_obj = count_total_objectives(pow)
if (m_obj > 0) {
let n_obj = count_captured_objectives(pow)
+ if (pow === P_AUSTRIA && view.oo)
+ m_obj += "*"
banner += ` \u2014 ${n_obj} of ${m_obj} objectives`
}
@@ -920,6 +926,8 @@ function on_update() {
}
ui.clock_of_fate.replaceChildren()
+ if (view.oo > 0)
+ ui.clock_of_fate.appendChild(ui.tc[view.oo])
ui.clock_of_fate.appendChild(ui.fate[0])
if (typeof view.fate === "object")
for (let c of view.fate)
diff --git a/rules.js b/rules.js
index ea98774..669135a 100644
--- a/rules.js
+++ b/rules.js
@@ -152,6 +152,7 @@ const all_power_depots = [
find_city_list([ "Koblenz", "Gemünden" ]),
]
+const COORDINATE_LINE_5 = 935
const MUNSTER_Y = data.cities.y[find_city("Munster")]
const HALLE = find_city("Halle")
const KUSTRIN = find_city("Küstrin")
@@ -317,6 +318,17 @@ function is_reserve(c) {
return to_suit(c) === RESERVE
}
+function is_south_of_line_5(s) {
+ return data.cities.y[s] > COORDINATE_LINE_5
+}
+
+function is_west_of(here, there) {
+ let dx = data.cities.x[there] - data.cities.x[here]
+ let dy = data.cities.y[there] - data.cities.y[here]
+ // more west than north/south
+ return dx < 0 && Math.abs(dx) >= Math.abs(dy)
+}
+
function format_cards(list) {
return list.map(c => is_reserve(c) ? suit_name[RESERVE] : to_value(c) + suit_name[to_suit(c)]).join(", ")
}
@@ -404,7 +416,11 @@ make_protect(P_HANOVER, data.country.Hanover)
make_protect(P_AUSTRIA, data.country.Austria)
function is_conquest_space(pow, s) {
- // TODO: prussian offensive option
+ if (pow === P_PRUSSIA) {
+ if (is_offensive_option() || game.turn < 3)
+ return set_has(primary_objective[pow], s)
+ return false
+ }
if (has_eased_victory(pow))
return set_has(primary_objective[pow], s)
return set_has(full_objective[pow], s)
@@ -452,9 +468,14 @@ function remove_secondary_objectives(power) {
set_delete(game.conquest, s)
}
+function remove_offensive_option_objectives() {
+ for (let s of primary_objective[P_PRUSSIA])
+ set_delete(game.conquest, s)
+}
+
/* STATE */
-function turn_power_draw() {
+function tc_per_turn() {
let n = 0
if (game.scenario === 1 && game.power === P_PRUSSIA) {
@@ -508,6 +529,15 @@ function turn_power_draw() {
return n
}
+function is_offensive_option() {
+ return !!game.oo
+}
+
+function is_offensive_option_failed() {
+ // if Austria has picked up the card AND subsidy reduction event has triggered
+ return game.oo < 0 && (set_has(game.fate, FC_POEMS) || set_has(game.fate, FC_LORD_BUTE))
+}
+
function has_power_dropped_out(pow) {
if (game.scenario === 1)
return pow !== P_PRUSSIA && pow !== P_HANOVER && pow !== P_FRANCE
@@ -843,6 +873,9 @@ function goto_action_stage() {
function end_action_stage() {
clear_undo()
+ if (check_offensive_option_victory())
+ return
+
if (++game.step === 7)
goto_clock_of_fate()
else
@@ -858,7 +891,31 @@ function has_conquered_all_of(list) {
return true
}
+function has_conquered_one_of(list) {
+ for (let s of list)
+ if (set_has(game.conquest, s))
+ return true
+ return false
+}
+
+function check_offensive_option_victory() {
+ if (game.power === P_PRUSSIA && !is_offensive_option_failed()) {
+ if (has_conquered_all_of(primary_objective[P_PRUSSIA])) {
+ goto_game_over(R_FREDERICK, "Prussia won with offensive option.")
+ return true
+ }
+ }
+ return false
+}
+
function check_power_victory(victory, city_list, power) {
+ if (power === P_AUSTRIA && is_offensive_option()) {
+ let n = count_captured_objectives(power)
+ if (n >= city_list.length - 4 && has_conquered_one_of(data.country.Saxony))
+ set_add(victory, power)
+ return
+ }
+
if (has_conquered_all_of(city_list[power]))
set_add(victory, power)
}
@@ -920,6 +977,14 @@ function has_eased_victory(power) {
return false
}
+function count_captured_objectives(pow) {
+ let n = 0
+ for (let s of full_objective[pow])
+ if (set_has(view.conquest, s))
+ ++n
+ return n
+}
+
function check_victory_4() {
// Prussian victory
if (has_russia_dropped_out() && has_sweden_dropped_out() && has_france_dropped_out()) {
@@ -969,6 +1034,8 @@ function next_tactics_deck() {
if (game.draw)
for (let c of game.draw)
held[to_deck(c)]++
+ if (game.oo > 0)
+ held[to_deck(game.oo)]++
// find next unused deck
for (let i = 1; i < 5; ++i) {
@@ -1020,13 +1087,14 @@ function draw_tc(n) {
--n
}
+ if (k > 0)
+ log("Drew " + k + " TC.")
+
return draw
}
function goto_tactical_cards() {
- let n = turn_power_draw()
-
- game.draw = draw_tc(turn_power_draw())
+ game.draw = draw_tc(tc_per_turn())
if (should_power_discard_tc() && game.draw.length > 0)
game.state = "tactical_cards_discard"
@@ -1087,6 +1155,11 @@ function end_tactical_cards() {
set_add(game.hand[game.power], c)
delete game.draw
+ if (game.scenario >= 3 && game.turn === 3 && game.power === P_PRUSSIA) {
+ goto_declare_offensive_option()
+ return
+ }
+
// MARIA: supply is before movement
goto_movement()
@@ -1765,6 +1838,11 @@ function can_re_enter_supply_train(s) {
function goto_recruit() {
game.count = 0
+ if (!can_recruit_anything_in_theory()) {
+ end_recruit()
+ return
+ }
+
game.recruit = {
pool: [],
used: [],
@@ -1773,12 +1851,6 @@ function goto_recruit() {
troops: 0,
}
- // TODO: reveal too much if we skip recruitment phase?
- if (!can_recruit_anything()) {
- end_recruit()
- return
- }
-
// if all depots have enemy pieces, choose ONE city in given sector and COST is 8
if (has_available_depot())
game.state = "recruit"
@@ -1818,6 +1890,11 @@ function is_attack_position(s) {
return false
}
+function can_recruit_anything_in_theory() {
+ let unused_everywhere = max_power_troops(game.power) - count_used_troops()
+ return unused_everywhere > 0 || count_eliminated_trains() > 0
+}
+
function can_recruit_anything() {
let unused_everywhere = max_power_troops(game.power) - count_used_troops()
let elim_trains = count_eliminated_trains()
@@ -2289,7 +2366,7 @@ function play_card(c, sign) {
function play_reserve(v, sign) {
if (fate_card_zero()) {
- log(">" + POWER_NAME[game.power] + " 0R to " + (sign * game.count))
+ log(">" + POWER_NAME[game.power] + " 0 C" + C + " to " + (sign * game.count))
clear_fate_effect()
return
}
@@ -2299,9 +2376,9 @@ function play_reserve(v, sign) {
else
game.count += v
if (bonus > 0)
- log(">" + POWER_NAME[game.power] + " " + (v-bonus) + "R +" + bonus + " to " + (sign * game.count))
+ log(">" + POWER_NAME[game.power] + " " + (v-bonus) + " C" + c + " + " + bonus + " to " + (sign * game.count))
else
- log(">" + POWER_NAME[game.power] + " " + (v) + "R to " + (sign * game.count))
+ log(">" + POWER_NAME[game.power] + " " + (v) + " C" + c + " to " + (sign * game.count))
if (bonus > 0)
clear_fate_effect()
}
@@ -2420,6 +2497,13 @@ function set_active_winner() {
set_active_defender()
}
+function set_active_loser() {
+ if (game.count > 0)
+ set_active_defender()
+ else
+ set_active_attacker()
+}
+
function remove_stack_from_combat(s) {
for (let i = game.combat.length - 2; i >= 0; i -= 2)
if (game.combat[i] === s || game.combat[i + 1] === s)
@@ -2431,6 +2515,8 @@ function goto_retreat() {
let hits = lost
let loser = get_loser()
+ let loser_power = get_stack_power(loser)
+ let winner_power = get_stack_power(get_winner())
// no more fighting for the loser
remove_stack_from_combat(loser)
@@ -2452,7 +2538,14 @@ function goto_retreat() {
}
}
- log("P" + get_supreme_commander(loser) + " lost " + (lost-hits) + " troops.")
+ log(POWER_NAME[loser_power] + " lost " + (lost-hits) + " troops.")
+
+ // OO and Prussia loses vs Austria with at least -3
+ if (game.oo > 0 && loser_power === P_PRUSSIA && winner_power === P_AUSTRIA && lost >= 3) {
+ set_active_to_power(P_AUSTRIA)
+ game.state = "pick_up_oo_card_after_retreat"
+ return
+ }
resume_retreat()
}
@@ -2837,6 +2930,10 @@ states.supply_eliminate = {
},
piece(x) {
let s = game.pos[x]
+
+ if (game.oo > 0 && game.power === P_PRUSSIA && is_south_of_line_5(s))
+ game.pick_up_oo = 1
+
for (let p of all_power_generals[game.power])
if (game.pos[p] === s)
eliminate_general(p)
@@ -2881,6 +2978,12 @@ states.supply_done = {
function end_supply() {
delete game.supply
+ if (game.pick_up_oo) {
+ set_active_to_power(P_AUSTRIA)
+ game.state = "pick_up_oo_card_after_supply"
+ return
+ }
+
end_action_stage()
}
@@ -3544,13 +3647,6 @@ states.laudon_done = {
},
}
-function is_west_of(here, there) {
- let dx = data.cities.x[there] - data.cities.x[here]
- let dy = data.cities.y[there] - data.cities.y[here]
- // more west than north/south
- return dx < 0 && Math.abs(dx) >= Math.abs(dy)
-}
-
states.prussia_may_move_hildburghausen_2_cities_westwards = {
inactive: "move Hildburghausen two cities westwards",
prompt() {
@@ -3692,6 +3788,71 @@ states.move_to_any_empty_adjacent_city = {
},
}
+/* OFFENSIVE OPTION */
+
+function goto_declare_offensive_option() {
+ set_active_to_power(P_PRUSSIA)
+ game.state = "declare_offensive_option"
+}
+
+states.declare_offensive_option = {
+ inactive: "declare offensive option",
+ prompt() {
+ prompt("You may use the Offensive Option by setting aside a TC.")
+ for (let c of game.hand[game.power])
+ gen_action_card(c)
+ view.actions.pass = 1
+ },
+ card(c) {
+ push_undo()
+ log_br()
+ log("Declared Offensive Option.")
+ log("Set aside C" + c + " for Austria.")
+ game.oo = c
+ goto_movement()
+ },
+ pass() {
+ push_undo()
+ remove_offensive_option_objectives()
+ goto_movement()
+ },
+}
+
+states.pick_up_oo_card_after_retreat = {
+ inactive: "pick up set-aside TC",
+ prompt() {
+ prompt("Pick up the set-aside TC.")
+ gen_action_card(game.oo)
+ },
+ card(_) {
+ log_br()
+ log("Austria picked up set-aside C" + game.oo + ".")
+ set_add(game.hand[P_AUSTRIA], game.oo)
+ game.oo = -1
+
+ // control back to loser for retreat
+ set_active_loser()
+ resume_retreat()
+ },
+}
+
+states.pick_up_oo_card_after_supply = {
+ inactive: "pick up set-aside TC",
+ prompt() {
+ prompt("Pick up the set-aside TC.")
+ gen_action_card(game.oo)
+ },
+ card(_) {
+ log_br()
+ log("Austria picked up set-aside C" + game.oo + ".")
+ set_add(game.hand[P_AUSTRIA], game.oo)
+ game.oo = -1
+
+ delete game.pick_up_oo
+ end_action_stage()
+ },
+}
+
/* SETUP */
const POWER_FROM_SETUP_STEP_4 = [
@@ -3836,6 +3997,8 @@ function make_tactics_deck(n) {
function make_tactics_discard(n) {
return make_tactics_deck(n).filter(c => {
+ if (c === game.oo)
+ return false
if (game.draw && set_has(game.draw, c))
return false
for (let pow of all_powers)
@@ -3860,6 +4023,7 @@ exports.setup = function (seed, scenario, options) {
step: 0,
clock: null,
fate: [],
+ oo: 0, // offensive option
vg: 0, // last victorious general for fate effect selection
fx: 0, // current card of fate effect
deck: null,
@@ -4081,6 +4245,7 @@ exports.view = function (state, player) {
conquest: game.conquest,
troops: mask_troops(player),
hand: mask_hand(player),
+ oo: game.oo,
pt: total_troops_list(),
power: game.power,