summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js1140
1 files changed, 1140 insertions, 0 deletions
diff --git a/rules.js b/rules.js
new file mode 100644
index 0000000..5f280d2
--- /dev/null
+++ b/rules.js
@@ -0,0 +1,1140 @@
+"use strict"
+
+const TEUTONS = "Teutons"
+const RUSSIANS = "Russians"
+
+const P1 = TEUTONS
+const P2 = RUSSIANS
+
+let game = null
+let view = null
+let states = {}
+
+exports.roles = [ P1, P2 ]
+
+exports.scenarios = [
+ "Pleskau - 1240",
+ "Watland - 1241",
+ "Peipus - 1242",
+ "Return of the Prince - 1241 to 1242",
+ "Return of the Prince - Nicolle Variant",
+ "Crusade on Novgorod - 1240 to 1242",
+ "Pleskau - 1240 (Quickstart)",
+]
+
+exports.scenarios = [
+ "Pleskau",
+ "Watland",
+ "Peipus",
+ "Return of the Prince",
+ "Return of the Prince (Nicolle Variant)",
+ "Crusade on Novgorod",
+ "Pleskau (Quickstart)",
+]
+
+// unit types
+const KNIGHTS = 0
+const SERGEANTS = 1
+const LIGHT_HORSE = 2
+const ASIATIC_HORSE = 3
+const MEN_AT_ARMS = 4
+const MILITIA = 5
+const SERFS = 6
+
+// asset types
+const PROV = 0
+const COIN = 1
+const LOOT = 2
+const CART = 3
+const SLED = 4
+const BOAT = 5
+const SHIP = 6
+
+const data = require("./data.js")
+
+function find_lord(name) { return data.lords.findIndex(x => x.name === name) }
+function find_locale(name) { return data.locales.findIndex(x => x?.name === name) }
+
+const lord_name = data.lords.map(lord => lord.name)
+
+const lord_count = data.lords.length
+const vassal_count = data.vassals.length
+const last_vassal = vassal_count - 1
+const last_lord = lord_count - 1
+
+const LORD_ANDREAS = find_lord("Andreas")
+const LORD_HEINRICH = find_lord("Heinrich")
+const LORD_HERMANN = find_lord("Hermann")
+const LORD_KNUD_ABEL = find_lord("Knud & Abel")
+const LORD_RUDOLF = find_lord("Rudolf")
+const LORD_YAROSLAV = find_lord("Yaroslav")
+
+const LORD_ALEKSANDR = find_lord("Aleksandr")
+const LORD_ANDREY = find_lord("Andrey")
+const LORD_DOMASH = find_lord("Domash")
+const LORD_GAVRILO = find_lord("Gavrilo")
+const LORD_KARELIANS = find_lord("Karelians")
+const LORD_VLADISLAV = find_lord("Vladislav")
+
+const LOC_REVAL = find_locale("Reval")
+const LOC_WESENBERG = find_locale("Wesenberg")
+const LOC_DORPAT = find_locale("Dorpat")
+const LOC_LEAL = find_locale("Leal")
+const LOC_RIGA = find_locale("Riga")
+const LOC_ADSEL = find_locale("Adsel")
+const LOC_FELLIN = find_locale("Fellin")
+const LOC_ODENPAH = find_locale("OdenpƤh")
+const LOC_WENDEN = find_locale("Wenden")
+const LOC_NOVGOROD = find_locale("Novgorod")
+const LOC_LADOGA = find_locale("Ladoga")
+const LOC_PSKOV = find_locale("Pskov")
+const LOC_RUSA = find_locale("Rusa")
+const LOC_LOVAT = find_locale("Lovat")
+const LOC_LUGA = find_locale("Luga")
+const LOC_NEVA = find_locale("Neva")
+const LOC_VOLKHOV = find_locale("Volkhov")
+const LOC_IZBORSK = find_locale("Izborsk")
+const LOC_KAIBOLOVO = find_locale("Kaibolovo")
+const LOC_KOPORYE = find_locale("Koporye")
+const LOC_PORKHOV = find_locale("Porkhov")
+const LOC_VELIKIYE_LUKI = find_locale("Velikiye Luki")
+
+const LOC_DUBROVNO = find_locale("Dubrovno")
+
+const NOBODY = -1
+const NOWHERE = -1
+const NOTHING = -1
+const NEVER = -1
+const CALENDAR = 100
+
+const SUMMER = 0
+const EARLY_WINTER = 1
+const LATE_WINTER = 2
+const RASPUTITSA = 3
+
+const SEASONS = [
+ SUMMER, SUMMER, EARLY_WINTER, EARLY_WINTER, LATE_WINTER, LATE_WINTER, RASPUTITSA, RASPUTITSA,
+ SUMMER, SUMMER, EARLY_WINTER, EARLY_WINTER, LATE_WINTER, LATE_WINTER, RASPUTITSA, RASPUTITSA,
+]
+
+const TURN_NAME = [
+ "1 - Summer 1240",
+ "2 - Summer 1240",
+ "3 - Early Winter 1240",
+ "4 - Early Winter 1240",
+ "5 - Late Winter 1241",
+ "6 - Late Winter 1241",
+ "7 - Rasputitsa 1241",
+ "8 - Rasputitsa 1241",
+ "9 - Summer 1241",
+ "10 - Summer 1241",
+ "11 - Early Winter 1241",
+ "12 - Early Winter 1241",
+ "13 - Late Winter 1242",
+ "14 - Late Winter 1242",
+ "15 - Rasputitsa 1242",
+ "16 - Rasputitsa 1242",
+]
+
+const USABLE_TRANSPORT = [
+ [ CART, BOAT, SHIP ],
+ [ SLED ],
+ [ SLED ],
+ [ BOAT, SHIP ],
+]
+
+function current_season() {
+ return SEASONS[game.turn >> 1]
+}
+
+function current_turn_name() {
+ return TURN_NAME[game.turn >> 1]
+}
+
+// === GAME STATE ===
+
+let first_friendly_lord = 0
+let last_friendly_lord = 5
+let first_enemy_lord = 6
+let last_enemy_lord = 11
+
+function update_aliases() {
+ if (game.active === P1) {
+ first_friendly_lord = 0
+ last_friendly_lord = 5
+ first_enemy_lord = 6
+ last_enemy_lord = 11
+ } else {
+ first_friendly_lord = 6
+ last_friendly_lord = 11
+ first_enemy_lord = 0
+ last_enemy_lord = 5
+ }
+}
+
+function load_state(state) {
+ if (game !== state) {
+ game = state
+ update_aliases()
+ }
+}
+
+function push_state(next) {
+ if (!states[next])
+ throw Error("No such state: " + next)
+ game.stack.push([game.state, game.who, game.count])
+ game.state = next
+}
+
+function pop_state() {
+ ;[ game.state, game.who, game.count ] = game.stack.pop()
+}
+
+function set_active(new_active) {
+ if (game.active !== new_active) {
+ game.active = new_active
+ update_active_aliases()
+ }
+}
+
+function set_active_enemy() {
+ game.active = enemy_player()
+ update_aliases()
+}
+
+function enemy_player() {
+ if (game.active === P1) return P2
+ if (game.active === P2) return P1
+ return null
+}
+
+function get_lord_locale(lord) {
+ return game.lords.locale[lord]
+}
+
+function get_lord_service(lord) {
+ return game.lords.service[lord]
+}
+
+function get_lord_capability(lord, n) {
+ return game.lords.cards[(lord << 1) + n]
+}
+
+function set_lord_capability(lord, n, x) {
+ game.lords.cards[(lord << 1) + n] = x
+}
+
+function get_lord_assets(lord, n) {
+ return pack4_get(game.lords.assets[lord], n)
+}
+
+function get_lord_forces(lord, n) {
+ return pack4_get(game.lords.forces[lord], n)
+}
+
+function get_lord_routed_forces(lord, n) {
+ return pack4_get(game.lords.routed_forces[lord], n)
+}
+
+function get_lord_moved(lord) {
+ return pack1_get(game.lords.moved, lord)
+}
+
+function set_lord_locale(lord, locale) {
+ game.lords.locale[lord] = locale
+}
+
+function set_lord_service(lord, service) {
+ game.lords.service[lord] = service
+}
+
+function set_lord_assets(lord, n, x) {
+ if (x < 0) x = 0
+ if (x > 8) x = 8
+ game.lords.assets[lord] = pack4_set(game.lords.assets[lord], n, x)
+}
+
+function add_lord_assets(lord, n, x) {
+ set_lord_assets(lord, n, get_lord_assets(lord, n) + x)
+}
+
+function set_lord_forces(lord, n, x) {
+ if (x < 0) x = 0
+ if (x > 15) x = 15
+ game.lords.forces[lord] = pack4_set(game.lords.forces[lord], n, x)
+}
+
+function add_lord_forces(lord, n, x) {
+ set_lord_forces(lord, n, get_lord_forces(lord, n) + x)
+}
+
+function set_lord_routed_forces(lord, n, x) {
+ if (x < 0) x = 0
+ if (x > 15) x = 15
+ game.lords.routed_forces[lord] = pack4_set(game.lords.routed_forces[lord], n, x)
+}
+
+function add_lord_routed_forces(lord, n, x) {
+ set_lord_routed_forces(lord, n, get_lord_routed_forces(lord, n) + x)
+}
+
+function set_lord_moved(lord, x) {
+ game.lords.moved = pack1_set(game.lords.moved, lord, x)
+}
+
+function get_lord_vassal_count(lord) {
+ return data.lords[lord].vassals.length
+}
+
+function get_lord_vassal_service(lord, n) {
+ let v = data.lords[lord].vassals[n]
+ return game.vassals[v]
+}
+
+function set_lord_vassal_service(lord, n, x) {
+ let v = data.lords[lord].vassals[n]
+ game.vassals[v] = x
+}
+
+// === GAME STATE HELPERS ===
+
+function is_card_in_use(c) {
+ if (set_has(game.global_cards, c))
+ return true
+ if (game.lords.cards.includes(c))
+ return true
+ if (c === 18 || c === 19 || c === 20)
+ return true
+ if (c === 39 || c === 40 || c === 41)
+ return true
+ return false
+}
+
+function is_lord_on_map(lord) {
+ let loc = get_lord_locale(lord)
+ return loc !== NOWHERE && loc < CALENDAR
+}
+
+function is_lord_on_calendar(lord) {
+ let loc = get_lord_locale(lord)
+ return loc >= CALENDAR
+}
+
+function is_lord_ready(lord) {
+ let loc = get_lord_locale(lord)
+ return (loc >= CALENDAR && loc <= CALENDAR + (game.turn >> 1))
+}
+
+function is_vassal_ready(vassal) {
+ return game.vassals[vassal] === 0
+}
+
+function is_lord_at_friendly_locale(lord) {
+ let loc = get_lord_locale(lord)
+ return is_friendly_locale(loc)
+}
+
+function is_friendly_locale(loc) {
+ // TODO
+ return loc !== NOWHERE && loc < CALENDAR
+}
+
+function for_each_friendly_arts_of_war(fn) {
+ if (game.active === P1)
+ for (let i = 0; i < 18; ++i)
+ fn(i)
+ else
+ for (let i = 21; i < 39; ++i)
+ fn(i)
+}
+
+function can_add_transport(who, what) {
+ // TODO: limit to transports usable in current season?
+ return get_lord_assets(who, what) < 8
+}
+
+function roll_die(reason) {
+ let die = random(6) + 1
+ log(`Rolled ${die}${reason}.`)
+ return die
+}
+
+// === SETUP ===
+
+function setup_lord_on_calendar(lord, turn) {
+ set_lord_locale(lord, CALENDAR + turn)
+}
+
+function muster_lord(lord, locale, service) {
+ let info = data.lords[lord]
+
+ if (!service)
+ service = (game.turn >> 1) + info.service
+
+ set_lord_locale(lord, locale)
+ set_lord_service(lord, service)
+
+ set_lord_assets(lord, PROV, info.assets.prov | 0)
+ set_lord_assets(lord, COIN, info.assets.coin | 0)
+ set_lord_assets(lord, LOOT, info.assets.loot | 0)
+
+ set_lord_assets(lord, CART, info.assets.cart | 0)
+ set_lord_assets(lord, SLED, info.assets.sled | 0)
+ set_lord_assets(lord, BOAT, info.assets.boat | 0)
+ set_lord_assets(lord, SHIP, info.assets.ship | 0)
+
+ set_lord_forces(lord, KNIGHTS, info.forces.knights | 0)
+ set_lord_forces(lord, SERGEANTS, info.forces.serfs | 0)
+ set_lord_forces(lord, LIGHT_HORSE, info.forces.light_horse | 0)
+ set_lord_forces(lord, ASIATIC_HORSE, info.forces.asiatic_horse | 0)
+ set_lord_forces(lord, MEN_AT_ARMS, info.forces.men_at_arms | 0)
+ set_lord_forces(lord, MILITIA, info.forces.militia | 0)
+ set_lord_forces(lord, SERFS, info.forces.serfs | 0)
+
+ for (let v of info.vassals)
+ game.vassals[v] = 0
+}
+
+function muster_vassal(lord, vassal) {
+ let info = data.vassals[vassal]
+
+ game.vassals[vassal] = 1
+
+ add_lord_forces(lord, KNIGHTS, info.forces.knights | 0)
+ add_lord_forces(lord, SERGEANTS, info.forces.serfs | 0)
+ add_lord_forces(lord, LIGHT_HORSE, info.forces.light_horse | 0)
+ add_lord_forces(lord, ASIATIC_HORSE, info.forces.asiatic_horse | 0)
+ add_lord_forces(lord, MEN_AT_ARMS, info.forces.men_at_arms | 0)
+ add_lord_forces(lord, MILITIA, info.forces.militia | 0)
+ add_lord_forces(lord, SERFS, info.forces.serfs | 0)
+}
+
+exports.setup = function (seed, scenario, options) {
+ game = {
+ seed,
+ scenario,
+ options,
+ log: [],
+ undo: [],
+
+ active: P1,
+ state: 'setup_lords',
+ stack: [],
+
+ turn: 0,
+ p1_hand: [],
+ p2_hand: [],
+ p1_plan: [],
+ p2_plan: [],
+ lords: {
+ locale: Array(lord_count).fill(NOWHERE),
+ service: Array(lord_count).fill(NEVER),
+ assets: Array(lord_count).fill(0),
+ forces: Array(lord_count).fill(0),
+ routed_forces: Array(lord_count).fill(0),
+ cards: Array(lord_count << 1).fill(NOTHING),
+ lieutenants: [],
+ moved: 0,
+ },
+ vassals: Array(vassal_count).fill(0),
+ legate: NOWHERE,
+ veche_vp: 0,
+ veche_coin: 0,
+ conquered: [],
+ ravaged: [],
+ global_cards: [],
+
+ who: NOBODY,
+ where: NOWHERE,
+ what: NOTHING,
+ levy: 0, // lordship used
+ count: 0,
+ }
+
+ log_h1(scenario)
+
+ switch (scenario) {
+ default:
+ case "Pleskau":
+ setup_pleskau()
+ break
+ case "Watland":
+ setup_watland()
+ break
+ case "Peipus":
+ setup_peipus()
+ break
+ case "Return of the Prince":
+ setup_return_of_the_prince()
+ break
+ case "Return of the Prince (Nicolle Variant)":
+ setup_return_of_the_prince_nicolle()
+ break
+ case "Crusade on Novgorod":
+ setup_crusade_on_novgorod()
+ break
+ case "Pleskau (Quickstart)":
+ setup_quickstart()
+ break
+ }
+
+ return game
+}
+
+function setup_pleskau() {
+ game.turn = 1 << 1
+ game.veche_vp = 1
+ muster_lord(LORD_HERMANN, LOC_DORPAT, 4)
+ muster_lord(LORD_KNUD_ABEL, LOC_REVAL, 3)
+ muster_lord(LORD_YAROSLAV, LOC_ODENPAH, 2)
+ muster_lord(LORD_GAVRILO, LOC_PSKOV, 4)
+ muster_lord(LORD_VLADISLAV, LOC_NEVA, 3)
+ setup_lord_on_calendar(LORD_RUDOLF, 1)
+ setup_lord_on_calendar(LORD_DOMASH, 1)
+}
+
+function setup_quickstart() {
+ setup_pleskau()
+ // TODO: automated muster
+}
+
+function setup_watland() {
+ game.turn = 4 << 1
+ game.veche_vp = 1
+ game.veche_coin = 1
+
+ set_add(game.conquered, LOC_IZBORSK)
+ set_add(game.conquered, LOC_PSKOV)
+ set_add(game.ravaged, LOC_PSKOV)
+ set_add(game.ravaged, LOC_DUBROVNO)
+
+ muster_lord(LORD_ANDREAS, LOC_FELLIN, 7)
+ muster_lord(LORD_KNUD_ABEL, LOC_WESENBERG, 6)
+ muster_lord(LORD_YAROSLAV, LOC_PSKOV, 5)
+ muster_lord(LORD_DOMASH, LOC_NOVGOROD, 7)
+
+ setup_lord_on_calendar(LORD_HEINRICH, 4)
+ setup_lord_on_calendar(LORD_RUDOLF, 4)
+ setup_lord_on_calendar(LORD_VLADISLAV, 4)
+ setup_lord_on_calendar(LORD_KARELIANS, 4)
+ setup_lord_on_calendar(LORD_ANDREY, 5)
+ setup_lord_on_calendar(LORD_ALEKSANDR, 7)
+ setup_lord_on_calendar(LORD_HERMANN, 8)
+}
+
+states.setup_lords = {
+ prompt() {
+ view.prompt = "Setup your Lords."
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_on_map(lord) && !get_lord_moved(lord)) {
+ if (data.lords[lord].assets.transport > 0) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ }
+ if (done) {
+ view.prompt += " All done."
+ view.actions.end_setup = 1
+ }
+ },
+ lord(lord) {
+ push_undo()
+ push_state('muster_lord_transport')
+ set_lord_moved(lord, 1)
+ game.who = lord
+ game.count = data.lords[lord].assets.transport
+ },
+ end_setup() {
+ clear_undo()
+ end_setup()
+ },
+}
+
+function end_setup_lords() {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ set_lord_moved(lord, 0)
+ if (game.active === P1) {
+ set_active_enemy()
+ } else {
+ set_active_enemy()
+ goto_levy_arts_of_war()
+ }
+}
+
+// === LEVY: ARTS OF WAR ===
+
+function goto_levy_arts_of_war() {
+ log_h1("Levy " + current_turn_name())
+ game.state = 'levy_arts_of_war'
+ end_levy_arts_of_war()
+}
+
+function end_levy_arts_of_war() {
+ goto_levy_pay()
+}
+
+states.levy_arts_of_war = {
+}
+
+// === LEVY: PAY ===
+
+function goto_levy_pay() {
+ game.state = 'levy_pay'
+ end_levy_pay()
+}
+
+function end_levy_pay() {
+ goto_levy_disband()
+}
+
+states.levy_pay = {
+}
+
+// === LEVY: DISBAND ===
+
+function goto_levy_disband() {
+ game.state = 'levy_disband'
+ end_levy_disband()
+}
+
+function end_levy_disband() {
+ goto_levy_muster()
+}
+
+states.levy_disband = {
+}
+
+// === LEVY: MUSTER ===
+
+function goto_levy_muster() {
+ game.state = 'levy_muster'
+}
+
+function end_levy_muster() {
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord)
+ set_lord_moved(lord, 0)
+ if (game.active === P1) {
+ set_active_enemy()
+ } else {
+ set_active_enemy()
+ goto_levy_call_to_arms()
+ }
+}
+
+states.levy_muster = {
+ prompt() {
+ view.prompt = "Muster your Lords."
+ let done = true
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (is_lord_at_friendly_locale(lord) && !get_lord_moved(lord)) {
+ gen_action_lord(lord)
+ done = false
+ }
+ }
+ if (done) {
+ view.prompt += " All done."
+ view.actions.end_muster = 1
+ }
+ },
+ lord(lord) {
+ push_undo()
+ push_state('levy_muster_lord')
+ game.who = lord
+ game.count = data.lords[lord].lordship
+ },
+ end_muster() {
+ clear_undo()
+ end_levy_muster()
+ },
+}
+
+states.levy_muster_lord = {
+ prompt() {
+ view.prompt = `Muster ${lord_name[game.who]}.`
+
+ if (game.count > 0) {
+ view.prompt += ` ${game.count} lordship left.`
+
+ // Roll to muster Ready Lord at Seat
+ for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) {
+ if (lord === ALEKSANDR)
+ continue
+ if (lord === ANDREY && game.who !== ALEKSANDR)
+ continue
+ if (is_lord_ready(lord))
+ // TODO: has available seat
+ gen_action_lord(lord)
+ }
+
+ // Muster Ready Vassal Forces
+ for (let vassal of data.lords[game.who].vassals) {
+ if (is_vassal_ready(vassal))
+ gen_action_vassal(vassal)
+ }
+
+ // Add Transport
+ if (data.lords[game.who].ships) {
+ if (can_add_transport(game.who, SHIP))
+ view.actions.ship = 1
+ }
+ if (can_add_transport(game.who, BOAT))
+ view.actions.boat = 1
+ if (can_add_transport(game.who, CART))
+ view.actions.cart = 1
+ if (can_add_transport(game.who, SLED))
+ view.actions.sled = 1
+
+ // Add Capability
+ view.actions.capability = 1
+ } else {
+ view.prompt += " All done."
+ }
+
+ view.actions.done = 1
+ },
+
+ lord(other) {
+ clear_undo()
+ --game.count
+ let die = roll_die(` to muster ${lord_name[other]}`)
+ // TODO: roll for lord
+ if (die <= data.lords[other].fealty) {
+ logi(`Success!`)
+ push_state('muster_lord_at_seat')
+ game.who = other
+ } else {
+ logi(`Failed.`)
+ }
+ },
+
+ vassal(vassal) {
+ push_undo()
+ --game.count
+ muster_vassal(game.who, vassal)
+ },
+
+ ship() {
+ push_undo()
+ --game.count
+ add_lord_assets(game.who, SHIP, 1)
+ },
+ boat() {
+ push_undo()
+ --game.count
+ add_lord_assets(game.who, BOAT, 1)
+ },
+ cart() {
+ push_undo()
+ --game.count
+ add_lord_assets(game.who, CART, 1)
+ },
+ sled() {
+ push_undo()
+ --game.count
+ add_lord_assets(game.who, SLED, 1)
+ },
+
+ capability() {
+ push_undo()
+ --game.count
+ push_state('muster_capability')
+ },
+
+ done() {
+ set_lord_moved(game.who, 1)
+ pop_state()
+ },
+}
+
+states.muster_lord_at_seat = {
+ prompt() {
+ view.prompt = `Select seat for ${lord_name[game.who]}.`
+ for (let loc of data.lords[game.who].seats)
+ if (is_friendly_locale(loc))
+ gen_action_locale(loc)
+ },
+ locale(loc) {
+ push_undo()
+ logi(`Mustered at %${loc}.`)
+ set_lord_moved(game.who, 1)
+ muster_lord(game.who, loc)
+ game.state = 'muster_lord_transport'
+ game.count = data.lords[game.who].assets.transport
+ },
+}
+
+states.muster_lord_transport = {
+ prompt() {
+ view.prompt = `Select Transport for ${lord_name[game.who]}.`
+ view.prompt += ` ${game.count} left.`
+ if (data.lords[game.who].ships) {
+ if (can_add_transport(game.who, SHIP))
+ view.actions.ship = 1
+ }
+ if (can_add_transport(game.who, BOAT))
+ view.actions.boat = 1
+ if (can_add_transport(game.who, CART))
+ view.actions.cart = 1
+ if (can_add_transport(game.who, SLED))
+ view.actions.sled = 1
+ view.actions.done = 0
+ },
+ ship() {
+ push_undo()
+ add_lord_assets(game.who, SHIP, 1)
+ if (--game.count === 0)
+ pop_state()
+ },
+ boat() {
+ push_undo()
+ add_lord_assets(game.who, BOAT, 1)
+ if (--game.count === 0)
+ pop_state()
+ },
+ cart() {
+ push_undo()
+ add_lord_assets(game.who, CART, 1)
+ if (--game.count === 0)
+ pop_state()
+ },
+ sled() {
+ push_undo()
+ add_lord_assets(game.who, SLED, 1)
+ if (--game.count === 0)
+ pop_state()
+ },
+}
+
+states.muster_capability = {
+ prompt() {
+ view.prompt = `Select a new capability for ${lord_name[game.who]}.`
+ for_each_friendly_arts_of_war(c => {
+ if (!is_card_in_use(c)) {
+ if (!data.cards[c].lords || set_has(data.cards[c].lords, game.who))
+ gen_action_arts_of_war(c)
+ }
+ })
+ },
+ arts_of_war(c) {
+ push_undo()
+ if (!data.cards[c].lords) {
+ set_add(game.global_cards, c)
+ } else {
+ if (get_lord_capability(game.who, 0) < 0)
+ set_lord_capability(game.who, 0, c)
+ else if (get_lord_capability(game.who, 1) < 0)
+ set_lord_capability(game.who, 1, c)
+ else {
+ game.what = c
+ game.state = 'muster_capability_discard'
+ return
+ }
+ }
+ pop_state()
+ },
+}
+
+// === LEVY: CALL TO ARMS ===
+
+function goto_levy_call_to_arms() {
+ game.state = 'levy_call_to_arms'
+ end_levy_call_to_arms()
+}
+
+function end_levy_call_to_arms() {
+ goto_campaign_plan()
+}
+
+states.levy_call_to_arms = {
+}
+
+// === CAMPAIGN ===
+
+function goto_campaign_plan() {
+ game.state = 'campaign_plan'
+}
+
+// === GAME OVER ===
+
+function goto_game_over(result, victory) {
+ game.state = 'game_over'
+ game.active = "None"
+ game.result = result
+ game.victory = victory
+ log_br()
+ log(game.victory)
+ return true
+}
+
+states.game_over = {
+ get inactive() {
+ return game.victory
+ },
+ prompt() {
+ view.prompt = game.victory
+ }
+}
+
+exports.resign = function (state, current) {
+ load_state(state)
+ if (game.state !== 'game_over') {
+ for (let opponent of exports.roles) {
+ if (opponent !== current) {
+ goto_game_over(opponent, current + " resigned.")
+ break
+ }
+ }
+ }
+ return game
+}
+
+// === UNCOMMON TEMPLATE ===
+
+function log_br() {
+ if (game.log.length > 0 && game.log[game.log.length-1] !== "")
+ game.log.push("")
+}
+
+function log(msg) {
+ game.log.push(msg)
+}
+
+function logi(msg) {
+ game.log.push(">" + msg)
+}
+
+function log_h1(msg) {
+ log_br()
+ log(".h1 " + msg)
+ log_br()
+}
+
+function log_h2(msg) {
+ log_br()
+ log(".h2 " + msg)
+ log_br()
+}
+
+function log_h3(msg) {
+ log_br()
+ log(".h3 " + msg)
+ log_br()
+}
+
+function log_h4(msg) {
+ log_br()
+ log(".h4 " + msg)
+}
+
+
+function gen_action(action, argument) {
+ if (!(action in view.actions))
+ view.actions[action] = []
+ set_add(view.actions[action], argument)
+}
+
+function gen_action_locale(locale) {
+ gen_action('locale', locale)
+}
+
+function gen_action_lord(lord) {
+ gen_action('lord', lord)
+}
+
+function gen_action_service(service) {
+ gen_action('service', service)
+}
+
+function gen_action_vassal(vassal) {
+ gen_action('vassal', vassal)
+}
+
+function gen_action_arts_of_war(c) {
+ gen_action('arts_of_war', c)
+}
+
+exports.view = function(state, current) {
+ game = state
+ view = {
+ log: game.log,
+ turn: game.turn,
+ lords: game.lords,
+ vassals: game.vassals,
+ legate: game.legate,
+ veche_vp: game.veche_vp,
+ veche_coin: game.veche_coin,
+ global_cards: game.global_cards,
+ conquered: game.conquered,
+ ravaged: game.ravaged,
+ who: game.who,
+ where: game.where,
+ }
+ if (game.state === 'game_over') {
+ view.prompt = game.victory
+ } else if (current === 'Observer' || game.active !== current) {
+ let inactive = states[game.state].inactive || game.state
+ view.prompt = `Waiting for ${game.active} \u2014 ${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
+}
+
+exports.action = function (state, current, action, arg) {
+ load_state(state)
+ Object.seal(game) // XXX: don't allow adding properties
+ 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
+}
+
+// === COMMON TEMPLATE ===
+
+function random(range) {
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ return (game.seed = game.seed * 200105 % 34359738337) % range
+}
+
+// Packed array of small numbers in one word
+
+function pack1_get(word, n) {
+ return (word >>> n) & 1
+}
+
+function pack4_get(word, n) {
+ n = n << 2
+ return (word >>> n) & 15
+}
+
+function pack1_set(word, n, x) {
+ return (word & ~(1 << n)) | (x << n)
+}
+
+function pack4_set(word, n, x) {
+ n = n << 2
+ return (word & ~(15 << n)) | (x << n)
+}
+
+// Sorted array treated as Set (for JSON)
+function set_index(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
+ return m
+ }
+ return -1
+}
+
+function set_has(set, item) {
+ return set_index(set, item) >= 0
+}
+
+function set_add(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
+ return
+ }
+ set.splice(a, 0, item)
+}
+
+function set_delete(set, item) {
+ let i = set_index(set, item)
+ if (i >= 0)
+ set.splice(i, 1)
+}
+
+function set_clear(set) {
+ set.length = 0
+}
+
+function set_toggle(set, item) {
+ if (set_has(set, item))
+ set_delete(set, item)
+ else
+ set_add(set, item)
+}
+
+function deep_copy(original) {
+ if (Array.isArray(original)) {
+ let n = original.length
+ let copy = new Array(n)
+ for (let i = 0; i < n; ++i) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = deep_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ } else {
+ let copy = {}
+ for (let i in original) {
+ let v = original[i]
+ if (typeof v === "object" && v !== null)
+ copy[i] = deep_copy(v)
+ else
+ copy[i] = v
+ }
+ return copy
+ }
+}
+
+function push_undo() {
+ let copy = {}
+ for (let k in game) {
+ let v = game[k]
+ if (k === "undo") continue
+ else if (k === "log") v = v.length
+ else if (typeof v === "object" && v !== null) v = deep_copy(v)
+ copy[k] = v
+ }
+ game.undo.push(copy)
+}
+
+function pop_undo() {
+ let save_log = game.log
+ let save_undo = game.undo
+ let state = save_undo.pop()
+ save_log.length = state.log
+ state.log = save_log
+ state.undo = save_undo
+ load_state(state)
+}
+
+function clear_undo() {
+ game.undo.length = 0
+}