summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js2823
1 files changed, 2704 insertions, 119 deletions
diff --git a/rules.js b/rules.js
index 953c8d5..f5969b9 100644
--- a/rules.js
+++ b/rules.js
@@ -1,5 +1,6 @@
"use strict"
+/*
var game, view
// piece list:
@@ -94,7 +95,7 @@ const COOPERATE = [
[ P_AUSTRIA, P_PRAGMATIC ],
]
-exports.setup = function (seed, scenario, options) {
+exports.setup = function (seed, _scenario, _options) {
game = {
seed: seed,
log: [],
@@ -110,8 +111,6 @@ exports.setup = function (seed, scenario, options) {
str: [],
}
- /* SETUP
-
Austria 6 Malmedy
Austria T Geel
@@ -152,33 +151,2704 @@ exports.setup = function (seed, scenario, options) {
Pragmatic 1 Delfzijl
Pragmatic 2 Delfzijl
- */
return game
}
+*/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"use strict"
+
+const R_LOUIS_XV = "Louis XV"
+const R_FREDERICK = "Frederick"
+const R_MARIA_THERESA = "Maria Theresa"
+
+exports.roles = [ R_LOUIS_XV, R_FREDERICK, R_MARIA_THERESA ]
+
+exports.scenarios = [ "Advanced" ]
+
+/* DATA */
+
+var game
+var view
+var states = {}
+
+const data = require("./data")
+
+function find_city(city) {
+ let n = data.cities.name.length
+ let x = -1
+ for (let c = 0; c < n; ++c) {
+ if (data.cities.name[c] === city) {
+ if (x < 0)
+ x = c
+ else
+ throw "TWO CITIES: " + city
+ }
+ }
+ if (x < 0)
+ throw "CITY NOT FOUND: " + city
+ return x
+}
+
+function find_city_list(names) {
+ let list = []
+ for (let n of names)
+ set_add(list, find_city(n))
+ return list
+}
+
+const deck_name = [ "red", "green", "blue", "yellow" ]
+const suit_name = [ "\u2660", "\u2663", "\u2665", "\u2666", "R" ]
+
+const P_FRANCE = 0
+const P_BAVARIA = 1
+const P_PRUSSIA = 2
+const P_SAXONY = 3
+const P_PRAGMATIC = 4
+const P_AUSTRIA = 5
+
+const POWER_NAME = [ "France", "Bavaria", "Prussia", "Saxony", "Pragmatic Army", "Austria" ]
+
+const SPADES = 0
+const CLUBS = 1
+const HEARTS = 2
+const DIAMONDS = 3
+const RESERVE = 4
+
+const IMPERIAL_ELECTION = 25
+
+const ELIMINATED = data.cities.name.length
+const REMOVED = ELIMINATED + 1
+
+const max_power_troops = [ 5*8, 1*8, 4*8, 1*8, 3*8, 6*8 ]
+
+const all_powers = [ 0, 1, 2, 3, 4, 5 ]
+
+const all_home_or_depot_cities = [
+]
+
+const all_power_depots = [
+]
+
+const all_power_re_entry_cities = [
+]
+
+const all_power_generals = [
+ [ 0, 1, 2, 3, 4 ],
+ [ 5 ],
+ [ 6, 7, 8, 9 ],
+ [ 10 ],
+ [ 11, 12, 13 ],
+ [ 14, 15, 16, 17, 18, 19 ],
+]
+
+const all_power_trains = [
+ [ 20, 21 ],
+ [ 22 ],
+ [ 23, 24 ],
+ [ 25 ],
+ [ 26 ],
+ [ 27, 28, 29 ],
+]
+
+const all_hussars = [ 30, 31 ]
+
+const piece_power = [
+ P_FRANCE, P_FRANCE, P_FRANCE, P_FRANCE, P_FRANCE,
+ P_BAVARIA,
+ P_PRUSSIA, P_PRUSSIA, P_PRUSSIA, P_PRUSSIA,
+ P_SAXONY,
+ P_PRAGMATIC, P_PRAGMATIC, P_PRAGMATIC,
+ P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA, P_AUSTRIA,
+ P_FRANCE, P_FRANCE,
+ P_BAVARIA,
+ P_PRUSSIA, P_PRUSSIA,
+ P_SAXONY,
+ P_PRAGMATIC,
+ P_AUSTRIA, P_AUSTRIA, P_AUSTRIA,
+ P_AUSTRIA, P_AUSTRIA
+]
+
+const piece_name = [
+ "Moritz",
+ "Belle-Isle",
+ "Broglie",
+ "Maillebois",
+ "Noailles",
+ "Törring",
+ "Friedrich",
+ "Schwerin",
+ "Leopold",
+ "Dessauer",
+ "Rutowski",
+ "George II",
+ "Cumberland",
+ "Earl of Stair",
+ "Karl",
+ "Traun",
+ "Khevenhüller",
+ "Batthyány",
+ "Neipperg",
+ "Arenberg",
+ "supply train", "supply train",
+ "supply train",
+ "supply train", "supply train",
+ "supply train",
+ "supply train",
+ "supply train", "supply train", "supply train",
+ "hussar", "hussar",
+]
+
+const all_power_generals_rev = all_power_generals.map(list => list.slice().reverse())
+
+const all_pieces = [ ...all_power_generals.flat(), ...all_power_trains.flat() ]
+const all_generals = [ ...all_power_generals.flat() ]
+
+const all_france_generals = [
+ ...all_power_generals[P_FRANCE],
+ ...all_power_generals[P_BAVARIA],
+]
+
+const all_prussia_generals = [
+ ...all_power_generals[P_PRUSSIA],
+ ...all_power_generals[P_SAXONY],
+ ...all_power_generals[P_PRAGMATIC],
+]
+
+const all_austria_generals = [
+ ...all_power_generals[P_AUSTRIA],
+]
+
+const all_france_trains = [
+ ...all_power_trains[P_FRANCE],
+ ...all_power_trains[P_BAVARIA],
+]
+
+const all_prussia_trains = [
+ ...all_power_trains[P_PRUSSIA],
+ ...all_power_trains[P_SAXONY],
+ ...all_power_trains[P_PRAGMATIC],
+]
+
+const all_austria_trains = [
+ ...all_power_trains[P_AUSTRIA],
+]
+
+function is_general(p) {
+ return p < 20
+}
+
+function is_supply_train(p) {
+ return p >= 20 && p < 30
+}
+
+function is_hussar(p) {
+ return p >= 30 && p < 32
+}
+
+function to_deck(c) {
+ return c >> 7
+}
+
+function to_suit(c) {
+ return (c >> 4) & 7
+}
+
+function to_value(c) {
+ if (to_suit(c) === RESERVE)
+ return 8
+ return c & 15
+}
+
+function format_card(c) {
+ if (is_reserve(c))
+ return "8R"
+ return to_value(c) + suit_name[to_suit(c)]
+}
+
+function is_reserve(c) {
+ return to_suit(c) === RESERVE
+}
+
+function format_cards(list) {
+ if (list.length > 0)
+ return list.map(format_card).join(", ")
+ return "nothing"
+}
+
+function format_selected() {
+ if (game.selected.length === 0)
+ return "nobody"
+ return game.selected.map(p => piece_name[p]).join(" and ")
+}
+
+function log_selected() {
+ log(game.selected.map(p => "P" + p).join(" and "))
+}
+
+/* OBJECTIVES */
+
+const all_objectives = []
+set_add_all(all_objectives, data.type.major_fortress)
+set_add_all(all_objectives, data.type.minor_fortress)
+
+const protect_range = []
+for (let s of all_objectives)
+ make_protect_range(protect_range[s] = [], s, s, 3)
+
+function make_protect_range(result, start, here, range) {
+ for (let next of data.cities.adjacent[here]) {
+ if (next !== start)
+ set_add(result, next)
+ if (range > 1)
+ make_protect_range(result, start, next, range - 1)
+ }
+}
+
+function is_conquest_space(_pow, s) {
+ return set_has(all_objectives, s)
+}
+
+function is_reconquest_space(_pow, s) {
+ return set_has(all_objectives, s)
+}
+
+function is_space_protected_by_piece(s, p) {
+ return set_has(protect_range[s], game.pos[p])
+}
+
+function is_protected_from_conquest(s) {
+ for (let pow of all_powers) {
+ for (let p of all_power_generals[pow])
+ if (is_space_protected_by_piece(s, p))
+ return true
+ }
+ return false
+}
+
+function is_protected_from_reconquest(s) {
+ for (let pow of all_powers) {
+ for (let p of all_power_generals[pow])
+ if (is_space_protected_by_piece(s, p))
+ return true
+ }
+ return false
+}
+
+/* STATE */
+
+const tc_per_turn_table = [ 5, 1, 3, 1, 3, 5 ]
+
+function tc_per_turn() {
+ let n = tc_per_turn_table[game.power]
+
+ // TODO: subsidies
+
+ return n
+}
+
+const player_from_power_table = [
+ R_LOUIS_XV,
+ R_LOUIS_XV,
+ R_FREDERICK,
+ R_FREDERICK,
+ R_FREDERICK,
+ R_MARIA_THERESA,
+]
+
+function player_from_power(pow) {
+ // TOOD: saxony allies with austria
+ return player_from_power_table[pow]
+}
+
+function set_active_to_power(power) {
+ game.power = power
+ game.active = current_player()
+}
+
+function current_player() {
+ return player_from_power(game.power)
+}
+
+function get_top_piece(s) {
+ for (let p of all_pieces)
+ if (game.pos[p] === s)
+ return p
+ return -1
+}
+
+function get_supreme_commander(s) {
+ // TODO: promoted minor power
+ for (let p of all_generals)
+ if (game.pos[p] === s)
+ return p
+ return -1
+}
+
+function get_stack_power(s) {
+ return piece_power[get_supreme_commander(s)]
+}
+
+function is_space_suit(s, ranges) {
+ for (let [a, b] of ranges)
+ if (s >= a && s <= b)
+ return true
+ return false
+}
+
+function get_space_suit(s) {
+ if (s >= ELIMINATED)
+ return SPADES
+ if (is_space_suit(s, data.suit.spades))
+ return SPADES
+ if (is_space_suit(s, data.suit.clubs))
+ return CLUBS
+ if (is_space_suit(s, data.suit.hearts))
+ return HEARTS
+ if (is_space_suit(s, data.suit.diamonds))
+ return DIAMONDS
+ throw "IMPOSSIBLE"
+}
+
+function count_eliminated_trains() {
+ let n = 0
+ for (let p of all_power_trains[game.power])
+ if (game.pos[p] === ELIMINATED)
+ ++n
+ return n
+}
+
+function count_eliminated_generals() {
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === ELIMINATED)
+ ++n
+ return n
+}
+
+function count_used_troops() {
+ let current = 0
+ for (let p of all_power_generals[game.power])
+ current += game.troops[p]
+ return current
+}
+
+function count_unused_troops_on_map() {
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] < ELIMINATED)
+ n += 8 - game.troops[p]
+ return n
+}
+
+function count_unused_generals() {
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] !== REMOVED && game.troops[p] === 0)
+ ++n
+ return n
+}
+
+function has_any_piece(to) {
+ for (let s of game.pos)
+ if (s === to)
+ return true
+ return false
+}
+
+function has_friendly_supply_train(to) {
+ for (let p of all_friendly_trains[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_enemy_supply_train(to) {
+ for (let p of all_enemy_trains[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_enemy_general(to) {
+ for (let p of all_enemy_generals[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_enemy_piece(to) {
+ for (let p of all_enemy_generals[game.power])
+ if (game.pos[p] === to)
+ return true
+ for (let p of all_enemy_trains[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_any_other_general(to) {
+ for (let other of all_powers)
+ if (other !== game.power)
+ for (let p of all_power_generals[other])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_own_general(to) {
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function count_generals(to) {
+ let n = 0
+ for (let p of all_generals)
+ if (game.pos[p] === to)
+ ++n
+ return n
+}
+
+function select_stack(s) {
+ let list = []
+ for (let p of all_generals)
+ if (game.pos[p] === s)
+ list.push(p)
+ return list
+}
+
+function add_one_troop(p) {
+ for (let x of all_power_generals[game.power]) {
+ if (game.pos[x] === game.pos[p] && game.troops[x] < 8) {
+ game.troops[x] ++
+ break
+ }
+ }
+}
+
+function remove_one_troop(p) {
+ for (let x of all_power_generals_rev[game.power]) {
+ if (game.pos[x] === game.pos[p] && game.troops[x] > 1) {
+ game.troops[x] --
+ break
+ }
+ }
+}
+
+function retire_general(p) {
+ // save troops if possible
+ let s = game.pos[p]
+ let n = game.troops[p]
+ game.pos[p] = REMOVED
+ game.troops[p] = 0
+ set_in_supply(p)
+
+ if (s < ELIMINATED) {
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === s) {
+ let x = Math.min(n, 8 - game.troops[p])
+ game.troops[p] += x
+ n -= x
+ }
+ }
+ if (n > 1)
+ log("P" + p + " retired with " + n + " troops.")
+ else if (n === 1)
+ log("P" + p + " retired with 1 troop.")
+ else
+ log("P" + p + " retired.")
+ } else {
+ log("P" + p + " retired.")
+ }
+}
+
+function eliminate_general(p) {
+ log(">P" + p + " eliminated")
+ game.pos[p] = ELIMINATED
+ game.troops[p] = 0
+ set_in_supply(p)
+}
+
+function eliminate_train(p) {
+ log("P" + p + " eliminated.")
+ game.pos[p] = ELIMINATED
+}
+
+/* SEQUENCE OF PLAY */
+
+const POWER_FROM_ACTION_STEP = [
+ P_FRANCE, // and bavaria
+ P_PRUSSIA, // and saxony
+ P_AUSTRIA, // and pragmatic army
+ P_PRAGMATIC, // moves interleaved with austria, but attacks after austria
+]
+
+function set_active_to_current_action_step() {
+ set_active_to_power(POWER_FROM_ACTION_STEP[game.step])
+}
+
+function goto_start_turn() {
+ game.turn += 1
+ game.step = 0
+
+ game.selected = null
+ delete game.ia_lost
+
+ // MARIA: politics
+ // MARIA: hussars
+
+ goto_action_stage()
+}
+
+function goto_action_stage() {
+ set_active_to_current_action_step()
+
+ clear_undo()
+
+ log("=" + game.power)
+ goto_tactical_cards()
+}
+
+function end_action_stage() {
+ clear_undo()
+
+ if (++game.step === 7)
+ goto_clock_of_fate()
+ else
+ goto_action_stage()
+}
+
+/* VICTORY */
+
+function check_victory() {
+ // TODO
+ return false
+}
+
+/* TACTICAL CARDS */
+
+function find_largest_discard(u) {
+ for (let i = 0; i < 4; ++i)
+ if (u[i] <= u[0] && u[i] <= u[1] && u[i] <= u[2] && u[i] <= u[3])
+ return i
+ throw "OUT OF CARDS"
+}
+
+function next_tactics_deck() {
+ let held = [ 0, 0, 0, 0, 0 ]
+
+ // count cards in hands
+ for (let pow of all_powers) {
+ for (let c of game.hand[pow])
+ held[to_deck(c)]++
+ }
+ 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) {
+ if (held[i] === 0) {
+ game.deck = make_tactics_deck(i)
+ shuffle_bigint(game.deck)
+ log("Shuffled " + deck_name[i] + ".")
+ return
+ }
+ }
+
+ // find two largest discard piles
+ let a = find_largest_discard(held)
+ if (held[a] === 38)
+ return
+ held[a] = 100
+
+ let b = find_largest_discard(held)
+ if (held[b] === 38)
+ return
+
+ log("Shuffled " + deck_name[a] + " and " + deck_name[b] + ".")
+
+ game.deck = [
+ make_tactics_discard(a),
+ make_tactics_discard(b)
+ ].flat()
+
+ shuffle_bigint(game.deck)
+}
+
+function draw_tc(n) {
+ game.draw = []
+
+ let k = 0
+ while (n > 0) {
+ if (game.deck.length === 0) {
+ if (k > 0)
+ log("Drew " + k + " TC.")
+ k = 0
+ next_tactics_deck()
+ if (game.deck.length === 0) {
+ log("The cards ran out!")
+ break
+ }
+ }
+ set_add(game.draw, game.deck.pop())
+ ++k
+ --n
+ }
+
+ if (k > 0)
+ log("Drew " + k + " TC.")
+}
+
+function goto_tactical_cards() {
+
+ // TODO: no TC (even subsidy) if major fortress is enemy controlled
+
+ draw_tc(tc_per_turn())
+
+ game.state = "tactical_cards_show"
+}
+
+states.tactical_cards_show = {
+ inactive: "draw tactical cards",
+ prompt() {
+ view.draw = game.draw
+ prompt("Draw " + format_cards(game.draw) + ".")
+ view.actions.end_cards = 1
+ },
+ end_cards() {
+ end_tactical_cards()
+ },
+}
+
+function end_tactical_cards() {
+ for (let c of game.draw)
+ set_add(game.hand[game.power], c)
+ delete game.draw
+
+ // MARIA: supply is before movement
+
+ goto_movement()
+}
+
+/* TRANSFER TROOPS */
+
+function count_stacked_take() {
+ let n = 0
+ for (let p of game.selected)
+ n += 8 - game.troops[p]
+ return n
+}
+
+function count_unstacked_take() {
+ let here = game.pos[game.selected[0]]
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === here && !set_has(game.selected, p))
+ n += 8 - game.troops[p]
+ return n
+}
+
+function count_stacked_give() {
+ let n = 0
+ for (let p of game.selected)
+ n += game.troops[p] - 1
+ return n
+}
+
+function count_unstacked_give() {
+ let here = game.pos[game.selected[0]]
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === here && !set_has(game.selected, p))
+ n += game.troops[p] - 1
+ return n
+}
+
+function take_troops(total) {
+ let here = game.pos[game.selected[0]]
+
+ let n = total
+ for (let p of game.selected) {
+ let x = Math.max(0, Math.min(n, 8 - game.troops[p]))
+ game.troops[p] += x
+ n -= x
+ }
+
+ n = total
+ for (let p of all_power_generals_rev[game.power]) {
+ if (game.pos[p] === here && !set_has(game.selected, p)) {
+ let x = Math.max(0, Math.min(n, game.troops[p] - 1))
+ game.troops[p] -= x
+ n -= x
+ }
+ }
+}
+
+function give_troops(total) {
+ let here = game.pos[game.selected[0]]
+
+ let n = total
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === here && !set_has(game.selected, p)) {
+ let x = Math.max(0, Math.min(n, 8 - game.troops[p]))
+ game.troops[p] += x
+ n -= x
+ }
+ }
+
+ n = total
+ for (let p of game.selected) {
+ let x = Math.max(0, Math.min(n, game.troops[p] - 1))
+ game.troops[p] -= x
+ n -= x
+ }
+}
+
+/* MOVEMENT */
+
+function movement_range() {
+ return 3
+}
+
+function goto_movement() {
+ game.state = "movement"
+ set_clear(game.moved)
+
+ log_br()
+
+ game.move_conq = []
+ game.move_reconq = []
+}
+
+function can_train_move_anywhere(p) {
+ let from = game.pos[p]
+ for (let to of data.cities.adjacent[from])
+ if (can_move_train_to(to))
+ return true
+ return false
+}
+
+function can_general_move_anywhere(p) {
+ let from = game.pos[p]
+ for (let to of data.cities.adjacent[from])
+ if (can_move_general_in_theory(p, to))
+ return true
+ return false
+}
+
+states.movement = {
+ inactive: "move",
+ prompt() {
+ let done_generals = true
+ let done_trains = true
+
+ for (let p of all_power_generals[game.power]) {
+ if (!set_has(game.moved, p) && game.pos[p] < ELIMINATED) {
+ if (can_general_move_anywhere(p)) {
+ gen_action_supreme_commander(game.pos[p])
+ done_generals = false
+ }
+ }
+ }
+
+ for (let p of all_power_trains[game.power]) {
+ if (!set_has(game.moved, p) && game.pos[p] < ELIMINATED) {
+ if (can_train_move_anywhere(p)) {
+ gen_action_piece(p)
+ done_trains = false
+ }
+ }
+ }
+
+ if (done_trains && done_generals)
+ prompt("Movement done.")
+ else if (done_generals && !done_trains)
+ prompt("Move your supply trains.")
+ else if (!done_generals && done_trains)
+ prompt("Move your generals.")
+ else
+ prompt("Move your generals and supply trains.")
+
+ if (done_trains && done_generals)
+ view.actions.end_movement = 1
+ else
+ // TODO view.actions.confirm_end_movement = 1
+ view.actions.end_movement = 1
+ },
+ piece(p) {
+ push_undo()
+
+ let here = game.pos[p]
+
+ if (is_general(p)) {
+ game.selected = []
+ for (let other of all_power_generals[game.power])
+ if (other >= p && game.pos[other] === here && !set_has(game.moved, other))
+ game.selected.push(other)
+ } else {
+ game.selected = [ p ]
+ }
+
+ game.count = 0
+
+ if (data.cities.major_roads[here].length > 0)
+ game.major = 1
+ else
+ game.major = 0
+
+ if (is_supply_train(p))
+ game.state = "move_supply_train"
+ else
+ game.state = "move_general"
+ },
+ confirm_end_movement() {
+ this.end_movement()
+ },
+ end_movement() {
+ push_undo()
+
+ if (game.moved.length === 0)
+ log("Nothing moved.")
+
+ set_clear(game.moved)
+
+ log_conquest(game.move_conq, game.move_reconq)
+ delete game.move_conq
+ delete game.move_reconq
+
+ goto_recruit()
+ },
+}
+
+function format_move(max) {
+ let n = max - game.count
+ if (game.major)
+ return ` up to ${n} cities (${n+1} on main roads).`
+ return ` up to ${n} cities.`
+}
+
+function can_move_train_to(to) {
+ return !has_any_piece(to)
+}
+
+function can_move_general_in_theory(_p, to) {
+ if (has_friendly_supply_train(to))
+ return false
+ if (has_any_other_general(to))
+ return false
+ if (has_enemy_supply_train(to))
+ return false
+ if (count_generals(to) >= 2)
+ return false
+ return true
+}
+
+function can_move_general_to(to) {
+ if (has_friendly_supply_train(to))
+ return false
+ if (has_any_other_general(to))
+ return false
+ if (has_enemy_supply_train(to))
+ return false
+ if (game.selected.length + count_generals(to) > 2)
+ return false
+ return true
+}
+
+function move_general_to(to) {
+ let pow = game.power
+ let who = game.selected[0]
+ let from = game.pos[who]
+ let stop = false
+
+ for (let p of game.selected) {
+ set_add(game.moved, p)
+ game.pos[p] = to
+ }
+
+ // uniting stacks (turn all oos if one is oos)
+ let oos = false
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === to && is_out_of_supply(p))
+ oos = true
+ if (oos)
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === to)
+ set_out_of_supply(p)
+
+ // conquer space
+ if (is_conquest_space(pow, from) && !set_has(game.conquest, from)) {
+ if (is_protected_from_conquest(from)) {
+ set_add(game.retro, from)
+ } else {
+ game.move_conq.push(from)
+ set_add(game.conquest, from)
+ }
+ }
+
+ // re-conquer space
+ if (is_reconquest_space(pow, from) && set_has(game.conquest, from)) {
+ if (is_protected_from_reconquest(from)) {
+ set_add(game.retro, from)
+ } else {
+ game.move_reconq.push(from)
+ set_delete(game.conquest, from)
+ }
+ }
+
+ // eliminate supply train
+ for (let p of all_enemy_trains[pow]) {
+ if (game.pos[p] === to) {
+ eliminate_train(p)
+ stop = true
+ }
+ }
+
+ // uniting stacks: flag all as moved and stop moving
+ for (let p of all_power_generals[pow]) {
+ if (game.pos[p] === to && !set_has(game.selected, p)) {
+ set_add(game.moved, p)
+ stop = true
+ }
+ }
+
+ return stop
+}
+
+states.move_supply_train = {
+ inactive: "move",
+ prompt() {
+ prompt("Move supply train" + format_move(2))
+ view.selected = game.selected
+
+ let who = game.selected[0]
+ let here = game.pos[who]
+
+ if (game.count < 2 + game.major)
+ for (let next of data.cities.major_roads[here])
+ if (!has_any_piece(next))
+ gen_action_space(next)
+ if (game.count < 2)
+ for (let next of data.cities.roads[here])
+ if (!has_any_piece(next))
+ gen_action_space(next)
+
+ if (game.count > 0)
+ gen_action_piece(who)
+ view.actions.stop = 1
+ },
+ piece(_) {
+ this.stop()
+ },
+ stop() {
+ let who = game.selected[0]
+ set_add(game.moved, who)
+ end_move_piece()
+ },
+ space(to) {
+ let who = game.selected[0]
+ let from = game.pos[who]
+
+ if (game.count === 0) {
+ log_selected()
+ log(">from S" + from)
+ }
+
+ log(">to S" + to)
+
+ if (!set_has(data.cities.major_roads[from], to))
+ game.major = 0
+
+ set_add(game.moved, who)
+ game.pos[who] = to
+
+ if (++game.count === 2 + game.major)
+ end_move_piece()
+ },
+}
+
+states.move_general = {
+ inactive: "move",
+ prompt() {
+ prompt("Move " + format_selected() + format_move(movement_range()))
+ view.selected = game.selected
+
+ let who = game.selected[0]
+ let here = game.pos[who]
+
+ if (game.count === 0) {
+ if (game.selected.length > 1) {
+ for (let p of game.selected) {
+ gen_action_piece(p)
+ gen_action_detach(p)
+ }
+ }
+
+ let s_take = count_stacked_take()
+ let s_give = count_stacked_give()
+ let u_take = count_unstacked_take()
+ let u_give = count_unstacked_give()
+
+ if (s_take > 0 && u_give > 0)
+ view.actions.take = 1
+ if (s_give > 0 && u_take > 0)
+ view.actions.give = 1
+
+ view.actions.stop = 1
+ } else {
+ gen_action_piece(who)
+ view.actions.stop = 1
+ }
+
+ if (game.count < movement_range() + game.major)
+ for (let next of data.cities.major_roads[here])
+ if (can_move_general_to(next))
+ gen_action_space_or_piece(next)
+
+ if (game.count < movement_range())
+ for (let next of data.cities.roads[here])
+ if (can_move_general_to(next))
+ gen_action_space_or_piece(next)
+ },
+ take() {
+ game.state = "move_take"
+ },
+ give() {
+ game.state = "move_give"
+ },
+ detach(p) {
+ set_delete(game.selected, p)
+ },
+ piece(p) {
+ if (game.count === 0) {
+ if (set_has(game.selected, p))
+ set_delete(game.selected, p)
+ else
+ this.space(game.pos[p])
+ } else {
+ if (p === game.selected[0])
+ this.stop()
+ else
+ this.space(game.pos[p])
+ }
+ },
+ stop() {
+ for (let p of game.selected)
+ set_add(game.moved, p)
+ end_move_piece()
+ },
+ space(to) {
+ let who = game.selected[0]
+ let from = game.pos[who]
+
+ if (game.count === 0) {
+ log_selected()
+ log(">from S" + from)
+ }
+
+ log(">to S" + to)
+
+ if (!set_has(data.cities.major_roads[from], to))
+ game.major = 0
+
+ if (move_general_to(to) || ++game.count === movement_range() + game.major)
+ end_move_piece()
+ },
+}
+
+states.move_take = {
+ inactive: "move",
+ prompt() {
+ prompt("Transfer troops to " + format_selected() + ".")
+ view.selected = game.selected
+ let take = count_stacked_take()
+ let give = count_unstacked_give()
+ let n = Math.min(take, give)
+ view.actions.value = []
+ for (let i = 1; i <= n; ++i)
+ view.actions.value.push(i)
+ },
+ value(v) {
+ take_troops(v)
+ if (game.state === "laudon_take")
+ game.state = "austria_may_move_laudon_by_one_city_immediately"
+ else
+ game.state = "move_general"
+ },
+}
+
+states.move_give = {
+ inactive: "move",
+ prompt() {
+ prompt("Transfer troops from " + format_selected() + ".")
+ view.selected = game.selected
+ let take = count_unstacked_take()
+ let give = count_stacked_give()
+ let n = Math.min(take, give)
+ view.actions.value = []
+ for (let i = 1; i <= n; ++i)
+ view.actions.value.push(i)
+ },
+ value(v) {
+ give_troops(v)
+ if (game.state === "laudon_give")
+ game.state = "austria_may_move_laudon_by_one_city_immediately"
+ else
+ game.state = "move_general"
+ },
+}
+
+function end_move_piece() {
+ game.selected = null
+ game.state = "movement"
+}
+
+/* RECRUITMENT */
+
+function troop_cost() {
+ if (game.recruit.re_enter < ELIMINATED)
+ return 8
+ return 6
+}
+
+function sum_card_values(list) {
+ let n = 0
+ for (let c of list)
+ n += to_value(c)
+ return n
+}
+
+function find_largest_card(list) {
+ for (let v = 13; v >= 2; --v) {
+ for (let c of list)
+ if (to_value(c) === v)
+ return c
+ }
+ throw "NO CARDS FOUND IN LIST"
+}
+
+function spend_recruit_cost() {
+ let spend = troop_cost()
+ if (game.count > 0) {
+ if (spend < game.count) {
+ game.count -= spend
+ spend = 0
+ } else {
+ spend -= game.count
+ game.count = 0
+ }
+ }
+ while (spend > 0) {
+ let c = find_largest_card(game.recruit.pool)
+ let v = to_value(c)
+ set_delete(game.recruit.pool, c)
+ set_add(game.recruit.used, c)
+ if (v > spend) {
+ game.count = v - spend
+ spend = 0
+ } else {
+ spend -= v
+ }
+ }
+}
+
+function has_available_depot() {
+ for (let s of all_power_depots[game.power])
+ // TODO: also allied other player's pieces?
+ if (!has_enemy_piece(s))
+ return true
+ return false
+}
+
+function can_re_enter_general(to) {
+ if (has_friendly_supply_train(to))
+ return false
+ if (has_any_other_general(to))
+ return false
+ if (1 + count_generals(to) > 3)
+ return false
+ return true
+}
+
+function can_re_enter_supply_train(s) {
+ return !has_any_piece(s)
+}
+
+function goto_recruit() {
+ game.count = 0
+
+ if (!can_recruit_anything_in_theory()) {
+ end_recruit()
+ return
+ }
+
+ game.recruit = {
+ pool: [],
+ used: [],
+ pieces: [],
+ re_enter: ELIMINATED,
+ troops: 0,
+ }
+
+ // if all depots have enemy pieces, choose ONE city in given sector and COST is 8
+ if (has_available_depot())
+ game.state = "recruit"
+ else
+ game.state = "re_enter_choose_city"
+}
+
+states.re_enter_choose_city = {
+ inactive: "recruit",
+ prompt() {
+ prompt("Choose city to re-enter troops.")
+ for (let s of all_power_re_entry_cities[game.power])
+ if (!has_enemy_piece(s))
+ gen_action_space(s)
+ },
+ space(s) {
+ push_undo()
+ game.recruit.re_enter = s
+ game.state = "recruit"
+ },
+}
+
+function has_re_entry_space(p) {
+ let can_re_enter_at = is_general(p) ? can_re_enter_general : can_re_enter_supply_train
+ if (game.recruit.re_enter < ELIMINATED)
+ return can_re_enter_at(game.recruit.re_enter)
+ for (let s of all_power_depots[game.power])
+ if (can_re_enter_at(s))
+ return true
+ return false
+}
+
+function is_attack_position(s) {
+ for (let p of all_enemy_generals[game.power])
+ if (set_has(data.cities.adjacent[s], game.pos[p]))
+ return true
+ 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()
+ let elim_generals = count_eliminated_generals()
+ let unused_on_map = count_unused_troops_on_map()
+ // can reinforce on-map generals
+ if (unused_everywhere > 0 && unused_on_map > 0)
+ return true
+ // can re-enter eliminated generals
+ if (unused_everywhere > 0 && elim_generals > 0 && has_re_entry_space())
+ return true
+ // can re-enter eliminated supply trains
+ if (elim_trains > 0 && has_re_entry_space())
+ return true
+ return false
+}
+
+states.recruit = {
+ inactive: "recruit",
+ prompt() {
+ let cost = troop_cost()
+ let n_troops = count_used_troops()
+ let av_troops = max_power_troops(game.power) - n_troops
+ let av_trains = count_eliminated_trains()
+ let possible = can_recruit_anything()
+
+ let str
+ if (av_trains > 0 && av_troops > 0)
+ str = `Recruit supply trains and up to ${av_troops} troops for ${cost} each`
+ else if (av_troops > 0)
+ str = `Recruit up to ${av_troops} troops for ${cost} each`
+ else if (av_trains > 0)
+ str = `Recruit supply trains for ${cost} each`
+ else
+ str = "Nothing to recruit"
+
+ let paid = game.count + sum_card_values(game.recruit.pool)
-exports.view = function (state) {
+ if (paid > 1)
+ str += " \u2014 " + paid + " points."
+ else if (paid === 1)
+ str += " \u2014 1 point."
+ else
+ str += "."
+
+ prompt(str)
+
+ view.draw = game.recruit.pool
+
+ if (possible && paid / cost < av_troops + av_trains) {
+ for (let c of game.hand[game.power])
+ gen_action_card(c)
+ }
+
+ if (paid >= cost) {
+ if (av_troops > 0) {
+ for (let p of all_power_generals[game.power]) {
+ if (game.troops[p] > 0 && game.troops[p] < 8) {
+ let s = game.pos[p]
+ gen_action_supreme_commander(s)
+ }
+ else if (game.pos[p] === ELIMINATED && has_re_entry_space(p))
+ gen_action_piece(p)
+ }
+ }
+ if (av_trains > 0) {
+ for (let p of all_power_trains[game.power]) {
+ if (game.pos[p] === ELIMINATED && has_re_entry_space(p))
+ gen_action_piece(p)
+ }
+ }
+ }
+
+ if (paid < cost || !possible)
+ view.actions.end_recruit = 1
+ },
+ card(c) {
+ push_undo()
+ set_delete(game.hand[game.power], c)
+ set_add(game.recruit.pool, c)
+ },
+ piece(p) {
+ push_undo()
+
+ spend_recruit_cost()
+
+ if (game.pos[p] === ELIMINATED) {
+ game.selected = [ p ]
+ game.state = "re_enter"
+ } else {
+ game.recruit.troops += 1
+ add_one_troop(p)
+ }
+ },
+ end_recruit() {
+ push_undo()
+ end_recruit()
+ },
+}
+
+function end_recruit() {
+ if (game.recruit) {
+ if (game.recruit.used.length > 0) {
+ log_br()
+ log("Recruited")
+ log(">" + game.recruit.used.map(format_card).join(", "))
+ map_for_each(game.recruit.pieces, (p,s) => {
+ log(">P" + p + " at S" + s)
+ })
+ if (game.recruit.troops)
+ log(">" + game.recruit.troops + " troops")
+ }
+
+ // put back into hand unused cards
+ for (let c of game.recruit.pool)
+ set_add(game.hand[game.power], c)
+
+ delete game.recruit
+ }
+
+ goto_combat()
+}
+
+states.re_enter = {
+ inactive: "recruit",
+ prompt() {
+ prompt("Re-enter " + format_selected() + ".")
+ view.selected = game.selected
+
+ let p = game.selected[0]
+ let can_re_enter_at = is_general(p) ? can_re_enter_general : can_re_enter_supply_train
+
+ if (game.recruit.re_enter < ELIMINATED) {
+ if (can_re_enter_at(game.recruit.re_enter))
+ gen_action_space(game.recruit.re_enter)
+ } else {
+ for (let s of all_power_depots[game.power])
+ if (can_re_enter_at(s))
+ gen_action_space(s)
+ }
+ },
+ space(s) {
+ let p = game.selected[0]
+ game.pos[p] = s
+ map_set(game.recruit.pieces, p, s)
+ if (is_general(p)) {
+ game.recruit.troops += 1
+ game.troops[p] = 1
+ }
+ game.selected = null
+ game.state = "recruit"
+ },
+}
+
+/* COMBAT (CHOOSE TARGETS) */
+
+function goto_combat() {
+ let from = []
+ let to = []
+
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] < ELIMINATED)
+ set_add(from, game.pos[p])
+
+ for (let p of all_enemy_generals[game.power])
+ if (game.pos[p] < ELIMINATED)
+ set_add(to, game.pos[p])
+
+ game.combat = []
+ for (let a of from) {
+ for (let b of to) {
+ if (set_has(data.cities.adjacent[a], b)) {
+ game.combat.push(a)
+ game.combat.push(b)
+ }
+ }
+ }
+
+ if (game.combat.length > 0)
+ game.state = "combat"
+ else
+ goto_retroactive_conquest()
+}
+
+function next_combat() {
+ clear_undo()
+ set_active_to_current_action_step()
+ game.count = 0
+ delete game.attacker
+ delete game.defender
+ if (game.combat.length > 0)
+ game.state = "combat"
+ else
+ // TODO: a bit abrupt, but saves time if
+ // game.state = "combat_done"
+ goto_retroactive_conquest()
+}
+
+
+states.combat = {
+ inactive: "attack",
+ prompt() {
+ prompt("Resolve your attacks.")
+ for (let i = 0; i < game.combat.length; i += 2)
+ gen_action_supreme_commander(game.combat[i])
+ },
+ piece(p) {
+ push_undo()
+ game.attacker = game.pos[p]
+ game.state = "combat_target"
+ },
+}
+
+// TODO: unused for now
+states.combat_done = {
+ inactive: "attack",
+ prompt() {
+ prompt("Combat done.")
+ view.actions.end_combat = 1
+ },
+ end_combat() {
+ goto_retroactive_conquest()
+ },
+}
+
+states.combat_target = {
+ inactive: "attack",
+ prompt() {
+ prompt("Choose enemy stack to attack.")
+ for (let i = 0; i < game.combat.length; i += 2)
+ if (game.combat[i] === game.attacker)
+ gen_action_supreme_commander(game.combat[i+1])
+ },
+ piece(p) {
+ clear_undo()
+
+ game.defender = game.pos[p]
+
+ for (let i = 0; i < game.combat.length; i += 2) {
+ if (game.combat[i] === game.attacker && game.combat[i+1] === game.defender) {
+ array_remove_pair(game.combat, i)
+ break
+ }
+ }
+
+ goto_resolve_combat()
+ },
+}
+
+function goto_resolve_combat() {
+ let a_troops = 0
+ let d_troops = 0
+
+ for (let p of all_generals) {
+ if (game.pos[p] === game.attacker)
+ a_troops += game.troops[p]
+ if (game.pos[p] === game.defender)
+ d_troops += game.troops[p]
+ }
+
+ log_br()
+
+ game.count = a_troops - d_troops
+
+ let a = get_supreme_commander(game.attacker)
+ let d = get_supreme_commander(game.defender)
+ log("!")
+ log(`>P${a} at S${game.attacker}`)
+ log(`>P${d} at S${game.defender}`)
+ log(`>Troops ${a_troops} - ${d_troops} = ${game.count}`)
+
+ if (game.count <= 0) {
+ set_active_attacker()
+ game.state = "combat_attack"
+ } else {
+ set_active_defender()
+ game.state = "combat_defend"
+ }
+}
+
+function end_resolve_combat() {
+ if (game.count === 0) {
+ log(">Tied")
+ next_combat()
+ } else if (game.count > 0) {
+ game.selected = select_stack(game.defender)
+ goto_retreat()
+ } else {
+ game.selected = select_stack(game.attacker)
+ goto_retreat()
+ }
+}
+
+/* COMBAT (CARD PLAY) */
+
+function format_combat_stack(s) {
+ let p = get_supreme_commander(s)
+ return suit_name[get_space_suit(s)] + " " + piece_name[p]
+}
+
+function signed_number(v) {
+ if (v > 0)
+ return "+" + v
+ if (v < 0)
+ return "\u2212" + (-v)
+ return "0"
+}
+
+function format_combat(value) {
+ let a = format_combat_stack(game.attacker)
+ let d = format_combat_stack(game.defender)
+ let s = signed_number(value)
+ let p = POWER_NAME[game.power]
+ return `${a} vs ${d}. ${p} is at ${s}`
+}
+
+function inactive_attack() {
+ return "combat " + format_combat(game.count)
+}
+
+function inactive_defend() {
+ return "combat " + format_combat(-game.count)
+}
+
+function prompt_combat(value, extra = null) {
+ let text = "Combat " + format_combat(value) + "."
+ if (extra)
+ text += " " + extra
+ prompt(text)
+}
+
+function set_active_attacker() {
+ set_active_to_power(get_stack_power(game.attacker))
+}
+
+function set_active_defender() {
+ set_active_to_power(get_stack_power(game.defender))
+}
+
+function resume_combat_attack() {
+ if (game.count === 0)
+ game.state = "combat_attack_swap"
+ else if (game.count > 0)
+ game.state = "combat_attack_swap"
+ else
+ game.state = "combat_attack"
+}
+
+function resume_combat_defend() {
+ if (game.count === 0)
+ game.state = "combat_defend_swap"
+ else if (game.count < 0)
+ game.state = "combat_defend_swap"
+ else
+ game.state = "combat_defend"
+}
+
+function gen_play_card(suit) {
+ let score = Math.abs(game.count)
+ let has_suit = false
+
+ for (let c of game.hand[game.power]) {
+ let c_suit = to_suit(c)
+ if (c_suit === suit) {
+ has_suit = true
+ gen_action_card(c)
+ } else if (c_suit === RESERVE) {
+ gen_action_card(c)
+ }
+ }
+
+ // cannot pass if at 0 (and can play)
+ if (score === 0 && has_suit)
+ view.actions.pass = 0
+ else
+ view.actions.pass = 1
+}
+
+function gen_play_reserve() {
+ view.actions.value = [ 1, 2, 3, 4, 5, 6, 7, 8 ]
+}
+
+function play_card(c, sign) {
+ let prefix = (sign < 0 ? ">>" : ">") + POWER_NAME[game.power]
+ if (sign < 0)
+ game.count -= to_value(c)
+ else
+ game.count += to_value(c)
+ let score = signed_number(sign * game.count)
+ log(`${prefix} ${format_card(c)} = ${score}`)
+}
+
+function play_reserve(v, sign) {
+ let prefix = (sign < 0 ? ">>" : ">") + POWER_NAME[game.power]
+ if (sign < 0)
+ game.count -= v
+ else
+ game.count += v
+ let score = signed_number(sign * game.count)
+ log(`${prefix} ${v}R = ${score}`)
+}
+
+function play_combat_card(c, sign, resume, next_state) {
+ push_undo()
+ array_remove_item(game.hand[game.power], c)
+ if (is_reserve(c)) {
+ game.state = next_state
+ } else {
+ play_card(c, sign)
+ resume()
+ }
+}
+
+states.combat_attack = {
+ inactive: inactive_attack,
+ prompt() {
+ prompt_combat(game.count)
+ gen_play_card(get_space_suit(game.attacker))
+ },
+ card(c) {
+ play_combat_card(c, +1, resume_combat_attack, "combat_attack_reserve")
+ },
+ pass() {
+ clear_undo()
+ end_resolve_combat()
+ },
+}
+
+states.combat_defend = {
+ inactive: inactive_defend,
+ prompt() {
+ prompt_combat(-game.count)
+ gen_play_card(get_space_suit(game.defender))
+ },
+ card(c) {
+ play_combat_card(c, -1, resume_combat_defend, "combat_defend_reserve")
+ },
+ pass() {
+ clear_undo()
+ end_resolve_combat()
+ },
+}
+
+states.combat_attack_reserve = {
+ inactive: inactive_attack,
+ prompt() {
+ prompt_combat(game.count, "Choose value.")
+ gen_play_reserve()
+ },
+ value(v) {
+ play_reserve(v, +1)
+ resume_combat_attack()
+ },
+}
+
+states.combat_defend_reserve = {
+ inactive: inactive_defend,
+ prompt() {
+ prompt_combat(-game.count, "Choose value.")
+ gen_play_reserve()
+ },
+ value(v) {
+ play_reserve(v, -1)
+ resume_combat_defend()
+ },
+}
+
+states.combat_attack_swap = {
+ inactive: inactive_attack,
+ prompt() {
+ prompt_combat(game.count)
+ view.actions.next = 1
+ },
+ next() {
+ clear_undo()
+ set_active_defender()
+ game.state = "combat_defend"
+ },
+}
+
+states.combat_defend_swap = {
+ inactive: inactive_defend,
+ prompt() {
+ prompt_combat(-game.count)
+ view.actions.next = 1
+ },
+ next() {
+ clear_undo()
+ set_active_attacker()
+ game.state = "combat_attack"
+ },
+}
+
+/* RETREAT */
+
+function get_winner() {
+ return game.count > 0 ? game.attacker : game.defender
+}
+
+function get_loser() {
+ return game.count < 0 ? game.attacker : game.defender
+}
+
+function set_active_winner() {
+ if (game.count > 0)
+ set_active_attacker()
+ else
+ 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)
+ array_remove_pair(game.combat, i)
+}
+
+function goto_retreat() {
+ let lost = Math.abs(game.count)
+ 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)
+
+ // apply hits
+ for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) {
+ let p = game.selected[i]
+ while (game.troops[p] > 1 && hits > 0) {
+ --game.troops[p]
+ --hits
+ }
+ }
+
+ for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) {
+ let p = game.selected[i]
+ while (game.troops[p] > 0 && hits > 0) {
+ --game.troops[p]
+ --hits
+ }
+ }
+
+ log(POWER_NAME[loser_power] + " lost " + (lost-hits) + " troops.")
+
+ resume_retreat()
+}
+
+function resume_retreat() {
+ // eliminate generals with no more hits
+ for (let p of game.selected) {
+ if (game.troops[p] === 0) {
+ game.state = "retreat_eliminate_hits"
+ return
+ }
+ }
+
+ // retreat remaining generals
+ if (game.selected.length > 0) {
+ game.retreat = search_retreat(get_loser(), get_winner(), Math.abs(game.count))
+ if (game.retreat.length > 0) {
+ // victor chooses retreat destination
+ set_active_winner()
+ game.state = "retreat"
+ } else {
+ // eliminate if there are no retreat possibilities
+ delete game.retreat
+ game.state = "retreat_eliminate_trapped"
+ }
+ return
+ }
+
+ // no retreat if generals wiped out
+ next_combat()
+}
+
+states.retreat_eliminate_hits = {
+ inactive: "retreat loser",
+ prompt() {
+ prompt("Eliminate generals without troops.")
+ // remove eliminated generals
+ for (let p of game.selected)
+ if (game.troops[p] === 0)
+ gen_action_piece(p)
+ },
+ piece(p) {
+ eliminate_general(p)
+ set_delete(game.selected, p)
+ resume_retreat()
+ },
+}
+
+states.retreat_eliminate_trapped = {
+ inactive: "retreat loser",
+ prompt() {
+ prompt("Eliminate " + format_selected() + " without a retreat path.")
+ for (let p of game.selected)
+ gen_action_piece(p)
+ },
+ piece(_) {
+ log("Trapped")
+ for (let p of game.selected)
+ eliminate_general(p)
+ next_combat()
+ },
+}
+
+// search distances from winner within retreat range
+function search_retreat_distance(from, range) {
+ let seen = [ from, 0 ]
+ let queue = [ from << 4 ]
+ while (queue.length > 0) {
+ let item = queue.shift()
+ let here = item >> 4
+ let dist = (item & 15) + 1
+ for (let next of data.cities.adjacent[here]) {
+ if (map_has(seen, next))
+ continue
+ if (dist <= range) {
+ map_set(seen, next, dist)
+ queue.push((next << 4) | dist)
+ }
+ }
+ }
+ return seen
+}
+
+// search all possible retreat paths of given length
+function search_retreat_possible_dfs(result, seen, here, range) {
+ for (let next of data.cities.adjacent[here]) {
+ if (seen.includes(next))
+ continue
+ if (has_any_piece(next))
+ continue
+ if (range === 1) {
+ set_add(result, next)
+ } else {
+ seen.push(next)
+ search_retreat_possible_dfs(result, seen, next, range - 1)
+ seen.pop()
+ }
+ }
+}
+
+function search_retreat_possible(from, range) {
+ let result = []
+ search_retreat_possible_dfs(result, [from], from, range)
+ return result
+}
+
+function search_retreat(loser, winner, range) {
+ let distance = search_retreat_distance(winner, range + 1)
+ let possible = search_retreat_possible(loser, range)
+
+ let max = 0
+ for (let s of possible) {
+ let d = map_get(distance, s, -1)
+ if (d > max)
+ max = d
+ }
+
+ let result = []
+ for (let s of possible)
+ if (map_get(distance, s, -1) === max)
+ result.push(s)
+ return result
+}
+
+states.retreat = {
+ inactive: "retreat loser",
+ prompt() {
+ prompt("Retreat " + format_selected() + " " + Math.abs(game.count) + " cities.")
+ view.selected = game.selected
+ for (let s of game.retreat)
+ gen_action_space(s)
+ },
+ space(to) {
+ push_undo()
+ log("Retreated to S" + to + ".")
+ for (let p of game.selected)
+ game.pos[p] = to
+ delete game.retreat
+ game.state = "retreat_done"
+ },
+}
+
+states.retreat_done = {
+ inactive: "retreat loser",
+ prompt() {
+ prompt("Retreat done.")
+ view.actions.next = 1
+ },
+ next() {
+ next_combat()
+ },
+}
+
+/* RETRO-ACTIVE CONQUEST */
+
+function log_conquest(conq, reconq) {
+ if (conq.length > 0 || reconq.length > 0) {
+ log_br()
+ if (conq.length > 0) {
+ log("Conquered")
+ for (let s of conq)
+ log(">S" + s)
+ }
+ if (reconq.length > 0) {
+ log("Reconquered")
+ for (let s of reconq)
+ log(">S" + s)
+ }
+ }
+}
+
+function goto_retroactive_conquest() {
+ delete game.combat
+
+ let conq = []
+ let reconq = []
+
+ for (let s of game.retro) {
+ if (is_conquest_space(game.power, s)) {
+ if (!is_protected_from_conquest(s)) {
+ set_add(game.conquest, s)
+ conq.push(s)
+ }
+ }
+ if (is_reconquest_space(game.power, s)) {
+ if (!is_protected_from_reconquest(s)) {
+ set_delete(game.conquest, s)
+ reconq.push(s)
+ }
+ }
+ }
+
+ log_conquest(conq, reconq)
+
+ set_clear(game.retro)
+
+ end_action_stage()
+}
+
+/* SUPPLY */
+
+function search_supply_bfs(from, range) {
+ let seen = [ from ]
+ let queue = [ from << 4 ]
+ while (queue.length > 0) {
+ let item = queue.shift()
+ let here = item >> 4
+ let dist = (item & 15) + 1
+ for (let next of data.cities.adjacent[here]) {
+ if (set_has(seen, next))
+ continue
+ if (has_enemy_piece(next))
+ continue
+ set_add(seen, next)
+ if (dist < range)
+ queue.push((next << 4) | dist)
+ }
+ }
+ return seen
+}
+
+function search_supply(range) {
+ for (let p of all_power_trains[game.power]) {
+ let here = game.pos[p]
+ if (here >= ELIMINATED)
+ continue
+ if (!game.supply)
+ game.supply = search_supply_bfs(here, range)
+ else
+ set_add_all(game.supply, search_supply_bfs(here, range))
+ }
+}
+
+function is_out_of_supply(p) {
+ return (game.oos & (1 << p)) !== 0
+}
+
+function set_out_of_supply(p) {
+ return game.oos |= (1 << p)
+}
+
+function set_in_supply(p) {
+ return game.oos &= ~(1 << p)
+}
+
+function has_supply_line(p) {
+ let s = game.pos[p]
+ if (set_has(all_home_or_depot_cities[game.power], s))
+ return true
+ if (game.supply && set_has(game.supply, s))
+ return true
+ return false
+}
+
+function should_supply_restore() {
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (is_out_of_supply(p) && has_supply_line(p))
+ return true
+ }
+ return false
+}
+
+function should_supply_eliminate() {
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (is_out_of_supply(p) && !has_supply_line(p))
+ return true
+ }
+ return false
+}
+
+function should_supply_flip() {
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (!is_out_of_supply(p) && !has_supply_line(p))
+ return true
+ }
+ return false
+}
+
+function goto_supply() {
+ set_clear(game.moved)
+ search_supply(6)
+
+ if (should_supply_restore())
+ goto_supply_restore()
+ else if (should_supply_eliminate())
+ goto_supply_eliminate()
+ else if (should_supply_flip())
+ goto_supply_flip()
+ else
+ end_supply()
+}
+
+function goto_supply_restore() {
+ log_br()
+ log("In supply")
+ resume_supply_restore()
+}
+
+function goto_supply_eliminate() {
+ log_br()
+ log("Out of supply")
+ resume_supply_eliminate()
+}
+
+function goto_supply_flip() {
+ log_br()
+ log("Out of supply")
+ resume_supply_flip()
+}
+
+function resume_supply_restore() {
+ if (should_supply_restore())
+ game.state = "supply_restore"
+ else if (should_supply_eliminate())
+ goto_supply_eliminate()
+ else if (should_supply_flip())
+ goto_supply_flip()
+ else
+ game.state = "supply_done"
+}
+
+function resume_supply_eliminate() {
+ if (should_supply_eliminate())
+ game.state = "supply_eliminate"
+ else if (should_supply_flip())
+ goto_supply_flip()
+ else
+ game.state = "supply_done"
+}
+
+function resume_supply_flip() {
+ if (should_supply_flip())
+ game.state = "supply_flip"
+ else
+ game.state = "supply_done"
+}
+
+states.supply_restore = {
+ inactive: "supply",
+ prompt() {
+ prompt("Restore supply to generals with a supply line.")
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (is_out_of_supply(p) && has_supply_line(p))
+ gen_action_supreme_commander(game.pos[p])
+ }
+ },
+ piece(x) {
+ let s = game.pos[x]
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === s) {
+ set_add(game.moved, p)
+ set_in_supply(p)
+ log(">P" + p + " at S" + s)
+ }
+ }
+ resume_supply_restore()
+ },
+}
+
+states.supply_eliminate = {
+ inactive: "supply",
+ prompt() {
+ prompt("Eliminate out of supply generals with no supply line.")
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (is_out_of_supply(p) && !has_supply_line(p))
+ gen_action_supreme_commander(game.pos[p])
+ }
+ },
+ piece(x) {
+ let s = game.pos[x]
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === s)
+ eliminate_general(p)
+ resume_supply_eliminate()
+ },
+}
+
+states.supply_flip = {
+ inactive: "supply",
+ prompt() {
+ prompt("Flip generals with no supply line.")
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (!is_out_of_supply(p) && !has_supply_line(p))
+ gen_action_supreme_commander(game.pos[p])
+ }
+ },
+ piece(x) {
+ let s = game.pos[x]
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === s) {
+ log(">P" + p + " at S" + s)
+ set_out_of_supply(p)
+ }
+ }
+ resume_supply_flip()
+ },
+}
+
+states.supply_done = {
+ inactive: "supply",
+ prompt() {
+ prompt("Supply done.")
+ view.actions.end_supply = 1
+ },
+ end_supply() {
+ end_supply()
+ },
+}
+
+function end_supply() {
+ delete game.supply
+
+ end_action_stage()
+}
+
+/* SETUP */
+
+const POWER_FROM_SETUP_STEP = [
+ P_FRANCE,
+ P_BAVARIA,
+ P_PRUSSIA,
+ P_SAXONY,
+ P_PRUSSIA,
+ P_AUSTRIA,
+]
+
+function set_active_setup_power() {
+ game.power = POWER_FROM_SETUP_STEP[game.step]
+ game.active = current_player()
+}
+
+const setup_initial_tcs = [ 2, 5, 9, 3, 3, 5 ]
+
+const setup_total_troops = [ 26, 5, 16+6, 5, 14, 28 ]
+
+const setup_troops = [
+ 0, 0, 0, 0, 0,
+ 5,
+ 0, 0, 4, 6,
+ 5,
+ 0, 0, 0,
+ 0, 0, 6, 2, 0, 4,
+]
+
+const setup_piece_position = [
+ // - GENERALS -
+
+ // F
+ find_city("Beaune"),
+ find_city("Schwandorf"),
+ find_city("Ergoldsbach"),
+ find_city("Créspy-en-V."),
+ find_city("Sarreguemines"),
+
+ // B
+ find_city("Ergoldsbach"),
+
+ // P
+ find_city("Steinau"),
+ find_city("Steinau"),
+ find_city("Sprottau"),
+ find_city("East Prussia"),
+
+ // S
+ find_city("Radeberg"),
+
+ // PA
+ find_city("Delfzijl"),
+ find_city("Delfzijl"),
+ find_city("Dordrecht"),
+
+ // A
+ find_city("Austerlitz"),
+ find_city("Steinamanger"),
+ find_city("Stuhlweißenburg"),
+ find_city("Stuhlweißenburg"),
+ find_city("Trübau"),
+ find_city("Malmedy"),
+
+ // - TRAINS -
+
+ // F
+ find_city("Bar-le-Duc"),
+ find_city("Regensburg"),
+
+ // B
+ find_city("Falkenstein"),
+
+ // P
+ find_city("Grünberg"),
+ ELIMINATED, // TODO: find_city("Silesia V"),
+
+ // S
+ find_city("Meißen"),
+
+ // PA
+ find_city("Tilburg"),
+
+ // A
+ find_city("Hlinsko"),
+ find_city("Bruck"),
+ find_city("Geel"),
+]
+
+function make_political_deck() {
+ let deck41 = [ 1, 2, 3, 4, 5, 6 ]
+ let deck42 = [ 7, 8, 9, 10, 11, 12, 25 ]
+ let deck43 = [ 13, 14, 15, 16, 17, 18 ]
+ let deck44 = [ 19, 20, 21, 22, 23, 24 ]
+ shuffle_bigint(deck41)
+ shuffle_bigint(deck42)
+ shuffle_bigint(deck43)
+ shuffle_bigint(deck44)
+ return [ deck44, deck43, deck42, deck41 ].flat()
+}
+
+function make_tactics_deck(n) {
+ let deck = []
+ for (let suit = 0; suit <= 3; ++suit)
+ for (let value = 2; value <= 13; ++value)
+ deck.push((n << 7) | (suit << 4) | value)
+ deck.push((n << 7) | (RESERVE << 4) | 2)
+ deck.push((n << 7) | (RESERVE << 4) | 3)
+ return deck
+}
+
+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)
+ if (set_has(game.hand[pow], c))
+ return false
+ return true
+ })
+}
+
+exports.setup = function (seed, _scenario, _options) {
+ game = {
+ seed: seed,
+ undo: [],
+ log: [],
+
+ state: "setup",
+ active: R_LOUIS_XV,
+ power: P_FRANCE,
+
+ turn: 0,
+ step: 0,
+ pol_deck: null,
+ deck: null,
+ hand: [],
+
+ pos: setup_piece_position.slice(),
+ oos: 0,
+ troops: setup_troops.slice(),
+ conquest: [],
+
+ moved: [],
+ retro: [],
+
+ selected: [],
+ count: 0,
+ }
+
+ game.pol_deck = make_political_deck()
+ game.deck = make_tactics_deck(0)
+
+ shuffle_bigint(game.deck)
+
+ // Deal initial cards
+ for (let pow of all_powers)
+ for (let i = 0; i < setup_initial_tcs[pow]; ++i)
+ set_add(game.hand[pow], game.deck.pop())
+
+ log("# 1741")
+
+ return game
+}
+
+states.setup = {
+ inactive: "setup troops",
+ prompt() {
+ let n_troops = setup_total_troops[game.power] - count_used_troops()
+ if (n_troops === 0) {
+ prompt("Setup done.")
+ view.actions.end_setup = 1
+ } else {
+ let n_stacks = 0
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] < ELIMINATED && !set_has(game.moved, p)) {
+ gen_action_piece(p)
+ n_stacks ++
+ }
+ }
+ if (n_stacks > 1)
+ prompt("Add " + n_troops + " troops to " + n_stacks + " generals.")
+ else if (n_troops > 1)
+ prompt("Add " + n_troops + " troops to last general.")
+ else
+ prompt("Add 1 troop to last general.")
+ }
+ },
+ piece(p) {
+ push_undo()
+ set_add(game.moved, p)
+ game.selected = [ p ]
+ game.state = "setup_general"
+ },
+ end_setup() {
+ clear_undo()
+ end_setup()
+ },
+}
+
+states.setup_general = {
+ inactive: "setup troops",
+ prompt() {
+ prompt("Add troops to " + format_selected() + ".")
+ view.selected = game.selected
+
+ let n_selected = game.selected.length
+ let n_other = count_unused_generals() - game.selected.length
+ let n_troops = setup_total_troops[game.power] - count_used_troops()
+
+ // leave at least 1 for each remaining general
+ let take_max = Math.min(8 * n_selected, n_troops - n_other)
+
+ // leave no more than 8 for each remaining general
+ let take_min = Math.max(1 * n_selected, n_troops - n_other * 8)
+
+ view.actions.value = []
+ for (let i = take_min; i <= take_max; ++i)
+ view.actions.value.push(i)
+ },
+ value(v) {
+ let save = game.selected.length - 1
+ for (let p of game.selected) {
+ let n = Math.min(8, v - save)
+ game.troops[p] = n
+ v -= n
+ --save
+ }
+ game.selected = null
+ game.state = "setup"
+ },
+}
+
+function end_setup() {
+ if (++game.step === 7) {
+ goto_start_turn()
+ } else {
+ set_active_setup_power()
+ if (count_unused_generals() === 0)
+ end_setup()
+ }
+}
+
+/* VIEW */
+
+function mask_troops(player) {
+ let view_troops = []
+ for (let pow of all_powers) {
+ if (player_from_power(pow) === player) {
+ for (let p of all_power_generals[pow])
+ view_troops.push(game.troops[p])
+ } else {
+ for (let p of all_power_generals[pow]) {
+ let s = game.pos[p]
+ if (game.attacker === s || game.defender === s)
+ view_troops.push(game.troops[p])
+ else
+ view_troops.push(0)
+ }
+ }
+ }
+ return view_troops
+}
+
+function mask_hand(player) {
+ let view_hand = []
+ for (let pow of all_powers) {
+ if (player_from_power(pow) === player)
+ view_hand[pow] = game.hand[pow]
+ else
+ view_hand[pow] = game.hand[pow].map(c => c & ~127)
+ }
+ return view_hand
+}
+
+function total_troops_list() {
+ let list = []
+ for (let pow of all_powers) {
+ let n = 0
+ for (let p of all_power_generals[pow])
+ n += game.troops[p]
+ list[pow] = n
+ }
+ return list
+}
+
+exports.view = function (state, player) {
game = state
view = {
+ prompt: null,
+ actions: null,
log: game.log,
+
+ fate: game.turn <= 5 ? game.turn : game.fate,
+ pos: game.pos,
+ oos: game.oos,
+ conquest: game.conquest,
+ troops: mask_troops(player),
+ hand: mask_hand(player),
+ oo: game.oo,
+ pt: total_troops_list(),
+
power: game.power,
- turn: game.turn,
- ctl: game.ctl,
- loc: game.loc,
- str: game.str, // TODO: redact!
+ retro: game.retro,
+ }
+
+ if (game.attacker !== undefined && game.defender !== undefined) {
+ view.attacker = game.attacker
+ view.defender = game.defender
}
+
+ if (game.state === "game_over") {
+ view.prompt = game.victory
+ } else if (game.active !== player) {
+ let inactive = states[game.state].inactive || game.state
+ if (typeof inactive === "function")
+ inactive = inactive()
+ view.prompt = `Waiting for ${POWER_NAME[game.power]} to ${inactive}.`
+ } else {
+ view.actions = {}
+ if (states[game.state])
+ states[game.state].prompt()
+ else
+ view.prompt = "Unknown state: " + game.state
+ if (view.actions.undo === undefined) {
+ if (game.undo && game.undo.length > 0)
+ view.actions.undo = 1
+ else
+ view.actions.undo = 0
+ }
+ }
+
return view
}
+/* COMMON FRAMEWORK */
+function goto_game_over(result, victory) {
+ game.active = "None"
+ game.state = "game_over"
+ game.result = result
+ game.victory = victory
+ log("# Game Over")
+ log(game.victory)
+ return true
+}
-// === COMMON LIBRARY ===
+function prompt(str) {
+ view.prompt = POWER_NAME[game.power] + ": " + str
+}
-function clear_undo() {
- if (game.undo) {
- game.undo.length = 0
+exports.action = function (state, _player, action, arg) {
+ game = state
+ let S = states[game.state]
+ if (S && action in S) {
+ S[action](arg)
+ } else {
+ if (action === "undo" && game.undo && game.undo.length > 0)
+ pop_undo()
+ else
+ throw new Error("Invalid action: " + action)
}
+ return game
+}
+
+function gen_action(action, argument) {
+ if (view.actions[action] === undefined)
+ view.actions[action] = [ argument ]
+ else
+ set_add(view.actions[action], argument)
+}
+
+function gen_action_piece(p) {
+ gen_action("piece", p)
+}
+
+function gen_action_space(s) {
+ gen_action("space", s)
+}
+
+function gen_action_supreme_commander(s) {
+ let p = get_supreme_commander(s)
+ if (p >= 0)
+ gen_action_piece(p)
+}
+
+function gen_action_space_or_piece(s) {
+ let p = get_top_piece(s)
+ if (p >= 0)
+ gen_action_piece(p)
+ else
+ gen_action_space(s)
+}
+
+function gen_action_card(c) {
+ gen_action("card", c)
+}
+
+function gen_action_detach(p) {
+ gen_action("detach", p)
+}
+
+function log(msg) {
+ game.log.push(msg)
+}
+
+function log_br() {
+ if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
+ game.log.push("")
+}
+
+/* COMMON LIBRARY */
+
+function clear_undo() {
+ game.undo.length = 0
}
function push_undo() {
@@ -209,13 +2879,6 @@ function pop_undo() {
}
}
-function random(range) {
- // An MLCG using integer arithmetic with doubles.
- // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
- // m = 2**35 − 31
- return (game.seed = game.seed * 200105 % 34359738337) % range
-}
-
function random_bigint(range) {
// Largest MLCG that will fit its state in a double.
// Uses BigInt for arithmetic, so is an order of magnitude slower.
@@ -224,16 +2887,6 @@ function random_bigint(range) {
return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range
}
-function shuffle(list) {
- // Fisher-Yates shuffle
- for (let i = list.length - 1; i > 0; --i) {
- let j = random(i + 1)
- let tmp = list[j]
- list[j] = list[i]
- list[i] = tmp
- }
-}
-
function shuffle_bigint(list) {
// Fisher-Yates shuffle
for (let i = list.length - 1; i > 0; --i) {
@@ -363,37 +3016,30 @@ function set_delete(set, item) {
}
}
-function set_toggle(set, item) {
- let a = 0
- let b = set.length - 1
- while (a <= b) {
- let m = (a + b) >> 1
- let x = set[m]
- if (item < x)
- b = m - 1
- else if (item > x)
- a = m + 1
- else {
- array_remove(set, m)
- return
- }
- }
- array_insert(set, a, item)
+function set_add_all(set, other) {
+ for (let item of other)
+ set_add(set, item)
}
-function set_union(a, b) {
- let out = a.slice()
- for (let item of b)
- set_add(out, item)
- return out
+function set_union(one, two) {
+ let set = []
+ for (let item of one)
+ set_add(set, item)
+ for (let item of two)
+ set_add(set, item)
+ return set
}
-// Map as plain sorted array of key/value pairs
-
-function map_clear(map) {
- map.length = 0
+function set_intersect(one, two) {
+ let set = []
+ for (let item of one)
+ if (set_has(two, item))
+ set_add(set, item)
+ return set
}
+// Map as plain sorted array of key/value pairs
+
function map_has(map, key) {
let a = 0
let b = (map.length >> 1) - 1
@@ -444,68 +3090,7 @@ function map_set(map, key, value) {
array_insert_pair(map, a<<1, key, value)
}
-function map_delete(map, key) {
- let a = 0
- let b = (map.length >> 1) - 1
- while (a <= b) {
- let m = (a + b) >> 1
- let x = map[m<<1]
- if (key < x)
- b = m - 1
- else if (key > x)
- a = m + 1
- else {
- array_remove_pair(map, m<<1)
- return
- }
- }
-}
-
-function object_diff(a, b) {
- if (a === b)
- return false
- if (a !== null && b !== null && typeof a === "object" && typeof b === "object") {
- if (Array.isArray(a)) {
- if (!Array.isArray(b))
- return true
- let a_length = a.length
- if (b.length !== a_length)
- return true
- for (let i = 0; i < a_length; ++i)
- if (object_diff(a[i], b[i]))
- return true
- return false
- }
- for (let key in a)
- if (object_diff(a[key], b[key]))
- return true
- for (let key in b)
- if (!(key in a))
- return true
- return false
- }
- return true
-}
-
-// same as Object.groupBy
-function object_group_by(items, callback) {
- let groups = {}
- if (typeof callback === "function") {
- for (let item of items) {
- let key = callback(item)
- if (key in groups)
- groups[key].push(item)
- else
- groups[key] = [ item ]
- }
- } else {
- for (let item of items) {
- let key = item[callback]
- if (key in groups)
- groups[key].push(item)
- else
- groups[key] = [ item ]
- }
- }
- return groups
+function map_for_each(map, f) {
+ for (let i = 0; i < map.length; i += 2)
+ f(map[i], map[i+1])
}