"use strict" const data = require("./data.js") const BOTH = "Both" const LANCASTER = "Lancaster" const YORK = "York" const P1 = LANCASTER const P2 = YORK const HIT = [ "0", '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ] const MISS = [ "0", '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ] function frac(x) { if (x === 1) return "\xbd" if (x & 1) return (x >> 1) + "\xbd" return x >> 1 } function range(x) { switch (x) { case 1: return "1" case 2: return "1-2" case 3: return "1-3" case 4: return "1-4" case 5: return "1-5" case 6: return "1-6" } } let game = null let view = null let states = {} exports.roles = [ LANCASTER, YORK ] exports.scenarios = [ "Ia. Henry VI", "Ib. Towton", "Ic. Somerset's Return", "II. Warwicks' Rebellion", "III. My Kingdom for a Horse", "I-III. Wars of the Roses", ] const scenario_first_turn = { "Ia. Henry VI": 1, "Ib. Towton": 1, "Ic. Somerset's Return": 5, "II. Warwicks' Rebellion": 1, "III. My Kingdom for a Horse": 9, "I-III. Wars of the Roses": 1, } const scenario_last_turn = { "Ia. Henry VI": 15, "Ib. Towton": 2, "Ic. Somerset's Return": 8, "II. Warwicks' Rebellion": 15, "III. My Kingdom for a Horse": 15, "I-III. Wars of the Roses": 15, } function should_remove_Y28_event_card() { return game.scenario !== "I-III. Wars of the Roses" } // unit types const RETINUE = 0 const VASSAL = 1 const MERCENARIES = 2 const BURGUNDIANS = 3 const MEN_AT_ARMS = 4 const MILITIA = 5 const LONGBOWMEN = 6 const FORCE_TYPE_NAME = [ "Retinue", "Vassal", "Mercenary", "Burgundians", "Men-at-Arms", "Militia", "Longbowmen" ] const FORCE_PROTECTION = [ 4, 4, 3, 3, 3, 1, 1 ] const FORCE_EVADE = [ 0, 0, 0, 0, 0, 0, 0 ] // asset types const PROV = 0 const COIN = 1 const CART = 2 const SHIP = 3 const ASSET_TYPE_NAME = [ "Provender", "Coin", "Cart", "Ship" ] // battle array const A1 = 0 // attackers const A2 = 1 const A3 = 2 const D1 = 3 // defenders const D2 = 4 const D3 = 5 const ARRAY_FLANKS = [ [ A2, A3 ], [ A1, A3 ], [ A1, A2 ], [ D2, D3 ], [ D1, D3 ], [ D1, D2 ], ] function find_card(name) { return data.cards.findIndex((x) => x.name === name) } 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 first_lord = 0 const last_lord = lord_count - 1 /*const first_p1_locale = 0 const last_p1_locale = 23 const first_p2_locale = 24 const last_p2_locale = 52 */ const first_locale = 0 const last_locale = data.locales.length - 1 const first_p1_card = 0 const last_p1_card = 36 const first_p2_card = 37 const last_p2_card = 73 const Y1 = find_card("Y1") const Y2 = find_card("Y2") const Y3 = find_card("Y3") const Y4 = find_card("Y4") const Y5 = find_card("Y5") const Y6 = find_card("Y6") const Y7 = find_card("Y7") const Y8 = find_card("Y8") const Y9 = find_card("Y9") const Y10 = find_card("Y10") const Y11 = find_card("Y11") const Y12 = find_card("Y12") const Y13 = find_card("Y13") const Y14 = find_card("Y14") const Y15 = find_card("Y15") const Y16 = find_card("Y16") const Y17 = find_card("Y17") const Y18 = find_card("Y18") const Y19 = find_card("Y19") const Y20 = find_card("Y20") const Y21 = find_card("Y21") const Y22 = find_card("Y22") const Y23 = find_card("Y23") const Y24 = find_card("Y24") const Y25 = find_card("Y25") const Y26 = find_card("Y26") const Y27 = find_card("Y27") const Y28 = find_card("Y28") const Y29 = find_card("Y29") const Y30 = find_card("Y30") const Y31 = find_card("Y31") const Y32 = find_card("Y32") const Y33 = find_card("Y33") const Y34 = find_card("Y34") const Y35 = find_card("Y35") const Y36 = find_card("Y36") const Y37 = find_card("Y37") const L1 = find_card("L1") const L2 = find_card("L2") const L3 = find_card("L3") const L4 = find_card("L4") const L5 = find_card("L5") const L6 = find_card("L6") const L7 = find_card("L7") const L8 = find_card("L8") const L9 = find_card("L9") const L10 = find_card("L10") const L11 = find_card("L11") const L12 = find_card("L12") const L13 = find_card("L13") const L14 = find_card("L14") const L15 = find_card("L15") const L16 = find_card("L16") const L17 = find_card("L17") const L18 = find_card("L18") const L19 = find_card("L19") const L20 = find_card("L20") const L21 = find_card("L21") const L22 = find_card("L22") const L23 = find_card("L23") const L24 = find_card("L24") const L25 = find_card("L25") const L26 = find_card("L26") const L27 = find_card("L27") const L28 = find_card("L28") const L29 = find_card("L29") const L30 = find_card("L30") const L31 = find_card("L31") const L32 = find_card("L32") const L33 = find_card("L33") const L34 = find_card("L34") const L35 = find_card("L35") const L36 = find_card("L36") const L37 = find_card("L37") //const GARRISON = 100 const LORD_YORK = find_lord("York") const LORD_MARCH = find_lord("March") const LORD_EDWARD_IV = find_lord("Edward IV") const LORD_SALISBURY = find_lord("Salisbury") const LORD_RUTLAND = find_lord("Rutland") const LORD_PEMBROKE = find_lord("Pembroke") const LORD_DEVON = find_lord("Devon") const LORD_NORTHUMBERLANDY = find_lord("Northumberland") const LORD_GLOUCESTER = find_lord("Gloucester") const LORD_RICHARD_III = find_lord("Richard III") const LORD_NORFOLK = find_lord("Norfolk") const LORD_WARWICKY = find_lord("WarwickY") const LORD_HENRYVI = find_lord("Henry VI") const LORD_MARGARET = find_lord("Margaret") const LORD_SOMERSET = find_lord("Somerset") const LORD_EXETER = find_lord("Exeter") const LORD_BUCKINGHAM = find_lord("Buckingham") const LORD_CLARENCE = find_lord("Clarence") const LORD_NORTHUMBERLANDL = find_lord("Northumberland") const LORD_JASPER_TUDOR = find_lord("Jasper Tudor") const LORD_HENRY_TUDOR = find_lord("Henry Tudor") const LORD_OXFORD = find_lord("Oxford") const LORD_WARWICKL = find_lord("WarwickL") const LOC_BAMBURGH = find_locale("Bamburgh") const LOC_NEWCASTLE = find_locale("Newcastle") const LOC_APPLEBY = find_locale("Appleby") const LOC_HEXHAM = find_locale("Hexham") const LOC_CARLISLE = find_locale("Carlisle") const LOC_HARLECH = find_locale("Harlech") const LOC_PEMBROKE = find_locale("Pembroke") const LOC_CARDIFF = find_locale("Cardiff") const LOC_HEREFORD = find_locale("Hereford") const LOC_LUDLOW = find_locale("Ludlow") const LOC_SHREWSBURY = find_locale("Shrewsbury") const LOC_SALISBURY = find_locale("Salisbury") const LOC_WINCHESTER = find_locale("Winchester") const LOC_GUILDFORD = find_locale("Guildford") const LOC_Arundel = find_locale("Arundel") const LOC_SOUTHAMPTON = find_locale("Southampton") const LOC_ROCHESTER = find_locale("Rochester") const LOC_DOVER = find_locale("Dover") const LOC_CANTERBURY = find_locale("Canterbury") const LOC_HASTINGS = find_locale("Hastings") const LOC_DORCHESTER = find_locale("Dorchester") const LOC_EXETER = find_locale("Exeter") const LOC_PLYMOUTH = find_locale("Plymouth") const LOC_LAUNCESTON = find_locale("Launceston") const LOC_TRURO = find_locale("Truro") const LOC_WELLS = find_locale("Wells") const LOC_BRISTOL = find_locale("Bristol") const LOC_GLOUCESTER = find_locale("Gloucester") const LOC_OXFORD = find_locale("Oxford") const LOC_NEwBURY = find_locale("Newbury") const LOC_LONDON = find_locale("London") const LOC_ST_ALBANS = find_locale("St Albans") const LOC_BEDFORD = find_locale("Bedford") const LOC_CAMBRIDGE = find_locale("Cambridge") const LOC_BURY_ST_EDMUNDS = find_locale("Bury St Edmunds") const LOC_IPSWICH = find_locale("Ipswich") const LOC_NORWICH = find_locale("Norwich") const LOC_LYNN = find_locale("Lynn") const LOC_ELY = find_locale("Ely") const LOC_PETERBOROUGH = find_locale("Peterborough") const LOC_NORTHAMPTON = find_locale("Northampton") const LOC_COVENTRY = find_locale("Coventry") const LOC_LEICESTER = find_locale("Leicester") const LOC_LICHFIELD = find_locale("Lichfield") const LOC_DERBY = find_locale("Derby") const LOC_NOTTINGHAM = find_locale("Notthingham") const LOC_WORCESTER = find_locale("Worcester") const LOC_CHESTER = find_locale("Chester") const LOC_LANCASTER = find_locale("Lancaster") const LOC_LINCOLN = find_locale("Lincoln") const LOC_YORK = find_locale("York") const LOC_CALAIS = find_locale("Calais") const LOC_FRANCE = find_locale("France") const LOC_SCOTLAND = find_locale("Scotland") const LOC_IRELAND = find_locale("Ireland") const LOC_BURGUNDY = find_locale("Burgundy") const LOC_ENGLISH_CHANNEL = find_locale("English Channel") const LOC_IRISH_SEA = find_locale("Irish Sea") const LOC_NORTH_SEA = find_locale("North Sea") const LOC_SCARBOROUGH = find_locale("Scarborough") const LOC_RAVENSPUR = find_locale("Ravenspur") // === === === === FROM NEVSKY === === === === // TODO: log end victory conditions at scenario start // WONTFIX: choose crossbow/normal hit application order // Check all push/clear_undo const VASSAL_UNAVAILABLE = 0 const VASSAL_READY = 1 const VASSAL_MUSTERED = 2 const NOBODY = -1 const NOWHERE = -1 const NOTHING = -1 const NEVER = -1 const CALENDAR = 100 function current_turn() { return game.turn >> 1 } function current_turn_name() { return String(game.turn >> 1) } function current_hand() { if (game.active === P1) return game.hand1 return game.hand2 } function is_campaign_phase() { return (game.turn & 1) === 1 } function is_levy_phase() { return (game.turn & 1) === 0 } // === GAME STATE === const first_p1_lord = 0 const last_p1_lord = 13 const first_p2_lord = 14 const last_p2_lord = 27 let first_friendly_lord = 0 let last_friendly_lord = 13 let first_enemy_lord = 14 let last_enemy_lord = 27 function update_aliases() { if (game.active === P1) { first_friendly_lord = 0 last_friendly_lord = 13 first_enemy_lord = 14 last_enemy_lord = 27 } else if (game.active === P2) { first_friendly_lord = 14 last_friendly_lord = 27 first_enemy_lord = 0 last_enemy_lord = 13 } else { first_friendly_lord = -1 last_friendly_lord = -1 first_enemy_lord = -1 last_enemy_lord = -1 } } 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_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 has_any_spoils() { return ( game.spoils && game.spoils[PROV] + game.spoils[COIN] + game.spoils[CART] + game.spoils[SHIP] > 0 ) } function get_spoils(type) { if (game.spoils) return game.spoils[type] return 0 } function add_spoils(type, n) { if (!game.spoils) game.spoils = [ 0, 0, 0, 0, 0, 0, 0 ] game.spoils[type] += n } function get_lord_calendar(lord) { if (is_lord_on_calendar(lord)) return get_lord_locale(lord) - CALENDAR else return get_lord_service(lord) } function set_lord_cylinder_on_calendar(lord, turn) { if (turn < 0) turn = 0 if (turn > 16) turn = 16 set_lord_locale(lord, CALENDAR + turn) } function set_lord_calendar(lord, turn) { if (is_lord_on_calendar(lord)) set_lord_cylinder_on_calendar(lord, turn) else set_lord_service(lord, turn) } function get_lord_locale(lord) { return game.pieces.locale[lord] } function get_lord_service(lord) { return game.pieces.service[lord] } function get_lord_capability(lord, n) { return game.pieces.capabilities[(lord << 1) + n] } function set_lord_capability(lord, n, x) { game.pieces.capabilities[(lord << 1) + n] = x } function get_lord_assets(lord, n) { return pack4_get(game.pieces.assets[lord], n) } function get_lord_forces(lord, n) { return pack4_get(game.pieces.forces[lord], n) } function get_lord_routed_forces(lord, n) { return pack4_get(game.pieces.routed[lord], n) } function lord_has_unrouted_units(lord) { return game.pieces.forces[lord] !== 0 } function lord_has_routed_units(lord) { return game.pieces.routed[lord] !== 0 } function set_lord_locale(lord, locale) { game.pieces.locale[lord] = locale } function shift_lord_cylinder(lord, dir) { set_lord_calendar(lord, get_lord_calendar(lord) + dir) } /*function set_lord_service(lord, service) { if (service < 0) service = 0 if (service > 17) service = 17 game.pieces.service[lord] = service }*/ /*function add_lord_service(lord, n) { set_lord_service(lord, get_lord_service(lord) + n) }*/ function set_lord_assets(lord, n, x) { if (x < 0) x = 0 if (x > 40) x = 40 game.pieces.assets[lord] = pack4_set(game.pieces.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.pieces.forces[lord] = pack4_set(game.pieces.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.pieces.routed[lord] = pack4_set(game.pieces.routed[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 clear_lords_moved() { game.pieces.moved = 0 } function get_lord_moved(lord) { return pack2_get(game.pieces.moved, lord) } function set_lord_moved(lord, x) { game.pieces.moved = pack2_set(game.pieces.moved, lord, x) } function set_lord_fought(lord) { set_lord_moved(lord, 1) game.battle.fought = pack1_set(game.battle.fought, lord, 1) } function get_lord_fought(lord) { return pack1_get(game.battle.fought, lord) } function set_lord_unfed(lord, n) { // reuse "moved" flag for hunger set_lord_moved(lord, n) } function is_lord_unfed(lord) { // reuse "moved" flag for hunger return get_lord_moved(lord) } function feed_lord_skip(lord) { // reuse "moved" flag for hunger set_lord_moved(lord, 0) } function feed_lord(lord) { // reuse "moved" flag for hunger let n = get_lord_moved(lord) - 1 set_lord_moved(lord, n) if (n === 0) log(`Fed L${lord}.`) } function get_lord_array_position(lord) { for (let p = 0; p < 12; ++p) if (game.battle.array[p] === lord) return p return -1 } /*function add_veche_vp(amount) { game.pieces.veche_vp += amount if (game.pieces.veche_vp < 0) game.pieces.veche_vp = 0 if (game.pieces.veche_vp > 8) game.pieces.veche_vp = 8 }*/ // === GAME STATE HELPERS === function roll_die() { return random(6) + 1 } function get_shared_assets(loc, what) { let n = 0 for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === loc) n += get_lord_assets(lord, what) return n } function count_lord_all_forces(lord) { return ( get_lord_forces(lord, RETINUE) + get_lord_forces(lord, VASSAL) + get_lord_forces(lord, BURGUNDIANS) + get_lord_forces(lord, MERCENARIES) + get_lord_forces(lord, MEN_AT_ARMS) + get_lord_forces(lord, MILITIA) + get_lord_forces(lord, LONGBOWMEN) ) } /*function count_lord_horses(lord) { return ( get_lord_forces(lord, KNIGHTS) + get_lord_forces(lord, SERGEANTS) + get_lord_forces(lord, LIGHT_HORSE) + get_lord_forces(lord, ASIATIC_HORSE) ) }*/ function count_lord_ships(lord) { let ships = get_lord_assets(lord, SHIP) return ships } function count_shared_ships() { let here = get_lord_locale(game.command) let n = 0 for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here) n += count_lord_ships(lord) return n } function count_group_ships() { let n = 0 for (let lord of game.group) n += count_lord_ships(lord) return n } function count_group_assets(type) { let n = 0 for (let lord of game.group) n += get_lord_assets(lord, type) return n } function count_group_forces(type) { let n = 0 for (let lord of game.group) n += get_lord_forces(lord, type) return n } /* function count_group_horses() { let n = 0 for (let lord of game.group) n += count_lord_horses(lord) return n }*/ function count_group_transport(type) { let n = 0 for (let lord of game.group) n += count_lord_transport(lord, type) return n } function max_plan_length() { switch (current_season()) { case SUMMER: return 6 case EARLY_WINTER: return 4 case LATE_WINTER: return 4 case RASPUTITSA: return 5 } } function count_cards_in_plan(plan, lord) { let n = 0 for (let c of plan) if (c === lord) ++n return n } /*function is_marshal(lord) { switch (lord) { case LORD_ANDREAS: return true case LORD_HERMANN: return !is_lord_on_map(LORD_ANDREAS) case LORD_ALEKSANDR: return true case LORD_ANDREY: return !is_lord_on_map(LORD_ALEKSANDR) default: return false } }*/ function is_armored_force(type) { return type === MEN_AT_ARMS || type === BURGUNDIANS || type === RETINUE || type === VASSAL || type === MERCENARIES } /* function is_no_event_card(c) { if (c === 18 || c === 19 || c === 20) return true if (c === 39 || c === 40 || c === 41) return true return false }*/ function is_p1_card(c) { return c >= first_p1_card && c <= last_p1_card_no_event } function is_p2_card(c) { return c >= first_p2_card && c <= last_p2_card_no_event } function is_card_in_use(c) { if (set_has(game.hand1, c)) return true if (set_has(game.hand2, c)) return true if (set_has(game.events, c)) return true if (game.pieces.capabilities.includes(c)) return true return false } function list_deck() { let deck = [] let first_card = (game.active === P1) ? first_p1_card : first_p2_card let last_card = (game.active === P1) ? last_p1_card : last_p2_card for (let c = first_card; c <= last_card; ++c) if (!is_card_in_use(c)) deck.push(c) for (let c = last_card + 1; c <= last_card + no; ++c) deck.push(c) return deck } function is_friendly_card(c) { if (game.active === P1) return is_p1_card(c) return is_p2_card(c) } function has_card_in_hand(c) { if (game.active === P1) return set_has(game.hand1, c) return set_has(game.hand2, c) } function can_discard_card(c) { if (set_has(game.hand1, c)) return true if (set_has(game.hand2, c)) return true if (game.pieces.capabilities.includes(c)) return true } function is_lord_on_map(lord) { let loc = get_lord_locale(lord) return loc !== NOWHERE && loc < CALENDAR } function is_lord_in_play(lord) { return get_lord_locale(lord) !== NOWHERE } /* function is_lord_besieged(lord) { return false } function is_lord_unbesieged(lord) { return true }*/ 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_special_vassal_available(vassal) { let cap = data.vassals[vassal].capability if (cap === "Crusade") return has_global_capability(AOW_TEUTONIC_CRUSADE) if (cap === "Steppe Warriors") return has_global_capability(AOW_RUSSIAN_STEPPE_WARRIORS) return true }*/ function is_vassal_ready(vassal) { return game.pieces.vassals[vassal] === VASSAL_READY } function is_vassal_mustered(vassal) { return game.pieces.vassals[vassal] === VASSAL_MUSTERED } function is_york_lord(lord) { return lord >= first_p1_lord && lord <= last_p1_lord } function is_lancaster_lord(lord) { return lord >= first_p2_lord && lord <= last_p2_lord } function is_p1_lord(lord) { return lord >= first_p1_lord && lord <= last_p1_lord } function is_friendly_lord(lord) { return lord >= first_friendly_lord && lord <= last_friendly_lord } function is_lord_at_friendly_locale(lord) { let loc = get_lord_locale(lord) return is_friendly_locale(loc) } /* function used_seat_capability(lord, where, extra) { let seats = data.lords[lord].seats if (extra) { if (set_has(seats, where) && !extra.includes(where)) return -1 } else { if (set_has(seats, where)) return -1 } if (is_teutonic_lord(lord)) if (has_global_capability(AOW_TEUTONIC_ORDENSBURGEN)) return AOW_TEUTONIC_ORDENSBURGEN if (is_russian_lord(lord)) if (has_global_capability(AOW_RUSSIAN_ARCHBISHOPRIC)) return AOW_RUSSIAN_ARCHBISHOPRIC return -1 }*/ /* function for_each_seat(lord, fn, repeat = false) { let list = data.lords[lord].seats for (let seat of list) fn(seat) if (is_teutonic_lord(lord)) { if (has_global_capability(AOW_TEUTONIC_ORDENSBURGEN)) { for (let commandery of COMMANDERIES) if (repeat || !set_has(list, commandery)) fn(commandery) } } if (is_russian_lord(lord)) { if (has_global_capability(AOW_RUSSIAN_ARCHBISHOPRIC)) if (repeat || !set_has(list, LOC_NOVGOROD)) fn(LOC_NOVGOROD) } if (lord === LORD_YAROSLAV) { if (has_conquered_marker(LOC_PSKOV)) if (repeat || !set_has(list, LOC_PSKOV)) fn(LOC_PSKOV) } }*/ function is_lord_seat(lord, here) { let result = false for_each_seat(lord, seat => { if (seat === here) result = true }) return result } function is_lord_at_seat(lord) { return is_lord_seat(lord, get_lord_locale(lord)) } function has_free_seat(lord) { let result = false for_each_seat(lord, seat => { if (!result && is_friendly_locale(seat)) result = true }) return result } function has_york_lord(here) { for (let lord = first_p1_lord; lord <= last_p1_lord; ++lord) if (get_lord_locale(lord) === here) return true } function has_lancaster_lord(here) { for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord) if (get_lord_locale(lord) === here) return true } function has_friendly_lord(loc) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === loc) return true return false } /* function has_besieged_friendly_lord(loc) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === loc && is_lord_besieged(lord)) return true return false }*/ function has_enemy_lord(loc) { for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) if (get_lord_locale(lord) === loc) return true return false } /* function has_unbesieged_enemy_lord(loc) { for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) if (get_lord_locale(lord) === loc && is_lord_unbesieged(lord)) return true return false } function has_unbesieged_friendly_lord(loc) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === loc && is_lord_unbesieged(lord)) return true return false } */ function is_p1_locale(loc) { return loc >= first_p1_locale && loc <= last_p1_locale } function is_p2_locale(loc) { return loc >= first_p2_locale && loc <= last_p2_locale } /* function is_friendly_territory(loc) { if (game.active === P1) return loc >= first_p1_locale && loc <= last_p1_locale return loc >= first_p2_locale && loc <= last_p2_locale } function is_enemy_territory(loc) { if (game.active === P1) return loc >= first_p2_locale && loc <= last_p2_locale return loc >= first_p1_locale && loc <= last_p1_locale }*/ function is_seaport(loc) { return set_has(data.seaports, loc) } function is_city(loc) { return data.locales[loc].type === "city" } function is_town(loc) { return data.locales[loc].type === "town" } function is_fortress(loc) { return data.locales[loc].type === "fortress" } function is_calais(loc) { return data.locales[loc].type === "calais" } function is_sea(loc) { return data.locales[loc].type === "sea" } function is_london(loc) { return data.locales[loc].type === "london" } function is_harlech(loc) { return data.locales[loc].type === "harlech" } function is_stronghold(loc) { return data.locales[loc].stronghold > 0 } function has_favour_marker(loc) { return set_has(game.pieces.favour, loc) } function add_favour_marker(loc) { set_add(game.pieces.favour, loc) } function remove_favour_marker(loc) { set_delete(game.pieces.favour, loc) } function has_exhausted_marker(loc) { return set_has(game.pieces.exhausted, loc) } function add_exhausted_marker(loc) { set_add(game.pieces.exhausted, loc) } function remove_exhausted_marker(loc) { set_delete(game.pieces.ravaged, loc) } /* function conquer_trade_route(loc) { if (game.active === RUSSIANS) { if (has_conquered_marker(loc)) { log(`Conquered %${loc}.`) remove_conquered_marker(loc) } } else { if (!has_conquered_marker(loc)) { log(`Conquered %${loc}.`) add_conquered_marker(loc) } } } function conquer_stronghold(loc) { if (has_castle_marker(loc)) flip_castle(loc) remove_all_siege_markers(loc) if (is_enemy_territory(loc)) add_conquered_marker(loc) else remove_conquered_marker(loc) } function count_castles() { return game.pieces.castles1.length + game.pieces.castles2.length } function add_friendly_castle(loc) { // only P1 can add set_add(game.pieces.castles1, loc) } function has_enemy_castle(loc) { if (game.active === P1) return set_has(game.pieces.castles2, loc) return set_has(game.pieces.castles1, loc) } function has_friendly_castle(loc) { if (game.active === P1) return set_has(game.pieces.castles1, loc) return set_has(game.pieces.castles2, loc) } function has_castle_marker(loc) { return ( set_has(game.pieces.castles1, loc) || set_has(game.pieces.castles2, loc) ) } function flip_castle(loc) { if (game.active === P1) { set_delete(game.pieces.castles2, loc) set_add(game.pieces.castles1, loc) } else { set_delete(game.pieces.castles1, loc) set_add(game.pieces.castles2, loc) } } function is_friendly_stronghold_locale(loc) { if (is_stronghold(loc) || has_friendly_castle(loc)) return is_friendly_locale(loc) return false } function is_enemy_stronghold(loc) { if (is_stronghold(loc)) { if (is_enemy_territory(loc) && !has_conquered_marker(loc)) return true if (is_friendly_territory(loc) && has_conquered_marker(loc)) return true } if (has_enemy_castle(loc)) return true return false } function is_friendly_stronghold(loc) { if (is_stronghold(loc)) { if (is_friendly_territory(loc) && !has_conquered_marker(loc)) return true if (is_enemy_territory(loc) && has_conquered_marker(loc)) return true } if (has_friendly_castle(loc)) return true return false } /* function is_unbesieged_enemy_stronghold(loc) { return is_enemy_stronghold(loc) && !has_siege_marker(loc) } function is_unbesieged_friendly_stronghold(loc) { return is_friendly_stronghold(loc) && !has_siege_marker(loc) } function is_besieged_enemy_stronghold(loc) { return is_enemy_stronghold(loc) && has_siege_marker(loc) } */ function is_friendly_locale(loc) { if (loc !== NOWHERE && loc < CALENDAR) { if (has_enemy_lord(loc)) return false if (has_favour_marker(loc)) { //to add friendly favour marker later return true } } return false } function can_add_transport(who, what) { return get_lord_assets(who, what) < 100 } function count_lord_transport(lord, type) { let season = current_season() let n = 0 n += get_lord_assets(lord, CART) return n } function list_ways(from, to) { for (let ways of data.locales[from].ways) if (ways[0] === to) return ways return null } function group_has_capability(c) { for (let lord of game.group) if (lord_has_capability(lord, c)) return true return false } /* function count_unbesieged_friendly_lords(loc) { let n = 0 for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === loc && is_lord_unbesieged(lord)) ++n return n } */ // === MAP === function calculate_distance(start, adjacent) { let queue = [] queue.push([start, 0]) let distance = new Array(last_locale+1).fill(-1) distance[start] = 0 while (queue.length > 0) { let [ here, d ] = queue.shift() for (let next of data.locales[here][adjacent]) { if (distance[next] < 0) { distance[next] = d+1 queue.push([next, d+1]) } } } return distance } for (let loc = 0; loc <= last_locale; ++loc) data.locales[loc].distance = calculate_distance(loc, "adjacent") function locale_distance(from, to) { return data.locales[from].distance[to] } // === SETUP === function muster_lord_forces(lord) { let info = data.lords[lord] set_lord_forces(lord, RETINUE, info.forces.retinue | 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, LONGBOWMEN, info.forces.longbowmen | 0) } /* function muster_vassal_forces(lord, vassal) { let info = data.vassals[vassal] add_lord_forces(lord, RETINUE, info.forces.knights | 0) add_lord_forces(lord, SERGEANTS, info.forces.sergeants | 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) }*/ function restore_lord_forces(lord, type, count) { if (get_lord_forces(lord, type) < count) { set_lord_forces(lord, type, count) return 1 } return 0 } function muster_lord(lord, locale) { let info = data.lords[lord] set_lord_locale(lord, locale) set_lord_assets(lord, PROV, info.assets.prov | 0) set_lord_assets(lord, COIN, info.assets.coin | 0) set_lord_assets(lord, CART, info.assets.cart | 0) set_lord_assets(lord, SHIP, info.assets.ship | 0) muster_lord_forces(lord) } /* function disband_vassal(vassal) { let info = data.vassals[vassal] let lord = data.vassals[vassal].lord add_lord_forces(lord, KNIGHTS, -(info.forces.knights | 0)) add_lord_forces(lord, SERGEANTS, -(info.forces.sergeants | 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)) game.pieces.vassals[vassal] = VASSAL_READY if (!lord_has_unrouted_units(lord)) { disband_lord(lord) } } */ function muster_vassal(lord, vassal) { game.pieces.vassals[vassal] = VASSAL_MUSTERED muster_vassal_forces(lord, vassal) } function draw_card(deck) { let i = random(deck.length) let c = deck[i] set_delete(deck, c) return c } function discard_events(when) { for (let i = 0; i < game.events.length; ) { let c = game.events[i] if (data.cards[c].when === when) array_remove(game.events, i) else ++i } } function discard_friendly_events(when) { for (let i = 0; i < game.events.length; ) { let c = game.events[i] if (is_friendly_card(c) && data.cards[c].when === when) array_remove(game.events, i) else ++i } } exports.setup = function (seed, scenario, options) { game = { seed, scenario, hidden: options.hidden ? 1 : 0, log: [], undo: [], active: P1, state: "setup_lords", stack: [], hand1: [], hand2: [], plan1: [], plan2: [], turn: 0, events: [], // this levy/this campaign cards pieces: { locale: Array(lord_count).fill(NOWHERE), assets: Array(lord_count).fill(0), forces: Array(lord_count).fill(0), routed: Array(lord_count).fill(0), capabilities: Array(lord_count << 1).fill(NOTHING), moved: 0, vassals: Array(vassal_count).fill(VASSAL_UNAVAILABLE), exhausted: [], favour: [], }, flags: { first_action: 0, first_march: 0, }, command: NOBODY, actions: 0, group: 0, who: NOBODY, where: NOWHERE, what: NOTHING, count: 0, supply: 0, march: 0, battle: 0, spoils: 0, } update_aliases() log_h1(scenario) switch (scenario) { default: case "Ia. Henry VI": setup_Ia() break case "Ib. Towton": setup_Ib() break case "Ic. Somerset's Return": setup_Ic() break case "II. Warwicks' Rebellion" : setup_II() break case "III. My Kingdom for a Horse": setup_III() break case "I-III. Wars of the Roses": setup_ItoIII() break } return game } function setup_Ia() { game.turn = 1 << 1 muster_lord(LORD_YORK, LOC_ELY) muster_lord(LORD_MARCH, LOC_LUDLOW) muster_lord(LORD_HENRY_VI, LOC_LONDON) muster_lord(LORD_SOMERSET, LOC_WELLS) set_lord_cylinder_on_calendar(LORD_NORTHUMBERLANDL, 2) set_lord_cylinder_on_calendar(LORD_EXETER, 3) set_lord_cylinder_on_calendar(LORD_BUCKINGHAM, 5) set_lord_cylinder_on_calendar(LORD_SALISBURY, 2) set_lord_cylinder_on_calendar(LORD_WARWICKY, 3) set_lord_cylinder_on_calendar(LORD_RUTLAND, 5) } function setup_Ib() { game.turn = 1 << 1 muster_lord(LORD_NORFOLK, LOC_LONDON) muster_lord(LORD_WARWICKY, LOC_LONDON) muster_lord(LORD_MARCH, LOC_LUDLOW) muster_lord(LORD_EXETER, LOC_NEWCASTLE) muster_lord(LORD_SOMERSET, LOC_NEWCASTLE) muster_lord(LORD_NORTHUMBERLANDL, LOC_CARLISLE) } function setup_Ic() { game.turn = 1 << 1 muster_lord(LORD_WARWICKY, LOC_LONDON) muster_lord(LORD_MARCH, LOC_LONDON) muster_lord(LORD_SOMERSET, LOC_BAMBURGH) set_lord_cylinder_on_calendar(LORD_HENRY_VI, 5) } function setup_II() { game.turn = 1 << 1 muster_lord(LORD_EDWARD_IV, LOC_LONDON) muster_lord(LORD_PEMBROKE, LOC_LONDON) muster_lord(LORD_WARWICKL, LOC_CALAIS) muster_lord(LORD_CLARENCE, LOC_YORK) muster_lord(LORD_JASPER_TUDOR, LOC_HARLECH) set_lord_cylinder_on_calendar(LORD_DEVON, 4) set_lord_cylinder_on_calendar(LORD_GLOUCESTER, 9) set_lord_cylinder_on_calendar(LORD_NORTHUMBERLANDY, 9) set_lord_cylinder_on_calendar(LORD_MARGARET, 9) set_lord_cylinder_on_calendar(LORD_SOMERSET, 9) set_lord_cylinder_on_calendar(LORD_OXFORD, 9) set_lord_cylinder_on_calendar(LORD_EXETER, 9) } function setup_III() { game.turn = 1 << 1 muster_lord(LORD_RICHARD_III, LOC_LONDON) muster_lord(LORD_NORTHUMBERLANDY, LOC_CARLISLE) muster_lord(LORD_NORFOLK, LOC_ARUNDEL) muster_lord(LORD_HENRY_TUDOR, LOC_FRANCE) muster_lord(LORD_JASPER_TUDOR, LOC_FRANCE) muster_lord(LORD_OXFORD, LOC_FRANCE) } function setup_ItoIII() { game.turn = 1 << 1 muster_lord(LORD_YORK, LOC_ELY) muster_lord(LORD_MARCH, LOC_LUDLOW) muster_lord(LORD_HENRY_VI, LOC_LONDON) muster_lord(LORD_SOMERSET, LOC_WELLS) set_lord_cylinder_on_calendar(LORD_NORTHUMBERLANDL, 2) set_lord_cylinder_on_calendar(LORD_EXETER, 3) set_lord_cylinder_on_calendar(LORD_BUCKINGHAM, 5) set_lord_cylinder_on_calendar(LORD_SALISBURY, 2) set_lord_cylinder_on_calendar(LORD_WARWICKY, 3) set_lord_cylinder_on_calendar(LORD_RUTLAND, 5) } /* states.setup_lords = { inactive: "Set up Lords", prompt() { view.prompt = "Set up 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() // FIXME: clean up these transitions push_state("muster_lord_transport") set_lord_moved(lord, 1) game.who = lord game.count = data.lords[lord].assets.transport }, } */ function end_setup() { clear_lords_moved() set_active_enemy() if (game.active === P1) { log_h1("Levy " + current_turn_name()) goto_levy_arts_of_war_first() } } // === EVENTS === function is_event_in_play(c) { return set_has(game.events, c) } function is_leeward_battle_line_in_play () { if (is_archery_step()) { if (game.active === LANCASTER) return is_event_in_play(EVENT_LANCASTER_LEEWARD_BATTLE_LINE) if (game.active === YORK) return is_event_in_play(EVENT_YORK_LEEWARD_BATTLE_LINE) } return false } function is_marsh_in_play() { if (game.battle.round <= 2) { if (game.active === TEUTONS && is_event_in_play(EVENT_RUSSIAN_MARSH)) return true if (game.active === RUSSIANS && is_event_in_play(EVENT_TEUTONIC_MARSH)) return true } return false } function is_hill_in_play() { if (game.battle.round <= 2) { if (game.active === TEUTONS && is_event_in_play(EVENT_TEUTONIC_HILL)) return true if (game.active === RUSSIANS && is_event_in_play(EVENT_RUSSIAN_HILL)) return true } return false } function is_famine_in_play() { if (game.active === TEUTONS) if (is_event_in_play(EVENT_RUSSIAN_FAMINE)) return true if (game.active === RUSSIANS) if (is_event_in_play(EVENT_TEUTONIC_FAMINE)) return true return false } function no_muster_of_or_by_lord(lord) { if (lord === LORD_KNUD_ABEL) return is_event_in_play(EVENT_RUSSIAN_VALDEMAR) if (lord === LORD_ANDREAS || lord === LORD_RUDOLF) return is_event_in_play(EVENT_RUSSIAN_DIETRICH_VON_GRUNINGEN) return false } function goto_immediate_event(c) { switch (c) { // This Levy / Campaign case EVENT_TEUTONIC_FAMINE: case EVENT_RUSSIAN_FAMINE: set_add(game.events, c) // No immediate effects return end_immediate_event() case EVENT_RUSSIAN_DEATH_OF_THE_POPE: set_add(game.events, c) return goto_russian_event_death_of_the_pope() case EVENT_RUSSIAN_VALDEMAR: set_add(game.events, c) return goto_russian_event_valdemar() case EVENT_RUSSIAN_DIETRICH_VON_GRUNINGEN: set_add(game.events, c) return goto_russian_event_dietrich() // Add to capabilities... case EVENT_TEUTONIC_POPE_GREGORY: deploy_global_capability(c) return goto_teutonic_event_pope_gregory() // Discard case EVENT_TEUTONIC_GRAND_PRINCE: return goto_teutonic_event_grand_prince() case EVENT_TEUTONIC_KHAN_BATY: return goto_teutonic_event_khan_baty() case EVENT_TEUTONIC_SWEDISH_CRUSADE: return goto_teutonic_event_swedish_crusade() case EVENT_RUSSIAN_OSILIAN_REVOLT: return goto_russian_event_osilian_revolt() case EVENT_RUSSIAN_BATU_KHAN: return goto_russian_event_batu_khan() case EVENT_RUSSIAN_PRUSSIAN_REVOLT: return goto_russian_event_prussian_revolt() case EVENT_TEUTONIC_BOUNTIFUL_HARVEST: return goto_event_bountiful_harvest() case EVENT_RUSSIAN_BOUNTIFUL_HARVEST: return goto_event_bountiful_harvest() case EVENT_TEUTONIC_MINDAUGAS: return goto_teutonic_event_mindaugas() case EVENT_RUSSIAN_MINDAUGAS: return goto_russian_event_mindaugas() case EVENT_TEUTONIC_TORZHOK: return goto_teutonic_event_torzhok() case EVENT_RUSSIAN_TEMPEST: return goto_russian_event_tempest() default: log("NOT IMPLEMENTED") return end_immediate_event() } } function end_immediate_event() { clear_undo() resume_levy_arts_of_war() } // === EVENTS: UNIQUE IMMEDIATE EVENTS === // === EVENTS: SHIFT LORD OR SERVICE (IMMEDIATE) === /* function prompt_shift_lord_on_calendar(boxes) { if (game.who !== NOBODY) { // Shift in direction beneficial to active player. if (is_friendly_lord(game.who)) { if (is_lord_on_calendar(game.who)) gen_action_calendar(get_lord_calendar(game.who) - boxes) else gen_action_calendar(get_lord_calendar(game.who) + boxes) } else { if (is_lord_on_calendar(game.who)) gen_action_calendar(get_lord_calendar(game.who) + boxes) else gen_action_calendar(get_lord_calendar(game.who) - boxes) } } } */ // === EVENTS: HOLD === function play_held_event(c) { log(`Played E${c}.`) if (c >= first_p1_card && c <= last_p1_card_no_event) set_delete(game.hand1, c) else set_delete(game.hand2, c) } function end_held_event() { pop_state() game.what = NOTHING } function prompt_held_event() { for (let c of current_hand()) if (can_play_held_event(c)) gen_action_card(c) } function prompt_held_event_lordship() { for (let c of current_hand()) if (can_play_held_event(c) || can_play_held_event_lordship(c)) gen_action_card(c) } function can_play_held_event(c) { switch (c) { } return false } function can_play_held_event_lordship(c) { switch (c) { } return false } function action_held_event(c) { push_undo() play_held_event(c) game.what = c goto_held_event(c) } function goto_held_event(c) { switch (c) { } } // === EVENTS: HOLD - UNIQUE === // === EVENTS: HOLD - SHIFT CYLINDER === function action_held_event_lordship(c) { push_undo() play_held_event(c) if (can_play_held_event(c)) { goto_held_event(c) game.what = c } else { push_state("lordship") game.what = c } } /* states.lordship = { get inactive() { return data.cards[game.what].event }, prompt() { view.prompt = `${data.cards[game.what].event}: Play for +2 Lordship.` view.actions.lordship = 1 }, lordship() { end_held_event() log("+2 Lordship") game.count += 2 } }*/ function prompt_shift_cylinder(list, boxes) { // HACK: look at parent state to see if this can be used as a +2 Lordship event let lordship = NOBODY let parent = game.stack[game.stack.length-1] if (parent[0] === "levy_muster_lord") lordship = parent[1] let names if (game.what === EVENT_RUSSIAN_PRINCE_OF_POLOTSK) names = "a Russian Lord" else names = list.filter(lord => is_lord_on_calendar(lord)).map(lord => lord_name[lord]).join(" or ") if (boxes === 1) view.prompt = `${data.cards[game.what].event}: Shift ${names} 1 Calendar box` else view.prompt = `${data.cards[game.what].event}: Shift ${names} 2 Calendar boxes` for (let lord of list) { if (lord === lordship) { view.prompt += " or +2 Lordship" view.actions.lordship = 1 } if (is_lord_on_calendar(lord)) prompt_select_lord(lord) } view.prompt += "." prompt_shift_lord_on_calendar(boxes) } /* function action_shift_cylinder_calendar(turn) { log(`Shifted L${game.who} to ${turn}.`) set_lord_calendar(game.who, turn) game.who = NOBODY end_held_event() } function action_shift_cylinder_lordship() { end_held_event() log("+2 Lordship") game.count += 2 }*/ // === CAPABILITIES === // === LEVY: ARTS OF WAR (FIRST TURN) === function draw_two_cards() { let deck = list_deck() return [ draw_card(deck), draw_card(deck) ] } function discard_card_capability(c) { log(`${game.active} discarded C${c}.`) } function discard_card_event(c) { log(`${game.active} discarded E${c}.`) } function goto_levy_arts_of_war_first() { if (game.active === TEUTONS) log_h2("Teutonic Arts of War") else log_h2("Russian Arts of War") game.state = "levy_arts_of_war_first" game.what = draw_two_cards() } function resume_levy_arts_of_war_first() { if (game.what.length === 0) end_levy_arts_of_war_first() } states.levy_arts_of_war_first = { inactive: "Arts of War", prompt() { let c = game.what[0] view.arts_of_war = game.what view.what = c if (is_no_event_card(c)) { view.prompt = `Arts of War: No Capability.` view.actions.discard = 1 } else if (data.cards[c].this_lord) { let discard = true for (let lord of data.cards[c].lords) { if (is_lord_on_map(lord) && !lord_already_has_capability(lord, c)) { gen_action_lord(lord) discard = false } } if (discard) { view.prompt = `Arts of War: Discard ${data.cards[c].capability}.` view.actions.discard = 1 } else { view.prompt = `Arts of War: Assign ${data.cards[c].capability} to a Lord.` } } else { view.prompt = `Arts of War: Deploy ${data.cards[c].capability}.` view.actions.deploy = 1 } }, lord(lord) { push_undo() let c = game.what.shift() log(`${game.active} deployed Capability.`) add_lord_capability(lord, c) resume_levy_arts_of_war_first() }, deploy() { push_undo() let c = game.what.shift() log(`${game.active} deployed C${c}.`) deploy_global_capability(c) resume_levy_arts_of_war_first() }, discard() { push_undo() let c = game.what.shift() discard_card_capability(c) resume_levy_arts_of_war_first() }, } function end_levy_arts_of_war_first() { clear_undo() game.what = NOTHING set_active_enemy() if (game.active === P2) goto_levy_arts_of_war_first() else goto_pay() } // === LEVY: ARTS OF WAR === function goto_levy_arts_of_war() { if (game.active === TEUTONS) log_h2("Teutonic Arts of War") else log_h2("Russian Arts of War") game.what = draw_two_cards() resume_levy_arts_of_war() } function resume_levy_arts_of_war() { game.state = "levy_arts_of_war" if (game.what.length === 0) end_levy_arts_of_war() } states.levy_arts_of_war = { inactive: "Arts of War", prompt() { let c = game.what[0] view.arts_of_war = [ c ] view.what = c switch (data.cards[c].when) { case "this_levy": case "this_campaign": case "now": view.prompt = `Arts of War: Play ${data.cards[c].event}.` view.actions.play = 1 break case "hold": view.prompt = `Arts of War: Hold ${data.cards[c].event}.` view.actions.hold = 1 break case "never": view.prompt = `Arts of War: Discard ${data.cards[c].event}.` view.actions.discard = 1 break } }, play() { let c = game.what.shift() log(`${game.active} played E${c}.`) goto_immediate_event(c) }, hold() { let c = game.what.shift() log(`${game.active} Held Event.`) if (game.active === P1) set_add(game.hand1, c) else set_add(game.hand2, c) resume_levy_arts_of_war() }, discard() { let c = game.what.shift() discard_card_event(c) resume_levy_arts_of_war() }, } function end_levy_arts_of_war() { game.what = NOTHING set_active_enemy() if (game.active === P2) goto_levy_arts_of_war() else goto_pay() } // === LEVY: MUSTER === function goto_levy_muster() { if (game.active === TEUTONS) log_h2("Teutonic Muster") else log_h2("Russian Muster") game.state = "levy_muster" } function end_levy_muster() { clear_lords_moved() set_active_enemy() if (game.active === P2) goto_levy_muster() else goto_levy_call_to_arms() } states.levy_muster = { inactive: "Muster", prompt() { view.prompt = "Levy: Muster with your Lords." prompt_held_event() 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)) { if (!no_muster_of_or_by_lord(lord)) { gen_action_lord(lord) done = false } } } if (done) { view.prompt += " All done." view.actions.end_muster = 1 } }, lord(lord) { push_undo() log(`Mustered with L${lord}.`) push_state("levy_muster_lord") game.who = lord game.count = data.lords[lord].lordship }, end_muster() { clear_undo() end_levy_muster() }, card: action_held_event, } function resume_levy_muster_lord() { --game.count if (game.count === 0) { set_lord_moved(game.who, 1) pop_state() } } states.levy_muster_lord = { inactive: "Muster", prompt() { if (game.count === 1) view.prompt = `Levy: ${lord_name[game.who]} has ${game.count} action.` else view.prompt = `Levy: ${lord_name[game.who]} has ${game.count} actions.` prompt_held_event_lordship() if (game.count > 0) { // Roll to muster Ready Lord at Seat for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (no_muster_of_or_by_lord(lord)) continue if (is_lord_ready(lord) && has_free_seat(lord)) 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.take_ship = 1 } if (can_add_transport(game.who, CART)) view.actions.take_cart = 1 // Add Capability if (can_muster_capability()) view.actions.capability = 1 } view.actions.done = 1 }, card: action_held_event_lordship, lord(other) { clear_undo() let die = roll_die() let fealty = data.lords[other].fealty if (die <= fealty) { log(`L${other} ${range(fealty)}: ${HIT[die]}`) push_state("muster_lord_at_seat") game.who = other } else { log(`L${other} ${range(fealty)}: ${MISS[die]}`) resume_levy_muster_lord() } }, vassal(vassal) { push_undo() muster_vassal(game.who, vassal) resume_levy_muster_lord() }, take_ship() { push_undo() add_lord_assets(game.who, SHIP, 1) resume_levy_muster_lord() }, take_cart() { push_undo() add_lord_assets(game.who, CART, 1) resume_levy_muster_lord() }, capability() { push_undo() push_state("muster_capability") }, done() { set_lord_moved(game.who, 1) pop_state() }, } states.muster_lord_at_seat = { inactive: "Muster", prompt() { view.prompt = `Muster: Select Seat for ${lord_name[game.who]}.` for_each_seat(game.who, seat => { if (is_friendly_locale(seat)) gen_action_locale(seat) }) }, locale(loc) { push_undo() let cap = used_seat_capability(game.who, loc) if (cap >= 0) log(`L${game.who} to %${loc} (C${cap}).`) else log(`L${game.who} to %${loc}.`) // FIXME: clean up these transitions set_lord_moved(game.who, 1) muster_lord(game.who, loc) game.state = "muster_lord_transport" game.count = data.lords[game.who].assets.transport | 0 resume_muster_lord_transport() }, } function resume_muster_lord_transport() { if (game.count === 0) pop_state() if (game.state === "levy_muster_lord") resume_levy_muster_lord() } states.muster_lord_transport = { inactive: "Muster", prompt() { if (game.state === "veliky_knyaz") view.prompt = `Veliky Knyaz: Select Transport for ${lord_name[game.who]}.` else view.prompt = `Muster: 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.take_ship = 1 } if (can_add_transport(game.who, CART)) view.actions.take_cart = 1 }, take_ship() { push_undo() add_lord_assets(game.who, SHIP, 1) --game.count resume_muster_lord_transport() }, take_cart() { push_undo() add_lord_assets(game.who, CART, 1) --game.count resume_muster_lord_transport() }, } function lord_has_capability_card(lord, c) { if (get_lord_capability(lord, 0) === c) return true if (get_lord_capability(lord, 1) === c) return true return false } function lord_has_capability(lord, card_or_list) { if (Array.isArray(card_or_list)) { for (let card of card_or_list) if (lord_has_capability_card(lord, card)) return true return false } return lord_has_capability_card(lord, card_or_list) } function lord_already_has_capability(lord, c) { // compare capabilities by name... let name = data.cards[c].capability let c1 = get_lord_capability(lord, 0) if (c1 >= 0 && data.cards[c1].capability === name) return true let c2 = get_lord_capability(lord, 1) if (c2 >= 0 && data.cards[c2].capability === name) return true return false } function can_add_lord_capability(lord) { if (get_lord_capability(lord, 0) < 0) return true if (get_lord_capability(lord, 1) < 0) return true return false } function add_lord_capability(lord, c) { if (get_lord_capability(lord, 0) < 0) return set_lord_capability(lord, 0, c) if (get_lord_capability(lord, 1) < 0) return set_lord_capability(lord, 1, c) throw new Error("no empty capability slots!") } function discard_lord_capability_n(lord, n) { set_lord_capability(lord, n, NOTHING) } function discard_lord_capability(lord, c) { if (get_lord_capability(lord, 0) === c) return set_lord_capability(lord, 0, NOTHING) if (get_lord_capability(lord, 1) === c) return set_lord_capability(lord, 1, NOTHING) throw new Error("capability not found") } function can_muster_capability() { let deck = list_deck() for (let c of deck) { if (is_no_event_card(c)) continue if (!data.cards[c].lords || set_has(data.cards[c].lords, game.who)) { if (data.cards[c].this_lord) { if (!lord_already_has_capability(game.who, c)) return true } else { if (can_deploy_global_capability(c)) return true } } } return false } states.muster_capability = { inactive: "Muster", prompt() { let deck = list_deck() view.prompt = `Muster: Select a new Capability for ${lord_name[game.who]}.` view.arts_of_war = deck for (let c of deck) { if (is_no_event_card(c)) continue if (!data.cards[c].lords || set_has(data.cards[c].lords, game.who)) { if (data.cards[c].this_lord) { if (!lord_already_has_capability(game.who, c)) gen_action_card(c) } else { if (can_deploy_global_capability(c)) gen_action_card(c) } } } }, card(c) { if (data.cards[c].this_lord) { if (can_add_lord_capability(game.who, c)) { add_lord_capability(game.who, c) } else { game.what = c game.state = "muster_capability_discard" return } } else { deploy_global_capability(c) } pop_state() resume_levy_muster_lord() }, } states.muster_capability_discard = { inactive: "Muster", prompt() { view.prompt = `Muster: Remove a Capability from ${lord_name[game.who]}.` gen_action_card(get_lord_capability(game.who, 0)) gen_action_card(get_lord_capability(game.who, 1)) }, card(c) { push_undo() discard_lord_capability(game.who, c) add_lord_capability(game.who, game.what) game.what = NOTHING pop_state() resume_levy_muster_lord() }, } // === LEVY: CALL TO ARMS === function goto_levy_call_to_arms() { if (game.active === TEUTONS) goto_teutonic_call_to_arms() else goto_russian_call_to_arms() } function end_levy_call_to_arms() { clear_undo() clear_lords_moved() set_active_enemy() if (game.active === P2) goto_levy_call_to_arms() else goto_levy_discard_events() } function goto_levy_discard_events() { // Discard "This Levy" events from play. discard_events("this_levy") set_active(P1) goto_capability_discard() } // === LEVY: CALL TO ARMS - PAPAL LEGATE === function goto_teutonic_call_to_arms() { end_levy_call_to_arms() } // === LEVY: CALL TO ARMS - NOVGOROD VECHE === function goto_russian_call_to_arms() { end_levy_call_to_arms() } // === CAMPAIGN: CAPABILITY DISCARD === function count_mustered_lords() { let n = 0 for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (is_lord_on_map(lord)) ++n return n } function count_global_capabilities() { let n = 0 for (let c of game.capabilities) { if (game.active === P1 && c >= first_p1_card && c <= last_p1_card) ++n if (game.active === P2 && c >= first_p2_card && c <= last_p2_card) ++n } return n } function goto_capability_discard() { // Start of Campaign phase if (check_campaign_victory()) return if (count_global_capabilities() > count_mustered_lords()) game.state = "capability_discard" else end_capability_discard() } states.capability_discard = { inactive: "Discard Capabilities", prompt() { if (count_global_capabilities() > count_mustered_lords()) { view.prompt = "Discard Capabilities in excess of Mustered Lords." for (let c of game.capabilities) { if (game.active === P1 && c >= first_p1_card && c <= last_p1_card) gen_action_card(c) if (game.active === P2 && c >= first_p2_card && c <= last_p2_card) gen_action_card(c) } } else { view.prompt = "Discard Capabilities: All done." view.actions.end_discard = 1 } }, card(c) { push_undo() discard_global_capability(c) }, end_discard() { clear_undo() end_capability_discard() }, } function end_capability_discard() { set_active_enemy() if (game.active === P2) goto_capability_discard() else goto_campaign_plan() } // === CAMPAIGN: PLAN === function goto_campaign_plan() { game.turn++ log_h1("Campaign " + current_turn_name()) set_active(BOTH) game.state = "campaign_plan" game.plan1 = [] game.plan2 = [] } function plan_has_lieutenant(first, last) { for (let lord = first; lord <= last; ++lord) if (is_upper_lord(lord)) return true return false } function plan_selected_lieutenant(first, last) { for (let lord = first; lord <= last; ++lord) if (is_upper_lord(lord) && get_lower_lord(lord) === NOBODY) return lord return NOBODY } function plan_can_make_lieutenant(plan, upper, first, last) { for (let lord = first; lord <= last; ++lord) { if (!is_lord_on_map(lord)) continue if (lord === upper) continue if (is_marshal(lord) || is_lord_besieged(lord)) continue if (is_upper_lord(lord) || is_lower_lord(lord)) continue if (get_lord_locale(upper) === get_lord_locale(lord)) return true } return false } states.campaign_plan = { inactive: "Plan", prompt(current) { let plan = current === P1 ? game.plan1 : game.plan2 let first = current === P1 ? first_p1_lord : first_p2_lord let last = current === P1 ? last_p1_lord : last_p2_lord let upper = plan_selected_lieutenant(first, last) view.plan = plan view.who = upper view.actions.plan = [] if (plan.length === 0 && upper === NOBODY) view.prompt = "Plan: Designate Lieutenants and build a Plan." else if (plan.length === 0 && upper !== NOBODY) view.prompt = `Plan: Select Lower Lord for ${lord_name[upper]}.` else if (plan.length === max_plan_length()) view.prompt = "Plan: All done." else view.prompt = "Plan: Build a Plan." if (upper === NOBODY) { if (plan.length < max_plan_length()) { view.actions.end_plan = 0 if (count_cards_in_plan(plan, NOBODY) < 3) gen_action_plan(NOBODY) for (let lord = first; lord <= last; ++lord) { if (is_lord_on_map(lord) && count_cards_in_plan(plan, lord) < 3) gen_action_plan(lord) } } else { view.actions.end_plan = 1 } } else { view.actions.end_plan = 0 } // Designate Lieutenants only if no plan started. if (plan.length === 0) { if (upper !== NOBODY) gen_action_lord(upper) for (let lord = first; lord <= last; ++lord) { if (is_marshal(lord) || is_lord_besieged(lord)) continue if (is_upper_lord(lord) || is_lower_lord(lord)) continue if (upper === NOBODY) { if (plan_can_make_lieutenant(plan, lord, first, last)) gen_action_lord(lord) } else { if (get_lord_locale(upper) === get_lord_locale(lord)) gen_action_lord(lord) } } } if (plan.length > 0 || plan_has_lieutenant(first, last)) view.actions.undo = 1 else view.actions.undo = 0 }, lord(lord, current) { let upper if (current === P1) upper = plan_selected_lieutenant(first_p1_lord, last_p1_lord) else upper = plan_selected_lieutenant(first_p2_lord, last_p2_lord) if (lord === upper) remove_lieutenant(upper) else if (upper === NOBODY) add_lieutenant(lord) else set_lower_lord(upper, lord) }, plan(lord, current) { if (current === P1) game.plan1.push(lord) else game.plan2.push(lord) }, undo(_, current) { if (current === P1) { if (game.plan1.length > 0) { game.plan1.pop() } else { for (let lord = first_p1_lord; lord <= last_p1_lord; ++lord) if (is_upper_lord(lord)) remove_lieutenant(lord) } } else { if (game.plan2.length > 0) { game.plan2.pop() } else { for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord) if (is_upper_lord(lord)) remove_lieutenant(lord) } } }, end_plan(_, current) { if (game.active === BOTH) { if (current === P1) set_active(P2) else set_active(P1) } else { end_campaign_plan() } }, } function end_campaign_plan() { if (game.pieces.lieutenants.length > 0) { log("Lieutenants") for (let i = 0; i < game.pieces.lieutenants.length; i += 2) { let upper = game.pieces.lieutenants[i] let lower = game.pieces.lieutenants[i + 1] logi(`L${upper} over L${lower}`) } } set_active(P1) goto_command_activation() } // === CAMPAIGN: COMMAND ACTIVATION === function goto_command_activation() { if (game.plan2.length === 0) { game.command = NOBODY goto_end_campaign() return } if (check_campaign_victory()) return if (game.plan2.length > game.plan1.length) { set_active(P2) game.command = game.plan2.shift() } else { set_active(P1) game.command = game.plan1.shift() } if (game.command === NOBODY) { log_h2("Pass") goto_command_activation() } else if (is_lower_lord(game.command)) { log_h2(`L${game.command} - Pass`) goto_command_activation() } else if (!is_lord_on_map(game.command)) { log_h2(`L${game.command} - Pass`) goto_command_activation() } else { log_h2(`L${game.command} at %${get_lord_locale(game.command)}`) goto_command() } } // === CAMPAIGN: ACTIONS === function set_active_command() { if (game.command >= first_p1_lord && game.command <= last_p1_lord) set_active(P1) else set_active(P2) } function is_active_command() { if (game.command >= first_p1_lord && game.command <= last_p1_lord) return game.active === P1 else return game.active === P2 } function is_first_action() { return game.flags.first_action } function is_first_march() { return game.flags.first_march } function goto_command() { game.actions = data.lords[game.command].command game.flags.first_action = 1 game.flags.first_march = 1 // 4.1.3 Lieutenants MUST take lower lord game.group = [ game.command ] let lower = get_lower_lord(game.command) if (lower !== NOBODY) set_add(game.group, lower) resume_command() update_supply_possible() } function resume_command() { game.state = "command" } function spend_action(cost) { game.flags.first_action = 0 game.actions -= cost } function spend_march_action(cost) { game.flags.first_action = 0 game.flags.first_march = 0 game.actions -= cost } function spend_all_actions() { game.flags.first_action = 0 game.flags.first_march = 0 game.actions = 0 } function end_command() { log_br() game.group = 0 game.flags.first_action = 0 game.flags.first_march = 0 game.flags.famine = 0 // NOTE: Feed currently acting side first for expedience. set_active_command() goto_feed() } function this_lord_has_russian_druzhina() { if (game.active === RUSSIANS) if (lord_has_capability(game.command, AOW_RUSSIAN_DRUZHINA)) return get_lord_forces(game.command, KNIGHTS) > 0 return false } function this_lord_has_house_of_suzdal() { if (game.active === RUSSIANS) if (lord_has_capability(game.command, AOW_RUSSIAN_HOUSE_OF_SUZDAL)) return is_lord_on_map(LORD_ALEKSANDR) && is_lord_on_map(LORD_ANDREY) return false } states.command = { inactive: "Command", prompt() { if (game.actions === 0) view.prompt = `Command: ${lord_name[game.command]} has no more actions.` else if (game.actions === 1) view.prompt = `Command: ${lord_name[game.command]} has ${game.actions} action.` else view.prompt = `Command: ${lord_name[game.command]} has ${game.actions} actions.` view.group = game.group let here = get_lord_locale(game.command) prompt_held_event() // 4.3.2 Marshals MAY take other lords if (is_marshal(game.command)) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (lord !== game.command && !is_lower_lord(lord)) if (get_lord_locale(lord) === here) gen_action_lord(lord) } if (game.actions > 0) view.actions.pass = 1 else view.actions.end_command = 1 prompt_march() if (can_action_supply()) view.actions.supply = 1 if (can_action_siege()) view.actions.siege = 1 if (can_action_forage()) view.actions.forage = 1 if (can_action_ravage()) view.actions.ravage = 1 if (can_action_tax()) view.actions.tax = 1 if (can_action_sail()) view.actions.sail = 1 }, pass() { push_undo() log("Passed.") spend_all_actions() }, end_command() { push_undo() end_command() }, forage: goto_forage, ravage: goto_ravage, supply: goto_supply, tax: goto_tax, sail: goto_sail, locale: goto_march, lord(lord) { set_toggle(game.group, lord) if (is_upper_lord(lord)) set_toggle(game.group, get_lower_lord(lord)) }, card: action_held_event, } // === ACTION: MARCH === function format_group_move() { if (game.group.length > 1) { let list = [] for (let lord of game.group) if (lord !== game.command) list.push("L" + lord) return " with " + list.join(" and ") } return "" } function group_has_teutonic_converts() { if (game.active === TEUTONS) { if (is_first_march()) if (group_has_capability(AOW_TEUTONIC_CONVERTS)) if (count_group_forces(LIGHT_HORSE) > 0) return true } return false } function prompt_march() { if (game.actions > 0 || group_has_teutonic_converts()) { let here = get_lord_locale(game.command) for (let to of data.locales[here].adjacent) gen_action_locale(to) } } function goto_march(to) { push_undo() let from = get_lord_locale(game.command) let ways = list_ways(from, to) if (ways.length > 2) { game.march = { from, to, approach: -1, avoid: -1 } game.state = "march_way" } else { game.march = { from, to, approach: ways[1], avoid: -1 } march_with_group_1() } } states.march_way = { inactive: "March", prompt() { view.prompt = `March: Select Way.` view.group = game.group let from = game.march.from let to = game.march.to let ways = list_ways(from, to) for (let i = 1; i < ways.length; ++i) gen_action_way(ways[i]) }, way(way) { game.march.approach = way march_with_group_1() }, } function march_with_group_1() { let way = game.march.approach let type = data.ways[way].type let transport = count_group_transport(type) let prov = count_group_assets(PROV) if (group_has_teutonic_converts() && prov <= transport * 2) return march_with_group_2() if (prov > transport) game.state = "march_laden" else march_with_group_2() } states.march_laden = { inactive: "March", prompt() { let to = game.march.to let way = game.march.approach let transport = count_group_transport(data.ways[way].type) let prov = count_group_assets(PROV) view.group = game.group if (prov <= transport * 2 && group_has_teutonic_converts()) view.prompt = `March: Converts.` else if (prov > transport * 2 || (prov > transport && view.actions < 2)) view.prompt = `March: Hindered with ${prov} Provender, and ${transport} Transport.` else if (prov > transport) view.prompt = `March: Laden with ${prov} Provender, and ${transport} Transport.` else view.prompt = `March: Unladen.` if (group_has_teutonic_converts()) { if (prov <= transport * 2) { view.actions.march = 1 gen_action_locale(to) } else { for (let lord of game.group) { if (get_lord_assets(lord, PROV) > 0) { view.prompt += " Discard Provender." gen_action_prov(lord) } } } return } if (prov <= transport * 2) { if (prov > transport) { if (game.actions >= 2) { view.actions.march = 1 // other button? gen_action_laden_march(to) } else { view.prompt += " 1 action left." } } else { view.actions.march = 1 gen_action_locale(to) } } if (prov > transport) { for (let lord of game.group) { if (prov > transport) { if (get_lord_assets(lord, PROV) > 0) { view.prompt += " Discard Provender." gen_action_prov(lord) } } } } }, prov: drop_prov, march: march_with_group_2, locale: march_with_group_2, laden_march: march_with_group_2, } function march_with_group_2() { let from = get_lord_locale(game.command) let way = game.march.approach let to = game.march.to let transport = count_group_transport(data.ways[way].type) let prov = count_group_assets(PROV) let laden = prov > transport if (group_has_teutonic_converts()) { logcap(AOW_TEUTONIC_CONVERTS) spend_march_action(0) } else if (laden) spend_march_action(2) else spend_march_action(1) if (data.ways[way].name) log(`Marched to %${to} via W${way}${format_group_move()}.`) else log(`Marched to %${to}${format_group_move()}.`) for (let lord of game.group) { set_lord_locale(lord, to) set_lord_moved(lord, 1) } if (has_unbesieged_enemy_lord(to)) { goto_confirm_approach() } else { march_with_group_3() } } function march_with_group_3() { let here = get_lord_locale(game.command) // Disbanded in battle! if (here === NOWHERE) { game.march = 0 spend_all_actions() resume_command() update_supply_possible() return } if (is_unbesieged_enemy_stronghold(here)) { add_siege_marker(here) spend_all_actions() // ENCAMP } if (is_trade_route(here)) conquer_trade_route(here) game.march = 0 resume_command() update_supply_possible() } function goto_confirm_approach() { if (game.skip_confirm_approach) { goto_avoid_battle() return } game.state = "confirm_approach" } states.confirm_approach = { inactive: "March", prompt() { view.prompt = `March: Confirm Approach to enemy Lord.` view.group = game.group view.actions.approach = 1 }, approach() { goto_avoid_battle() } } // === ACTION: MARCH - AVOID BATTLE === function count_besieged_lords(loc) { let n = 0 for (let lord = first_lord; lord <= last_lord; ++lord) if (get_lord_locale(lord) === loc && is_lord_besieged(lord)) ++n return n } function stronghold_strength(loc) { if (has_castle_marker(loc)) return 2 return data.locales[loc].stronghold } function stronghold_capacity(loc) { return stronghold_strength(loc) - count_besieged_lords(loc) } function spoil_prov(lord) { add_lord_assets(lord, PROV, -1) add_spoils(PROV, 1) } function can_any_avoid_battle() { let here = game.march.to for (let [to, way] of data.locales[here].ways) if (can_avoid_battle(to, way)) return true return false } function can_avoid_battle(to, way) { if (way === game.march.approach) return false if (has_unbesieged_enemy_lord(to)) return false if (is_unbesieged_enemy_stronghold(to)) return false return true } function goto_avoid_battle() { clear_undo() set_active_enemy() if (can_any_avoid_battle()) { // TODO: pre-select lone lord? game.march.group = game.group // save group game.state = "avoid_battle" game.spoils = 0 resume_avoid_battle() } else { goto_march_withdraw() } } function resume_avoid_battle() { let here = game.march.to if (has_unbesieged_friendly_lord(here)) { game.group = [] game.state = "avoid_battle" } else { end_avoid_battle() } } states.avoid_battle = { inactive: "Avoid Battle", prompt() { view.prompt = "March: Select Lords and destination to Avoid Battle." view.group = game.group let here = game.march.to for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here && !is_lower_lord(lord)) gen_action_lord(lord) if (game.group.length > 0) { for (let [to, way] of data.locales[here].ways) { if (can_avoid_battle(to, way)) gen_action_locale(to) } } view.actions.end_avoid_battle = 1 }, lord(lord) { set_toggle(game.group, lord) if (is_upper_lord(lord)) set_toggle(game.group, get_lower_lord(lord)) }, locale(to) { push_undo() // Save Assets and Lords in case Ambush cancels Avoid Battle. if (!game.march.ambush) { if (could_enemy_play_ambush()) { // TODO: ambush object... game.march.ambush = { lords: [], assets: game.pieces.assets.slice(), conquered: game.pieces.conquered.slice(), } } } let from = get_lord_locale(game.command) let ways = list_ways(from, to) if (ways.length > 2) { game.march.avoid_to = to game.state = "avoid_battle_way" } else { game.march.avoid_to = to game.march.avoid_way = ways[1] avoid_battle_1() } }, end_avoid_battle() { push_undo() end_avoid_battle() }, } states.avoid_battle_way = { inactive: "Avoid Battle", prompt() { view.prompt = `Avoid Battle: Select Way to destination.` view.group = game.group let from = game.march.to let to = game.march.avoid_to let ways = list_ways(from, to) for (let i = 1; i < ways.length; ++i) if (can_avoid_battle(to, ways[i])) gen_action_way(ways[i]) }, way(way) { game.march.avoid_way = way avoid_battle_1() }, } function avoid_battle_1() { let way = game.march.avoid_way let transport = count_group_transport(data.ways[way].type) let prov = count_group_assets(PROV) if (prov > transport) game.state = "avoid_battle_laden" else avoid_battle_2() } states.avoid_battle_laden = { inactive: "Avoid Battle", prompt() { let to = game.march.avoid_to let way = game.march.avoid_way let transport = count_group_transport(data.ways[way].type) let prov = count_group_assets(PROV) if (prov > transport) view.prompt = `Avoid Battle: Hindered with ${prov} Provender and ${transport} Transport.` else view.prompt = `Avoid Battle: Unladen.` view.group = game.group if (prov > transport) { view.prompt += " Discard Provender." for (let lord of game.group) { if (get_lord_assets(lord, PROV) > 0) gen_action_prov(lord) } } else { gen_action_locale(to) view.actions.avoid = 1 } }, prov(lord) { push_undo() spoil_prov(lord) }, locale(_) { avoid_battle_2() }, avoid() { avoid_battle_2() }, } function avoid_battle_2() { let to = game.march.avoid_to for (let lord of game.group) { log(`L${lord} Avoided Battle to %${to}.`) if (game.march.ambush) set_add(game.march.ambush.lords, lord) set_lord_locale(lord, to) set_lord_moved(lord, 1) } if (is_trade_route(to)) conquer_trade_route(to) game.march.avoid_to = 0 game.march.avoid_way = 0 resume_avoid_battle() } function end_avoid_battle() { game.group = game.march.group // restore group game.march.group = 0 goto_march_withdraw() } // === ACTION: MARCH - WITHDRAW === function can_withdraw(here, n) { if (is_unbesieged_friendly_stronghold(here)) if (stronghold_capacity(here) >= n) return true return false } function goto_march_withdraw() { let here = game.march.to if (has_unbesieged_friendly_lord(here) && can_withdraw(here, 1)) { game.state = "march_withdraw" } else { end_march_withdraw() } } states.march_withdraw = { inactive: "Withdraw", prompt() { view.prompt = "March: Select Lords to Withdraw into Stronghold." let here = get_lord_locale(game.command) let capacity = stronghold_capacity(here) if (capacity >= 1) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === here && !is_lower_lord(lord) && is_lord_unbesieged(lord)) { if (is_upper_lord(lord)) { if (capacity >= 2) gen_action_lord(lord) } else { gen_action_lord(lord) } } } } view.actions.end_withdraw = 1 }, lord(lord) { push_undo() let lower = get_lower_lord(lord) log(`L${lord} Withdrew.`) set_lord_besieged(lord, 1) if (lower !== NOBODY) { log(`L${lower} Withdrew.`) set_lord_besieged(lower, 1) } }, end_withdraw() { end_march_withdraw() }, } function end_march_withdraw() { clear_undo() set_active_enemy() goto_march_ambush() } // === ACTION: MARCH - AMBUSH === function could_enemy_play_ambush() { if (game.active === TEUTONS) return could_play_card(EVENT_RUSSIAN_AMBUSH) else return could_play_card(EVENT_TEUTONIC_AMBUSH) } function goto_march_ambush() { if (game.march.ambush && game.march.ambush.lords.length > 0) game.state = "march_ambush" else goto_spoils_after_avoid_battle() } states.march_ambush = { inactive: "Ambush", prompt() { view.prompt = "Avoid Battle: You may play Ambush if you have it." if (has_card_in_hand(EVENT_TEUTONIC_AMBUSH)) gen_action_card(EVENT_TEUTONIC_AMBUSH) if (has_card_in_hand(EVENT_RUSSIAN_AMBUSH)) gen_action_card(EVENT_RUSSIAN_AMBUSH) view.actions.pass = 1 }, card(c) { play_held_event(c) // Restore assets and spoils and withdrawn lords game.pieces.assets = game.march.ambush.assets game.pieces.conquered = game.march.ambush.conquered game.spoils = 0 // Restore lords who avoided battle for (let lord of game.march.ambush.lords) { set_lord_locale(lord, game.march.to) set_lord_moved(lord, 0) } set_active_enemy() game.march.ambush = 0 goto_march_withdraw() }, pass() { game.march.ambush = 0 goto_spoils_after_avoid_battle() }, } // === ACTION: MARCH - DIVIDE SPOILS AFTER AVOID BATTLE === function list_spoils() { let list = [] for (let type = 0; type < 7; ++type) { let n = get_spoils(type) if (n > 0) list.push(`${n} ${ASSET_TYPE_NAME[type]}`) } if (list.length > 0) return list.join(", ") return "nothing" } function prompt_spoils() { if (get_spoils(PROV) > 0) view.actions.take_prov = 1 if (get_spoils(COIN) > 0) view.actions.take_coin = 1 if (get_spoils(CART) > 0) view.actions.take_cart = 1 } function take_spoils(type) { push_undo_without_who() add_lord_assets(game.who, type, 1) add_spoils(type, -1) if (!has_any_spoils()) game.who = NOBODY } function take_spoils_prov() { take_spoils(PROV) } function take_spoils_coin() { take_spoils(COIN) } function take_spoils_cart() { take_spoils(CART) } function goto_spoils_after_avoid_battle() { if (has_any_spoils()) { game.state = "spoils_after_avoid_battle" if (game.group.length === 1) game.who = game.group[0] } else { goto_battle() } } states.spoils_after_avoid_battle = { inactive: "Spoils", prompt() { if (has_any_spoils()) { view.prompt = "Spoils: Divide " + list_spoils() + "." // only moving lords get to divide the spoils for (let lord of game.group) prompt_select_lord(lord) if (game.who !== NOBODY) prompt_spoils() } else { view.prompt = "Spoils: All done." view.actions.end_spoils = 1 } }, lord: action_select_lord, take_prov: take_spoils_prov, end_spoils() { clear_undo() game.spoils = 0 game.who = NOBODY goto_battle() }, } // === ACTION: SUPPLY (SEARCHING) === let _supply_stop = new Array(last_locale+1) let _supply_reached = new Array(last_locale+1) let _supply_seen = new Array(last_locale+1).fill(0) let _supply_cost = new Array(last_locale+1) let _supply_carts = new Array(last_locale+1) function is_supply_forbidden(here) { if (has_unbesieged_enemy_lord(here)) return true if (is_unbesieged_enemy_stronghold(here)) return true if (is_friendly_territory(here) && has_conquered_marker(here)) return true return false } function init_supply_forbidden() { for (let here = 0; here <= last_locale; ++here) { if (is_supply_forbidden(here)) _supply_stop[here] = 1 else _supply_stop[here] = 0 } } function init_supply() { let season = current_season() let here = get_lord_locale(game.command) let carts = 0 let ships = 0 let available = 2 if (season === SUMMER) { carts = get_shared_assets(here, CART) } if (season === SUMMER || season === RASPUTITSA) { ships = count_shared_ships() } if (ships > 2) ships = 2 if (is_famine_in_play()) available = game.flags.famine ? 0 : 1 let seats = [] if (available > 0) { for_each_seat(game.command, seat => { if (!is_supply_forbidden(seat)) seats.push(seat) }, true) available = Math.min(seats.length, available) } let seaports = [] if (ships > 0) { if (game.active === TEUTONS) for (let port of data.seaports) if (!is_supply_forbidden(port)) seaports.push(port) if (game.active === RUSSIANS) if (!is_supply_forbidden(LOC_NOVGOROD)) seaports.push(LOC_NOVGOROD) } if (seaports.length === 0) ships = 0 game.supply = { seats, seaports, available, carts, ships } } function search_supply(start, carts, exit) { if (_supply_stop[start]) return 0 _supply_reached[start] = 1 _supply_cost[start] = 0 if (exit && set_has(exit, start)) return 1 if (carts === 0) return 0 let queue = [ start ] while (queue.length > 0) { let item = queue.shift() let here = item & 63 let used = item >> 6 if (used + 1 <= carts) { for (let next of data.locales[here].adjacent) { if (!_supply_reached[next] && !_supply_stop[next]) { if (exit && set_has(exit, next)) return 1 _supply_reached[next] = 1 _supply_cost[next] = used + 1 if (used + 1 < carts) queue.push(next | ((used + 1) << 6)) } } } } return 0 } // === ACTION: SUPPLY === function update_supply_possible() { if (game.actions < 1) { game.supply = 0 return } update_supply_possible_pass() } function update_supply_possible_pass() { init_supply() init_supply_forbidden() _supply_reached.fill(0) let sources = [] for (let loc of game.supply.seats) set_add(sources, loc) for (let loc of game.supply.seaports) set_add(sources, loc) game.supply = search_supply(get_lord_locale(game.command), game.supply.carts, sources) } function search_supply_cost() { init_supply_forbidden() _supply_reached.fill(0) search_supply(get_lord_locale(game.command), game.supply.carts, null) } function can_action_supply() { if (game.actions < 1) return false return !!game.supply } function can_supply() { if (game.supply.available > 0 && game.supply.seats.length > 0) return true if (game.supply.ships > 0 && game.supply.seaports.length > 0) return true return false } function goto_supply() { push_undo() if (is_famine_in_play() && !game.flags.famine) { if (game.active === TEUTONS) logevent(EVENT_RUSSIAN_FAMINE) else logevent(EVENT_TEUTONIC_FAMINE) } log(`Supplied`) init_supply() resume_supply() game.state = "supply_source" } function resume_supply() { if (game.supply.available + game.supply.ships === 0) { game.supply.seats = [] game.supply.seaports = [] } else { search_supply_cost() game.supply.seats = game.supply.seats.filter(loc => _supply_reached[loc]) game.supply.seaports = game.supply.seaports.filter(loc => _supply_reached[loc]) } if (can_supply()) game.state = "supply_source" else end_supply() } states.supply_source = { inactive: "Supply", prompt() { if (!can_supply()) { view.prompt = "Supply: No valid Supply Sources." return } view.prompt = "Supply: Select Supply Source and Route." let list = [] if (game.supply.carts > 0) list.push(`${game.supply.carts} Cart`) if (game.supply.ships > 0) list.push(`${game.supply.ships} Ship`) if (list.length > 0) view.prompt += " " + list.join(", ") + "." if (game.supply.available > 0) for (let source of game.supply.seats) gen_action_locale(source) if (game.supply.ships > 0) for (let source of game.supply.seaports) gen_action_locale(source) view.actions.end_supply = 1 }, locale(source) { if (game.supply.available > 0 && game.supply.seats.includes(source)) { array_remove_item(game.supply.seats, source) let cap = used_seat_capability(game.command, source, game.supply.seats) if (cap >= 0) logi(`Seat at %${source} (C${cap})`) else logi(`Seat at %${source}`) game.supply.available-- if (is_famine_in_play()) game.flags.famine = 1 } else { logi(`Seaport at %${source}`) game.supply.ships-- } add_lord_assets(game.command, PROV, 1) spend_supply_transport(source) }, end_supply: end_supply, } function end_supply() { spend_action(1) resume_command() game.supply = 1 // supply is possible! } function spend_supply_transport(source) { if (source === get_lord_locale(game.command)) { resume_supply() return } search_supply_cost() game.supply.carts -= _supply_cost[source] resume_supply() } states.supply_path = { inactive: "Supply", prompt() { view.prompt = "Supply: Trace Route to Supply Source." view.supply = [ game.supply.here, game.supply.end ] if (game.supply.carts > 0) view.prompt += ` ${game.supply.carts} cart` for (let i = 0; i < game.supply.path.length; i += 2) { let wayloc = game.supply.path[i] gen_action_locale(wayloc >> 8) } }, locale(next) { let useloc = -1 let useway = -1 let twoway = false for (let i = 0; i < game.supply.path.length; i += 2) { let wayloc = game.supply.path[i] let way = wayloc & 255 let loc = wayloc >> 8 if (loc === next) { if (useloc < 0) { useloc = loc useway = way } else { twoway = true } } } if (twoway) { game.state = "supply_path_way" game.supply.next = next } else { walk_supply_path_way(next, useway) } }, } function walk_supply_path_way(next, way) { let type = data.ways[way].type game.supply.carts-- game.supply.here = next game.supply.path = map_get(game.supply.path, (next << 8) | way) if (game.supply.path === 0) resume_supply() else // Auto-pick path if only one choice. if (AUTOWALK && game.supply.path.length === 2) walk_supply_path_way(game.supply.path[0] >> 8, game.supply.path[0] & 255) } states.supply_path_way = { inactive: "Supply", prompt() { view.prompt = "Supply: Trace path to supply source." view.supply = [ game.supply.here, game.supply.end ] if (game.supply.carts > 0) view.prompt += ` ${game.supply.carts} cart` for (let i = 0; i < game.supply.path.length; i += 2) { let wayloc = game.supply.path[i] let way = wayloc & 255 let loc = wayloc >> 8 if (loc === game.supply.next) gen_action_way(way) } }, way(way) { game.state = "supply_path" walk_supply_path_way(game.supply.next, way) }, } // === ACTION: FORAGE === function can_action_forage() { if (game.actions < 1) return false if (is_famine_in_play()) return false let here = get_lord_locale(game.command) if (has_ravaged_marker(here)) return false if (is_summer()) return true if (is_friendly_stronghold_locale(here)) // FIXME: simpler check? return true return false } function goto_forage() { push_undo() let here = get_lord_locale(game.command) log(`Foraged at %${here}`) add_lord_assets(game.command, PROV, 1) spend_action(1) resume_command() } // === ACTION: RAVAGE === function has_adjacent_unbesieged_enemy_lord(loc) { for (let next of data.locales[loc].adjacent) if (has_unbesieged_enemy_lord(next)) return true return false } function can_ravage_locale(loc) { if (!is_enemy_territory(loc)) return false if (has_conquered_marker(loc)) return false if (has_ravaged_marker(loc)) return false if (is_friendly_locale(loc)) // faster check? return false if (has_adjacent_unbesieged_enemy_lord(loc)) return game.actions >= 2 else return game.actions >= 1 } function can_action_ravage() { if (game.actions < 1) return false let here = get_lord_locale(game.command) if (can_ravage_locale(here)) return true if (this_lord_has_teutonic_raiders()) { for (let there of data.locales[here].adjacent_by_trackway) // XXX has_enemy_lord redundant with is_friendly_locale in can_ravage_locale if (can_ravage_locale(there) && !has_enemy_lord(there)) return true } if (this_lord_has_russian_raiders()) { for (let there of data.locales[here].adjacent) // XXX has_enemy_lord redundant with is_friendly_locale in can_ravage_locale if (can_ravage_locale(there) && !has_enemy_lord(there)) return true } return false } function goto_ravage() { push_undo() if (this_lord_has_teutonic_raiders() || this_lord_has_russian_raiders()) { game.state = "ravage" } else { let here = get_lord_locale(game.command) ravage_location(here, here) } } states.ravage = { inactive: "Ravage", prompt() { view.prompt = `Ravage: Select enemy territory to Ravage.` let here = get_lord_locale(game.command) if (can_ravage_locale(here)) gen_action_locale(here) if (this_lord_has_teutonic_raiders()) { for (let there of data.locales[here].adjacent_by_trackway) if (can_ravage_locale(there) && !has_enemy_lord(there)) gen_action_locale(there) } if (this_lord_has_russian_raiders()) { for (let there of data.locales[here].adjacent) if (can_ravage_locale(there) && !has_enemy_lord(there)) gen_action_locale(there) } }, locale(there) { let here = get_lord_locale(game.command) ravage_location(here, there) }, } function ravage_location(here, there) { if (here !== there) { if (is_teutonic_lord(game.command)) log(`Ravaged %${there} (C${AOW_TEUTONIC_RAIDERS}).`) else log(`Ravaged %${there} (C${which_lord_capability(game.command, AOW_RUSSIAN_RAIDERS)}).`) } else { log(`Ravaged %${there}.`) } add_ravaged_marker(there) add_lord_assets(game.command, PROV, 1) if (here !== there && game.active === TEUTONS) game.flags.teutonic_raiders = 1 if (has_adjacent_unbesieged_enemy_lord(there)) spend_action(2) else spend_action(1) resume_command() } // === ACTION: TAX === function restore_mustered_forces(lord) { muster_lord_forces(lord) for (let v of data.lords[lord].vassals) if (is_vassal_mustered(v)) muster_vassal_forces(lord, v) } function can_action_tax() { // Must use whole action if (!is_first_action()) return false // Must have space left to hold Coin if (get_lord_assets(game.command, COIN) >= 8) return false // Must be at own seat return is_lord_at_seat(game.command) } function goto_tax() { push_undo() let here = get_lord_locale(game.command) log(`Taxed %${here}.`) add_lord_assets(game.command, COIN, 1) spend_all_actions() resume_command() if (lord_has_capability(game.command, AOW_RUSSIAN_VELIKY_KNYAZ)) { logcap(AOW_RUSSIAN_VELIKY_KNYAZ) restore_mustered_forces(game.command) push_state("veliky_knyaz") game.who = game.command game.count = 2 } } states.veliky_knyaz = states.muster_lord_transport // === ACTION: SAIL === function drop_prov(lord) { add_lord_assets(lord, PROV, -1) } function has_enough_available_ships_for_horses() { let ships = count_group_ships() let horses = count_group_horses() let needed_ships = horses if (game.active === RUSSIANS) needed_ships = horses * 2 return needed_ships <= ships } function can_action_sail() { // Must use whole action if (!is_first_action()) return false // at a seaport let here = get_lord_locale(game.command) if (!is_seaport(here)) return false // during Rasputitsa or Summer if (is_winter()) return false // with enough ships to carry all the horses if (!has_enough_available_ships_for_horses()) return false // and a valid destination for (let to of data.seaports) if (to !== here && !has_enemy_lord(to)) return true return false } function goto_sail() { push_undo() game.state = "sail" } states.sail = { inactive: "Sail", prompt() { view.group = game.group let here = get_lord_locale(game.command) let ships = count_group_ships() let horses = count_group_horses() let prov = count_group_assets(PROV) let overflow = 0 if (game.active === TEUTONS) overflow = (horses + prov) - ships if (game.active === RUSSIANS) overflow = (horses * 2 + prov) - ships if (overflow > 0) { view.prompt = `Sailing with ${ships} Ships and ${horses} Horses. Discard Loot or Provender.` // TODO: stricter greed! if (prov > 0) { for (let lord of game.group) { if (get_lord_assets(lord, PROV) > 0) gen_action_prov(lord) } } } else { view.prompt = `Sail: Select a destination Seaport.` for (let to of data.seaports) { if (to === here) continue if (!has_enemy_lord(to)) gen_action_locale(to) } } }, prov: drop_prov, locale(to) { push_undo() log(`Sailed to %${to}${format_group_move()}.`) let from = get_lord_locale(game.command) for (let lord of game.group) { set_lord_locale(lord, to) set_lord_moved(lord, 1) } if (is_trade_route(to)) conquer_trade_route(to) spend_all_actions() resume_command() update_supply_possible() }, } // === BATTLE === function set_active_attacker() { set_active(game.battle.attacker) } function set_active_defender() { if (game.battle.attacker === P1) set_active(P2) else set_active(P1) } function goto_battle() { if (has_unbesieged_enemy_lord(game.march.to)) start_battle() else march_with_group_3() } function init_battle(here) { game.battle = { where: here, round: 1, step: 0, relief: 0, attacker: game.active, ambush: 0, conceded: 0, loser: 0, fought: 0, // flag all lords who participated array: [ -1, game.command, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], garrison: 0, reserves: [], retreated: 0, rearguard: 0, strikers: 0, warrior_monks: 0, hits: 0, xhits: 0, fc: -1, rc: -1, } } function start_battle() { let here = get_lord_locale(game.command) log_h3(`Battle at %${here}`) init_battle(here, 0, 0) // All attacking lords to reserve for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === here && !is_lord_besieged(lord)) { set_lord_fought(lord) if (lord !== game.command) set_add(game.battle.reserves, lord) } } // Array attacking lords if fewer than 3. if (game.battle.reserves.length === 2) game.battle.array[A3] = game.battle.reserves.pop() if (game.battle.reserves.length === 1) game.battle.array[A1] = game.battle.reserves.pop() // All defending lords to reserve for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) { if (get_lord_locale(lord) === here && !is_lord_besieged(lord)) { set_lord_fought(lord) set_add(game.battle.reserves, lord) } } goto_relief_sally() } function init_garrison(knights, men_at_arms) { game.battle.garrison = { knights, men_at_arms } } // === BATTLE: BATTLE ARRAY === // 0) Defender decides to stand for Battle, not Avoid. // 1) Attacker decides which Lords will relief sally, if any. // 2) Attacker positions front A. // 3) Defender positions front D. // 4) Attacker positions SA. // 5) Defender positions reaguard RG. function has_friendly_reserves() { for (let lord of game.battle.reserves) if (is_friendly_lord(lord)) return true return false } function has_friendly_attacking_reserves() { for (let lord of game.battle.reserves) if (is_friendly_lord(lord) && (game.battle.sally || is_lord_unbesieged(lord))) return true return false } function has_friendly_sallying_reserves() { for (let lord of game.battle.reserves) if (is_friendly_lord(lord) && is_lord_besieged(lord)) return true return false } function count_friendly_reserves() { let n = 0 for (let lord of game.battle.reserves) if (is_friendly_lord(lord)) ++n return n } function pop_first_reserve() { for (let lord of game.battle.reserves) { if (is_friendly_lord(lord)) { set_delete(game.battle.reserves, lord) return lord } } return NOBODY } function prompt_array_place_opposed(X1, X2, X3, Y1, Y3) { let array = game.battle.array if (array[X2] === NOBODY) { gen_action_array(X2) } else if (array[Y1] !== NOBODY && array[Y3] === NOBODY && array[X1] === NOBODY) { gen_action_array(X1) } else if (array[Y1] === NOBODY && array[Y3] !== NOBODY && array[X3] === NOBODY) { gen_action_array(X3) } else { if (array[X1] === NOBODY) gen_action_array(X1) if (array[X3] === NOBODY) gen_action_array(X3) } } function action_array_place(pos) { push_undo_without_who() game.battle.array[pos] = game.who set_delete(game.battle.reserves, game.who) game.who = NOBODY } function goto_array_attacker() { clear_undo() set_active_attacker() game.state = "array_attacker" game.who = NOBODY if (!has_friendly_attacking_reserves()) end_array_attacker() } function goto_array_defender() { clear_undo() set_active_defender() game.state = "array_defender" game.who = NOBODY let n = count_friendly_reserves() if (n === 1) { game.battle.array[D2] = pop_first_reserve() end_array_defender() } if (n === 0) end_array_defender() } function goto_array_sally() { clear_undo() set_active_attacker() game.state = "array_sally" game.who = NOBODY if (!has_friendly_sallying_reserves()) end_array_sally() } function goto_array_rearguard() { clear_undo() set_active_defender() game.state = "array_rearguard" game.who = NOBODY if (!has_friendly_reserves() || empty(SA2)) end_array_rearguard() } // NOTE: The order here can be easily change to attacker/sally/defender/rearguard if desired. function end_array_attacker() { goto_array_defender() } function end_array_defender() { goto_array_sally() } function end_array_sally() { goto_array_rearguard() } function end_array_rearguard() { goto_attacker_events() } states.array_attacker = { inactive: "Array Attacking Lords", prompt() { view.prompt = "Battle Array: Position your Attacking Lords." let array = game.battle.array let done = true if (array[A1] === NOBODY || array[A2] === NOBODY || array[A3] === NOBODY) { for (let lord of game.battle.reserves) { if (lord !== game.who && is_friendly_lord(lord)) { if (game.battle.sally || is_lord_unbesieged(lord)) { gen_action_lord(lord) done = false } } } } if (game.who === NOBODY && done) view.actions.end_array = 1 if (game.who !== NOBODY) { // A2 is already filled by command lord! if (array[A1] === NOBODY) gen_action_array(A1) if (array[A3] === NOBODY) gen_action_array(A3) } }, array: action_array_place, lord: action_select_lord, end_array: end_array_attacker, } states.array_defender = { inactive: "Array Defending Lords", prompt() { view.prompt = "Battle Array: Position your Defending Lords." let array = game.battle.array let done = true if (array[D1] === NOBODY || array[D2] === NOBODY || array[D3] === NOBODY) { for (let lord of game.battle.reserves) { if (lord !== game.who && is_friendly_lord(lord)) { gen_action_lord(lord) done = false } } } if (done && game.who === NOBODY) view.actions.end_array = 1 if (game.who !== NOBODY) prompt_array_place_opposed(D1, D2, D3, A1, A3) }, array: action_array_place, lord: action_select_lord, end_array: end_array_defender, } // === BATTLE: EVENTS === function goto_attacker_events() { clear_undo() set_active_attacker() log_br() if (can_play_battle_events()) game.state = "attacker_events" else end_attacker_events() } function end_attacker_events() { goto_defender_events() } function goto_defender_events() { set_active_defender() log_br() if (can_play_battle_events()) game.state = "defender_events" else end_defender_events() } function end_defender_events() { goto_battle_rounds() } function resume_battle_events() { game.what = -1 if (is_attacker()) goto_attacker_events() else goto_defender_events() } function could_play_card(c) { if (set_has(game.capabilities, c)) return false if (!game.hidden) { // TODO: check capabilities on lords revealed in battle if hidden if (game.pieces.capabilities.includes(c)) return false } if (set_has(game.events, c)) return false if (is_p1_card(c)) return game.hand1.length > 0 if (is_p2_card(c)) return game.hand2.length > 0 return true } function has_lords_in_battle() { for (let p = 0; p < 12; ++p) if (is_friendly_lord(game.battle.array[p])) return true return has_friendly_reserves() } function can_play_battle_events() { if (game.active === TEUTONS) { if (could_play_card(EVENT_TEUTONIC_AMBUSH)) return true if (is_defender()) { if (could_play_card(EVENT_TEUTONIC_HILL)) return true if (!is_winter()) if (could_play_card(EVENT_TEUTONIC_MARSH)) return true } if (!is_winter()) if (could_play_card(EVENT_TEUTONIC_BRIDGE)) return true } if (game.active === RUSSIANS) { if (could_play_card(EVENT_RUSSIAN_AMBUSH)) return true if (is_defender()) { if (could_play_card(EVENT_RUSSIAN_HILL)) return true if (!is_winter()) if (could_play_card(EVENT_RUSSIAN_MARSH)) return true } if (!is_winter()) if (could_play_card(EVENT_RUSSIAN_BRIDGE)) return true if (!is_summer()) if (could_play_card(EVENT_RUSSIAN_RAVENS_ROCK)) return true } // Battle or Storm if (game.active === TEUTONS) { if (could_play_card(EVENT_TEUTONIC_FIELD_ORGAN)) if (has_lords_in_battle()) return true } return false } function prompt_battle_events() { // both attacker and defender events if (game.active === TEUTONS) { gen_action_card_if_held(EVENT_TEUTONIC_AMBUSH) if (!is_winter()) gen_action_card_if_held(EVENT_TEUTONIC_BRIDGE) if (has_lords_in_battle()) gen_action_card_if_held(EVENT_TEUTONIC_FIELD_ORGAN) } if (game.active === RUSSIANS) { gen_action_card_if_held(EVENT_RUSSIAN_AMBUSH) if (!is_winter()) gen_action_card_if_held(EVENT_RUSSIAN_BRIDGE) if (!is_summer()) gen_action_card_if_held(EVENT_RUSSIAN_RAVENS_ROCK) } view.actions.done = 1 } states.attacker_events = { inactive: "Attacker Events", prompt() { view.prompt = "Attacker may play Events." prompt_battle_events() }, card: action_battle_events, done() { end_attacker_events() }, } states.defender_events = { inactive: "Defender Events", prompt() { view.prompt = "Defender may play Events." prompt_battle_events() // defender only events if (game.active === TEUTONS) { if (!is_winter()) gen_action_card_if_held(EVENT_TEUTONIC_MARSH) gen_action_card_if_held(EVENT_TEUTONIC_HILL) } if (game.active === RUSSIANS) { if (!is_winter()) gen_action_card_if_held(EVENT_RUSSIAN_MARSH) gen_action_card_if_held(EVENT_RUSSIAN_HILL) } }, card: action_battle_events, done() { end_defender_events() }, } function action_battle_events(c) { game.what = c set_delete(current_hand(), c) set_add(game.events, c) switch (c) { case EVENT_TEUTONIC_HILL: case EVENT_TEUTONIC_MARSH: case EVENT_RUSSIAN_HILL: case EVENT_RUSSIAN_MARSH: case EVENT_RUSSIAN_RAVENS_ROCK: // nothing more needs to be done for these log(`Played E${c}.`) resume_battle_events() break case EVENT_TEUTONIC_AMBUSH: case EVENT_RUSSIAN_AMBUSH: log(`Played E${c}.`) if (is_attacker()) game.battle.ambush |= 2 else game.battle.ambush |= 1 break case EVENT_TEUTONIC_BRIDGE: case EVENT_RUSSIAN_BRIDGE: // must select target lord game.state = "bridge" break case EVENT_TEUTONIC_FIELD_ORGAN: // must select target lord game.state = "field_organ" break } } states.bridge = { inactive: "Bridge", prompt() { view.prompt = "Bridge: Play on a Center Lord." view.what = game.what let array = game.battle.array if (is_attacker()) { if (array[D2] !== NOBODY) gen_action_lord(array[D2]) if (array[RG2] !== NOBODY) gen_action_lord(array[RG2]) } else { // Cannot play on Relief Sallying lord if (array[A2] !== NOBODY) gen_action_lord(array[A2]) } }, lord(lord) { log(`Played E${game.what} on L${lord}.`) if (!game.battle.bridge) game.battle.bridge = { lord1: NOBODY, lord2: NOBODY, n1: 0, n2: 0 } if (is_p1_lord(lord)) game.battle.bridge.lord1 = lord else game.battle.bridge.lord2 = lord resume_battle_events() }, } states.field_organ = { inactive: "Field Organ", prompt() { view.prompt = "Field Organ: Play on a Lord." view.what = game.what let array = game.battle.array if (is_attacker()) { for (let pos of battle_attacking_positions) if (array[pos] !== NOBODY) gen_action_lord(array[pos]) } else { for (let pos of battle_defending_positions) if (array[pos] !== NOBODY) gen_action_lord(array[pos]) } }, lord(lord) { log(`Played E${game.what} on L${lord}.`) game.battle.field_organ = lord resume_battle_events() }, } // === BATTLE: CONCEDE THE FIELD === function goto_battle_rounds() { set_active_attacker() goto_concede() } function goto_concede() { log_h4(`Battle Round ${game.battle.round}`) game.state = "concede_battle" } states.concede_battle = { inactive: "Concede", prompt() { view.prompt = "Battle: Concede the Field?" view.actions.concede = 1 view.actions.battle = 1 }, concede() { log(game.active + " Conceded.") game.battle.conceded = game.active goto_reposition_battle() }, battle() { set_active_enemy() if (is_attacker()) goto_reposition_battle() }, } // === BATTLE: REPOSITION === function send_to_reserve(pos) { if (game.battle.array[pos] !== NOBODY) { set_add(game.battle.reserves, game.battle.array[pos]) game.battle.array[pos] = NOBODY } } function slide_array(from, to) { game.battle.array[to] = game.battle.array[from] game.battle.array[from] = NOBODY } function goto_reposition_battle() { let array = game.battle.array // If all D routed. if (array[D1] === NOBODY && array[D2] === NOBODY && array[D3] === NOBODY) { log("Defenders Routed.") } // If all A routed. if (array[A1] === NOBODY && array[A2] === NOBODY && array[A3] === NOBODY) { log("Attackers Routed.") } set_active_attacker() goto_reposition_advance() } function goto_reposition_advance() { if (can_reposition_advance()) game.state = "reposition_advance" else end_reposition_advance() } function end_reposition_advance() { game.who = NOBODY set_active_enemy() if (is_attacker()) goto_reposition_center() else goto_reposition_advance() } function goto_reposition_center() { if (can_reposition_center()) game.state = "reposition_center" else end_reposition_center() } function end_reposition_center() { game.who = NOBODY set_active_enemy() if (is_attacker()) goto_first_strike() else goto_reposition_center() } function can_reposition_advance() { if (has_friendly_reserves()) { let array = game.battle.array if (is_attacker()) { if (array[A1] === NOBODY || array[A2] === NOBODY || array[A3] === NOBODY) return true } else { if (array[D1] === NOBODY || array[D2] === NOBODY || array[D3] === NOBODY) return true } } return false } states.reposition_advance = { inactive: "Reposition", prompt() { view.prompt = "Reposition: Advance from Reserve." let array = game.battle.array for (let lord of game.battle.reserves) if (is_friendly_lord(lord) && lord !== game.who) gen_action_lord(lord) if (game.who !== NOBODY) { if (is_attacker()) { if (array[A1] === NOBODY) gen_action_array(A1) if (array[A2] === NOBODY) gen_action_array(A2) if (array[A3] === NOBODY) gen_action_array(A3) } else { if (array[D1] === NOBODY) gen_action_array(D1) if (array[D2] === NOBODY) gen_action_array(D2) if (array[D3] === NOBODY) gen_action_array(D3) } } }, lord(lord) { game.who = lord }, array(pos) { set_delete(game.battle.reserves, game.who) game.battle.array[pos] = game.who game.who = NOBODY goto_reposition_advance() }, } function can_reposition_center() { let array = game.battle.array if (is_attacker()) { if (array[A2] === NOBODY && (array[A1] !== NOBODY || array[A3] !== NOBODY)) return true } else { if (array[D2] === NOBODY && (array[D1] !== NOBODY || array[D3] !== NOBODY)) return true } return false } states.reposition_center = { inactive: "Reposition", prompt() { view.prompt = "Reposition: Slide to Center." let array = game.battle.array if (is_attacker()) { if (array[A2] === NOBODY) { if (array[A1] !== NOBODY) gen_action_lord(game.battle.array[A1]) if (array[A3] !== NOBODY) gen_action_lord(game.battle.array[A3]) } } else { if (array[D2] === NOBODY) { if (array[D1] !== NOBODY) gen_action_lord(game.battle.array[D1]) if (array[D3] !== NOBODY) gen_action_lord(game.battle.array[D3]) } } if (game.who !== NOBODY) { let from = get_lord_array_position(game.who) if (from === A1 || from === A3) gen_action_array(A2) if (from === D1 || from === D3) gen_action_array(D2) } }, lord(lord) { game.who = lord }, array(pos) { let from = get_lord_array_position(game.who) slide_array(from, pos) game.who = NOBODY goto_reposition_center() }, } // === BATTLE: STRIKE === // Strike groups: // Strike opposing lord // Strike closest flanked lord (choice left/right) if not directly opposed // Combine strikes with lords targeting same position // // Target groups: // If any striker is flanking target, single target. // If any other lords flank all strikers, add them to target group. function get_battle_array(pos) { if (game.battle.ambush & 1) if (pos === A1 || pos === A3) return NOBODY if (game.battle.ambush & 2) if (pos === D1 || pos === D3) return NOBODY return game.battle.array[pos] } function filled(pos) { return get_battle_array(pos) !== NOBODY } function empty(pos) { return get_battle_array(pos) === NOBODY } const battle_defending_positions = [ D1, D2, D3 ] const battle_attacking_positions = [ A1, A2, A3 ] const battle_steps = [ { name: "Defending Archery", hits: count_archery_hits, xhits: count_archery_xhits }, { name: "Attacking Archery", hits: count_archery_hits, xhits: count_archery_xhits }, { name: "Defending Horse", hits: count_horse_hits, xhits: count_zero_hits }, { name: "Attacking Horse", hits: count_horse_hits, xhits: count_zero_hits }, { name: "Defending Foot", hits: count_foot_hits, xhits: count_zero_hits }, { name: "Attacking Foot", hits: count_foot_hits, xhits: count_zero_hits }, ] function count_zero_hits(_) { return 0 } function count_archery_xhits(lord) { let xhits = 0 if (lord_has_capability(lord, AOW_TEUTONIC_BALISTARII) || lord_has_capability(lord, AOW_RUSSIAN_STRELTSY)) xhits += get_lord_forces(lord, MEN_AT_ARMS) if (is_hill_in_play()) return xhits << 1 return xhits } function count_archery_hits(lord) { let hits = 0 if (!is_marsh_in_play()) { if (lord_has_capability(lord, AOW_RUSSIAN_LUCHNIKI)) { hits += get_lord_forces(lord, LIGHT_HORSE) hits += get_lord_forces(lord, MILITIA) } hits += get_lord_forces(lord, ASIATIC_HORSE) } else { if (lord_has_capability(lord, AOW_RUSSIAN_LUCHNIKI)) { hits += get_lord_forces(lord, MILITIA) } } if (is_hill_in_play()) return hits << 1 return hits } function count_melee_hits(lord) { return count_horse_hits(lord) + count_foot_hits(lord) } function assemble_melee_forces(lord) { let forces = { knights: get_lord_forces(lord, KNIGHTS), sergeants: get_lord_forces(lord, SERGEANTS), light_horse: get_lord_forces(lord, LIGHT_HORSE), men_at_arms: get_lord_forces(lord, MEN_AT_ARMS), militia: get_lord_forces(lord, MILITIA), serfs: get_lord_forces(lord, SERFS), } if (is_marsh_in_play()) { forces.knights = 0 forces.sergeants = 0 forces.light_horse = 0 } if (game.battle.bridge && (game.battle.bridge.lord1 === lord || game.battle.bridge.lord2 === lord)) { let n = is_p1_lord(lord) ? game.battle.bridge.n1 : game.battle.bridge.n2 log(`Bridge L${lord}`) if (is_horse_step()) { // Pick at most 1 LH if there are any Foot (for +1/2 rounding benefit) if (forces.men_at_arms + forces.militia + forces.serfs > 0 && forces.light_horse > 1) forces.light_horse = 1 if (forces.knights >= n) forces.knights = n n -= forces.knights if (forces.sergeants >= n) forces.sergeants = n n -= forces.sergeants if (forces.light_horse >= n) forces.light_horse = n n -= forces.light_horse if (forces.knights > 0) logi(`${forces.knights} Knights`) if (forces.sergeants > 0) logi(`${forces.sergeants} Sergeants`) if (forces.light_horse > 0) logi(`${forces.light_horse} Light Horse`) if (forces.knights + forces.sergeants + forces.light_horse === 0) logi(`None`) } if (is_foot_step()) { if (forces.men_at_arms >= n) forces.men_at_arms = n n -= forces.men_at_arms if (forces.militia >= n) forces.militia = n n -= forces.militia if (forces.serfs >= n) forces.serfs = n n -= forces.serfs if (forces.men_at_arms > 0) logi(`${forces.men_at_arms} Men-at-Arms`) if (forces.militia > 0) logi(`${forces.militia} Militia`) if (forces.serfs > 0) logi(`${forces.serfs} Serfs`) if (forces.men_at_arms + forces.militia + forces.serfs === 0) logi(`None`) } if (is_p1_lord(lord)) game.battle.bridge.n1 = n else game.battle.bridge.n2 = n } return forces } function count_horse_hits(lord) { let hits = 0 if (!is_marsh_in_play()) { let forces = assemble_melee_forces(lord) hits += forces.knights << 2 hits += forces.sergeants << 1 hits += forces.light_horse if (game.battle.field_organ === lord && game.battle.round === 1) { log(`E${EVENT_TEUTONIC_FIELD_ORGAN} L${lord}.`) hits += forces.knights << 1 hits += forces.sergeants << 1 } } return hits } function count_foot_hits(lord) { let forces = assemble_melee_forces(lord) let hits = 0 hits += forces.men_at_arms << 1 hits += forces.militia hits += forces.serfs return hits } function count_garrison_xhits() { if (is_archery_step()) return game.battle.garrison.men_at_arms return 0 } function count_garrison_hits() { if (is_melee_step()) return (game.battle.garrison.knights << 1) + (game.battle.garrison.men_at_arms << 1) return 0 } function count_lord_xhits(lord) { return battle_steps[game.battle.step].xhits(lord) } function count_lord_hits(lord) { return battle_steps[game.battle.step].hits(lord) } function is_battle_over() { set_active_attacker() if (has_no_unrouted_forces()) return true set_active_defender() if (has_no_unrouted_forces()) return true return false } function has_no_unrouted_forces() { // All unrouted lords are either in battle array or in reserves for (let p = 0; p < 12; ++p) if (is_friendly_lord(game.battle.array[p])) return false for (let lord of game.battle.reserves) if (is_friendly_lord(lord)) return false return true } function is_attacker() { return game.active === game.battle.attacker } function is_defender() { return game.active !== game.battle.attacker } function is_attacker_step() { return (game.battle.step & 1) === 1 } function is_defender_step() { return (game.battle.step & 1) === 0 } function is_archery_step() { return game.battle.step < 2 } function is_melee_step() { return game.battle.step >= 2 } function is_horse_step() { return game.battle.step === 2 || game.battle.step === 3 } function is_foot_step() { return game.battle.step === 4 || game.battle.step === 5 } function did_concede() { return game.active === game.battle.conceded } function did_not_concede() { return game.active !== game.battle.conceded } function has_strike(pos) { return game.battle.ah[pos] + game.battle.ahx[pos] > 0 } function current_strike_positions() { return is_attacker_step() ? battle_attacking_positions : battle_defending_positions } function find_closest_target(A, B, C) { if (filled(A)) return A if (filled(B)) return B if (filled(C)) return C return -1 } function find_closest_target_center(T2) { if (game.battle.fc < 0) throw Error("unset front l/r choice") if (game.battle.rc < 0) throw Error("unset rear l/r choice") if (filled(T2)) return T2 if (T2 >= A1 && T2 <= D3) return game.battle.fc return game.battle.rc } function find_strike_target(S) { switch (S) { case A1: return find_closest_target(D1, D2, D3) case A2: return find_closest_target_center(D2) case A3: return find_closest_target(D3, D2, D1) case D1: return find_closest_target(A1, A2, A3) case D2: return find_closest_target_center(A2) case D3: return find_closest_target(A3, A2, A1) } } function has_strike_target(S) { if (is_attacker_step() && has_garrison()) return true if (S === A1 || S === A2 || S === A3) return filled(D1) || filled(D2) || filled(D3) if (S === D1 || S === D2 || S === D3) return filled(A1) || filled(A2) || filled(A3) } function has_no_strike_targets() { if (is_defender_step() && has_garrison()) if (has_strike_target(D2)) return false for (let striker of game.battle.strikers) if (has_strike_target(striker)) return false return true } function has_no_strikers_and_strike_targets() { if (is_defender_step() && has_garrison()) { if (is_archery_step() && game.battle.garrison.men_at_arms > 0) if (has_strike_target(D2)) return false if (is_melee_step() && game.battle.garrison.men_at_arms + game.battle.garrison.knights > 0) if (has_strike_target(D2)) return false } for (let pos of current_strike_positions()) if (has_strike(pos) && has_strike_target(pos)) return false return true } function create_strike_group(start) { let strikers = [ start ] let target = find_strike_target(start) for (let pos of current_strike_positions()) if (pos !== start && filled(pos) && find_strike_target(pos) === target) set_add(strikers, pos) return strikers } function flanks_position_row(S, T, S1, S2, S3, T1, T2, T3) { // S and T are not empty switch (S) { case S1: switch (T) { case T1: return false case T2: return empty(T1) case T3: return empty(T1) && empty(T2) } break case S2: return empty(T2) case S3: switch (T) { case T1: return empty(T3) && empty(T2) case T2: return empty(T3) case T3: return false } break } return false } function flanks_position(S, T) { if (S === A1 || S === A2 || S === A3) return flanks_position_row(S, T, A1, A2, A3, D1, D2, D3) if (S === D1 || S === D2 || S === D3) return flanks_position_row(S, T, D1, D2, D3, A1, A2, A3) } function flanks_all_positions(S, TT) { for (let T of TT) if (!flanks_position(S, T)) return false return true } function strike_left_or_right(S2, T1, T2, T3) { if (has_strike(S2)) { if (filled(T2)) return T2 let has_t1 = filled(T1) let has_t3 = filled(T3) if (has_t1 && has_t3) return -1 if (has_t1) return T1 if (has_t3) return T3 } return 1000 // No target! } function strike_defender_row() { let has_d1 = filled(D1) let has_d2 = filled(D2) let has_d3 = filled(D3) if (has_d1 && !has_d2 && !has_d3) return D1 if (!has_d1 && has_d2 && !has_d3) return D2 if (!has_d1 && !has_d2 && has_d3) return D3 return -1 } // === BATTLE: STRIKE === // for each battle step: // generate strikes for each lord // while strikes remain: // create list of strike groups (choose left/right both rows) // select strike group // create target group (choose if sally) // total strikes and roll for walls // while hits remain: // assign hit to unit in target group // if lord routs: // forget choice of left/right strike group in current row // create new target group (choose if left/right/sally) function format_strike_step() { return battle_steps[game.battle.step].name } function format_hits() { if (game.battle.xhits > 0 && game.battle.hits > 0) { if (game.battle.xhits > 1 && game.battle.hits > 1) return `${game.battle.xhits} Crossbow Hits and ${game.battle.hits} Hits` else if (game.battle.xhits > 1) return `${game.battle.xhits} Crossbow Hits and ${game.battle.hits} Hit` else if (game.battle.hits > 1) return `${game.battle.xhits} Crossbow Hit and ${game.battle.hits} Hits` else return `${game.battle.xhits} Crossbow Hit and ${game.battle.hits} Hit` } else if (game.battle.xhits > 0) { if (game.battle.xhits > 1) return `${game.battle.xhits} Crossbow Hits` else return `${game.battle.xhits} Crossbow Hit` } else { if (game.battle.hits > 1) return `${game.battle.hits} Hits` else return `${game.battle.hits} Hit` } } function goto_first_strike() { game.battle.step = 0 if (game.battle.bridge) { game.battle.bridge.n1 = game.battle.round * 2 game.battle.bridge.n2 = game.battle.round * 2 } goto_strike() } function goto_next_strike() { let end = 6 game.battle.step++ if (game.battle.step >= end) end_battle_round() else goto_strike() } function goto_strike() { // Exit early if one side is completely routed if (is_battle_over()) { end_battle_round() return } if (is_attacker_step()) set_active_attacker() else set_active_defender() log_h5(battle_steps[game.battle.step].name) // Once per Archery and once per Melee. if (game.battle.step === 0 || game.battle.step === 2) { game.battle.warrior_monks = 0 for (let p = 0; p < 12; ++p) { let lord = game.battle.array[p] if (lord !== NOBODY && lord_has_capability(lord, AOW_TEUTONIC_WARRIOR_MONKS)) game.battle.warrior_monks |= 1 << lord } } if (is_marsh_in_play()) { if (game.active === TEUTONS) logevent(EVENT_RUSSIAN_MARSH) else logevent(EVENT_TEUTONIC_MARSH) } if (is_archery_step() && is_hill_in_play()) { if (game.active === TEUTONS) logevent(EVENT_TEUTONIC_HILL) else logevent(EVENT_RUSSIAN_HILL) } // Generate hits game.battle.ah = [ 0, 0, 0, 0, 0, 0 ] game.battle.ahx = [ 0, 0, 0, 0, 0, 0 ] for (let pos of current_strike_positions()) { let lord = get_battle_array(pos) if (lord !== NOBODY) { let hits = count_lord_hits(lord) let xhits = count_lord_xhits(lord) game.battle.ah[pos] = hits game.battle.ahx[pos] = xhits if (xhits > 2) log(`L${lord} ${frac(xhits)} Crossbow Hits.`) else if (xhits > 0) log(`L${lord} ${frac(xhits)} Crossbow Hit.`) if (hits > 2) log(`L${lord} ${frac(hits)} Hits.`) else if (hits > 0) log(`L${lord} ${frac(hits)} Hit.`) } } if (did_concede()) log("Pursuit.") // Strike left or right or defender if (is_attacker_step()) game.battle.fc = strike_left_or_right(A2, D1, D2, D3) else game.battle.fc = strike_left_or_right(D2, A1, A2, A3) if (has_no_strikers_and_strike_targets()) log("None.") resume_strike() } function resume_strike() { if (has_no_strikers_and_strike_targets()) goto_next_strike() else if (game.battle.fc < 0 || game.battle.rc < 0) game.state = "strike_left_right" else goto_strike_group() } function prompt_target_2(S1, T1, T3) { view.who = game.battle.array[S1] gen_action_lord(game.battle.array[T1]) gen_action_lord(game.battle.array[T3]) } function prompt_left_right() { view.prompt = `${format_strike_step()}: Strike left or right?` if (is_attacker_step()) prompt_target_2(A2, D1, D3) else prompt_target_2(D2, A1, A3) } function action_left_right(lord) { log(`Targeted L${lord}.`) let pos = get_lord_array_position(lord) if (game.battle.fc < 0) game.battle.fc = pos else game.battle.rc = pos } states.strike_left_right = { get inactive() { return format_strike_step() + " \u2014 Strike" }, prompt: prompt_left_right, lord(lord) { action_left_right(lord) resume_strike() }, } states.assign_left_right = { get inactive() { return format_strike_step() + " \u2014 Strike" }, prompt: prompt_left_right, lord(lord) { action_left_right(lord) set_active_enemy() goto_assign_hits() }, } function goto_strike_group() { game.state = "strike_group" } function select_strike_group(pos) { game.battle.strikers = create_strike_group(pos) goto_strike_total_hits() } states.strike_group = { get inactive() { return format_strike_step() + " \u2014 Strike" }, prompt() { view.prompt = `${format_strike_step()}: Strike.` if (has_garrison_strike()) { view.actions.garrison = 1 if (!has_strike(D2)) view.prompt = `${format_strike_step()}: Strike with Garrison.` } for (let pos of current_strike_positions()) if (has_strike(pos)) gen_action_lord(game.battle.array[pos]) }, lord(lord) { select_strike_group(get_lord_array_position(lord)) }, garrison() { if (has_strike(D2)) select_strike_group(D2) else select_strike_group(-1) }, } // === BATTLE: TOTAL HITS (ROUND UP) === function goto_strike_total_hits() { let hits = 0 let xhits = 0 let slist = [] // Total hits for (let pos of game.battle.strikers) { if (game.battle.ah[pos] + game.battle.ahx[pos] > 0) { slist.push(lord_name[game.battle.array[pos]]) hits += game.battle.ah[pos] xhits += game.battle.ahx[pos] } } // Round in favor of crossbow hits. if (xhits & 1) { hits = (hits >> 1) xhits = (xhits >> 1) + 1 } else { if (hits & 1) hits = (hits >> 1) + 1 else hits = (hits >> 1) xhits = (xhits >> 1) } // Conceding side halves its total Hits, rounded up. if (did_concede()) { hits = (hits + 1) >> 1 xhits = (xhits + 1) >> 1 } game.battle.hits = hits game.battle.xhits = xhits log_br() log(slist.join(", ")) goto_strike_roll_walls() } // === BATTLE: ROLL WALLS === function goto_strike_roll_walls() { set_active_enemy() if (game.battle.xhits > 0) log_hits(game.battle.xhits, "Crossbow Hit") if (game.battle.hits > 0) log_hits(game.battle.hits, "Hit") game.who = -2 goto_assign_hits() } function log_hits(total, name) { if (total === 1) logi(`${total} ${name}`) else if (total > 1) logi(`${total} ${name}s`) else logi(`No ${name}s`) } // === BATTLE: ASSIGN HITS TO UNITS / ROLL BY HIT / ROUT === function goto_assign_hits() { if (game.battle.hits + game.battle.xhits === 0) return end_assign_hits() if (has_no_strike_targets()) { log("Lost " + format_hits() + ".") return end_assign_hits() } if (is_attacker_step()) { if (game.battle.fc < 0 && set_has(game.battle.strikers, A2)) return goto_assign_left_right() } else { if (game.battle.fc < 0 && set_has(game.battle.strikers, D2)) return goto_assign_left_right() } game.state = "assign_hits" } function goto_assign_left_right() { set_active_enemy() game.state = "assign_left_right" } function end_assign_hits() { for (let pos of game.battle.strikers) { game.battle.ah[pos] = 0 game.battle.ahx[pos] = 0 } game.who = NOBODY game.battle.strikers = 0 game.battle.hits = 0 game.battle.xhits = 0 set_active_enemy() resume_strike() } function for_each_target(fn) { if (is_defender_step() && has_garrison()) { if (filled(A2)) fn(game.battle.array[A2]) return } let start = game.battle.strikers[0] let target = find_strike_target(start) fn(game.battle.array[target]) // If any striker flanks target, target must take all hits for (let striker of game.battle.strikers) if (flanks_position(striker, target)) return // If other lord flanks all strikers, he may take hits instead for (let flanker of ARRAY_FLANKS[target]) if (filled(flanker) && flanks_all_positions(flanker, game.battle.strikers)) fn(game.battle.array[flanker]) } function prompt_hit_armored_forces() { let has_armored = false for_each_target(lord => { if (get_lord_forces(lord, KNIGHTS) > 0) { gen_action_knights(lord) has_armored = true } if (get_lord_forces(lord, SERGEANTS) > 0) { gen_action_sergeants(lord) has_armored = true } if (get_lord_forces(lord, MEN_AT_ARMS) > 0) { gen_action_men_at_arms(lord) has_armored = true } }) return has_armored } function prompt_hit_unarmored_forces() { for_each_target(lord => { if (get_lord_forces(lord, LIGHT_HORSE) > 0) gen_action_light_horse(lord) if (get_lord_forces(lord, ASIATIC_HORSE) > 0) gen_action_asiatic_horse(lord) if (get_lord_forces(lord, MILITIA) > 0) gen_action_militia(lord) if (get_lord_forces(lord, SERFS) > 0) gen_action_serfs(lord) }) } function prompt_hit_forces() { for_each_target(lord => { if (get_lord_forces(lord, KNIGHTS) > 0) gen_action_knights(lord) if (get_lord_forces(lord, SERGEANTS) > 0) gen_action_sergeants(lord) if (get_lord_forces(lord, LIGHT_HORSE) > 0) gen_action_light_horse(lord) if (get_lord_forces(lord, ASIATIC_HORSE) > 0) gen_action_asiatic_horse(lord) if (get_lord_forces(lord, MEN_AT_ARMS) > 0) gen_action_men_at_arms(lord) if (get_lord_forces(lord, MILITIA) > 0) gen_action_militia(lord) if (get_lord_forces(lord, SERFS) > 0) gen_action_serfs(lord) }) } states.assign_hits = { get inactive() { return format_strike_step() + " \u2014 Assign " + format_hits() }, prompt() { view.prompt = `${format_strike_step()}: Assign ${format_hits()} to units.` view.group = game.battle.strikers.map(p => game.battle.array[p]) prompt_hit_forces() }, knights(lord) { action_assign_hits(lord, KNIGHTS) }, sergeants(lord) { action_assign_hits(lord, SERGEANTS) }, light_horse(lord) { action_assign_hits(lord, LIGHT_HORSE) }, asiatic_horse(lord) { action_assign_hits(lord, ASIATIC_HORSE) }, men_at_arms(lord) { action_assign_hits(lord, MEN_AT_ARMS) }, militia(lord) { action_assign_hits(lord, MILITIA) }, serfs(lord) { action_assign_hits(lord, SERFS) }, } function rout_lord(lord) { log(`L${lord} Routed.`) let pos = get_lord_array_position(lord) // Remove from battle array game.battle.array[pos] = NOBODY // Strike left or right or defender if (pos >= A1 && pos <= A3) { game.battle.fc = strike_left_or_right(D2, A1, A2, A3) } else if (pos >= D1 && pos <= D3) { game.battle.fc = strike_left_or_right(A2, D1, D2, D3) if (is_sa_without_rg()) game.battle.rc = strike_defender_row() } } function rout_unit(lord, type) { if (lord === GARRISON) { if (type === KNIGHTS) game.battle.garrison.knights-- if (type === MEN_AT_ARMS) game.battle.garrison.men_at_arms-- if (game.battle.garrison.knights + game.battle.garrison.men_at_arms === 0) { log("Garrison Routed.") game.battle.garrison = 0 } } else { add_lord_forces(lord, type, -1) add_lord_routed_forces(lord, type, 1) } } function remove_serf(lord) { add_lord_forces(lord, SERFS, -1) game.pieces.smerdi++ } function use_warrior_monks(lord, type) { if (type === KNIGHTS) { let bit = 1 << lord if (game.battle.warrior_monks & bit) { game.battle.warrior_monks ^= bit return true } } return false } function which_lord_capability(lord, list) { for (let c of list) if (lord_has_capability_card(lord, c)) return c return -1 } function assign_hit_roll(what, prot, extra) { let die = roll_die() if (die <= prot) { logi(`${what} ${range(prot)}: ${MISS[die]}${extra}`) return false } else { logi(`${what} ${range(prot)}: ${HIT[die]}${extra}`) return true } } function action_assign_hits(lord, type) { let protection = FORCE_PROTECTION[type] let evade = FORCE_EVADE[type] if (game.who !== lord) { game.who = lord if (lord === GARRISON) log("Garrison") else log(`L${lord}`) } let extra = "" let crossbow = 0 if (is_armored_force(type) && game.battle.xhits > 0) { extra += " (-2\xa0Crossbow)" crossbow = 2 } if (type === SERGEANTS || type === MEN_AT_ARMS) { if (lord_has_capability(lord, AOW_TEUTONIC_HALBBRUDER)) { extra += ` (+1\xa0C${which_lord_capability(lord, AOW_TEUTONIC_HALBBRUDER)})` protection += 1 } } // Evade only in Battle Melee steps if (evade > 0 && is_melee_step()) { if (assign_hit_roll(FORCE_TYPE_NAME[type], evade, extra)) rout_unit(lord, type) } else if (protection > 0) { if (assign_hit_roll(FORCE_TYPE_NAME[type], protection - crossbow, extra)) { if (use_warrior_monks(lord, type)) { let monks = which_lord_capability(lord, AOW_TEUTONIC_WARRIOR_MONKS) if (assign_hit_roll(`C${monks}`, protection - crossbow, extra)) rout_unit(lord, type) } else { rout_unit(lord, type) } } } else { logi(`${FORCE_TYPE_NAME[type]} removed`) remove_serf(lord, type) } if (game.battle.xhits) game.battle.xhits-- else game.battle.hits-- if (!lord_has_unrouted_units(lord)) rout_lord(lord) goto_assign_hits() } // === BATTLE: NEW ROUND === function end_battle_round() { if (game.battle.conceded) { game.battle.loser = game.battle.conceded end_battle() return } set_active_attacker() if (has_no_unrouted_forces()) { game.battle.loser = game.active end_battle() return } set_active_defender() if (has_no_unrouted_forces()) { game.battle.loser = game.active end_battle() return } game.battle.round ++ game.battle.ambush = 0 set_active_attacker() goto_concede() } // === ENDING THE BATTLE === // Ending the Battle - optimized from rules as written // Loser retreat / withdraw / remove // Loser losses // Loser service // Victor losses // Victor spoils // Ending the Storm // Sack (loser removes lords) // Victor losses // Victor spoils function set_active_loser() { set_active(game.battle.loser) } function set_active_victor() { if (game.battle.loser === P1) set_active(P2) else set_active(P1) } function end_battle() { log_h4(`${game.battle.loser} Lost`) game.battle.array = 0 goto_battle_withdraw() } // === ENDING THE STORM: SACK === function award_spoils(n) { add_spoils(PROV, n) add_spoils(COIN, n) } function goto_sack() { let here = game.battle.where set_active_victor() log(`${game.active} Sacked %${here}.`) conquer_stronghold(game.battle.where) remove_walls(game.battle.where) if (here === LOC_NOVGOROD) { if (game.pieces.veche_coin > 0) { add_spoils(COIN, game.pieces.veche_coin) log(`Awarded ${game.pieces.veche_coin} Coin from Veche.`) game.pieces.veche_coin = 0 } award_spoils(3) } else if (is_city(here)) award_spoils(2) else if (is_fort(here)) award_spoils(1) else if (is_bishopric(here)) award_spoils(2) else if (is_castle(here)) award_spoils(1) set_active_loser() resume_sack() } function resume_sack() { if (has_friendly_lord(game.battle.where)) game.state = "sack" else goto_battle_losses_loser() } states.sack = { inactive: "Remove Lords", prompt() { let here = game.battle.where view.prompt = `Sack: Remove all Lords at ${data.locales[here].name}.` for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here) gen_action_lord(lord) }, lord(lord) { transfer_assets_except_ships(lord) if (can_ransom_lord_battle(lord)) { goto_ransom(lord) } else { disband_lord(lord, true) resume_sack() } }, } function end_ransom_sack() { resume_sack() } // === ENDING THE BATTLE: WITHDRAW === function withdrawal_capacity_needed(here) { let has_upper = 0 let has_other = 0 for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === here && is_lord_unbesieged(lord) && !is_lower_lord(lord)) { if (is_upper_lord(lord)) has_upper++ else has_other++ } } if (has_upper) return 2 if (has_other) return 1 return 0 } function goto_battle_withdraw() { set_active_loser() game.spoils = 0 let here = game.battle.where let wn = withdrawal_capacity_needed(here) if (wn > 0 && can_withdraw(here, wn)) { game.state = "battle_withdraw" } else { end_battle_withdraw() } } function end_battle_withdraw() { goto_retreat() } states.battle_withdraw = { inactive: "Withdraw", prompt() { let here = game.battle.where let capacity = stronghold_capacity(here) view.prompt = "Battle: Select Lords to Withdraw into Stronghold." // NOTE: Sallying lords are still flagged "besieged" and are thus already withdrawn! if (capacity >= 1) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === here && !is_lower_lord(lord) && is_lord_unbesieged(lord)) { if (is_upper_lord(lord)) { if (capacity >= 2) gen_action_lord(lord) } else { gen_action_lord(lord) } } } } view.actions.end_withdraw = 1 }, lord(lord) { push_undo() let lower = get_lower_lord(lord) log(`L${lord} Withdrew.`) set_lord_besieged(lord, 1) if (lower !== NOBODY) { log(`L${lower} Withdrew.`) set_lord_besieged(lower, 1) } }, end_withdraw() { clear_undo() end_battle_withdraw() }, } // === ENDING THE BATTLE: RETREAT === function count_retreat_transport(type) { let n = 0 for (let lord of game.battle.retreated) n += count_lord_transport(lord, type) return n } function count_retreat_assets(type) { let n = 0 for (let lord of game.battle.retreated) n += get_lord_assets(lord, type) return n } function transfer_assets_except_ships(lord) { add_spoils(PROV, get_lord_assets(lord, PROV)) add_spoils(COIN, get_lord_assets(lord, COIN)) add_spoils(CART, get_lord_assets(lord, CART)) set_lord_assets(lord, PROV, 0) set_lord_assets(lord, COIN, 0) set_lord_assets(lord, CART, 0) } function can_retreat_to(to) { return !has_unbesieged_enemy_lord(to) && !is_unbesieged_enemy_stronghold(to) } function can_retreat() { if (game.march) { // Battle after March if (is_attacker()) return can_retreat_to(game.march.from) for (let [to, way] of data.locales[game.battle.where].ways) if (way !== game.march.approach && can_retreat_to(to)) return true } else { // Battle after Sally for (let to of data.locales[game.battle.where].adjacent) if (can_retreat_to(to)) return true } return false } function goto_retreat() { let here = game.battle.where if (count_unbesieged_friendly_lords(here) > 0 && can_retreat()) { game.battle.retreated = [] for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here && is_lord_unbesieged(lord)) set_add(game.battle.retreated, lord) game.state = "retreat" } else { end_retreat() } } function end_retreat() { goto_battle_remove() } states.retreat = { inactive: "Retreat", prompt() { view.prompt = "Battle: Retreat losing Lords." view.group = game.battle.retreated if (game.march) { // after March if (is_attacker()) { gen_action_locale(game.march.from) } else { for (let [to, way] of data.locales[game.battle.where].ways) if (way !== game.march.approach && can_retreat_to(to)) gen_action_locale(to) } } else { // after Sally for (let to of data.locales[game.battle.where].adjacent) if (can_retreat_to(to)) gen_action_locale(to) } }, locale(to) { push_undo() if (game.march) { if (is_attacker()) { game.battle.retreat_to = to game.battle.retreat_way = game.march.approach retreat_1() } else { let ways = list_ways(game.battle.where, to) if (ways.length > 2) { game.battle.retreat_to = to game.state = "retreat_way" } else { game.battle.retreat_to = to game.battle.retreat_way = ways[1] retreat_1() } } } else { let ways = list_ways(game.battle.where, to) if (ways.length > 2) { game.battle.retreat_to = to game.state = "retreat_way" } else { game.battle.retreat_to = to game.battle.retreat_way = ways[1] retreat_1() } } }, } states.retreat_way = { inactive: "Retreat", prompt() { view.prompt = `Retreat: Select Way.` view.group = game.battle.retreated let from = game.battle.where let to = game.battle.retreat_to let ways = list_ways(from, to) for (let i = 1; i < ways.length; ++i) gen_action_way(ways[i]) }, way(way) { game.battle.retreat_way = way retreat_1() }, } function retreat_1() { // Retreated without having conceded the Field if (did_not_concede()) { for (let lord of game.battle.retreated) transfer_assets_except_ships(lord) retreat_2() } else { let way = game.battle.retreat_way let transport = count_retreat_transport(data.ways[way].type) let prov = count_retreat_assets(PROV) if (prov > transport) game.state = "retreat_laden" else retreat_2() } } states.retreat_laden = { inactive: "Retreat", prompt() { let to = game.battle.retreat_to let way = game.battle.retreat_way let transport = count_retreat_transport(data.ways[way].type) let prov = count_retreat_assets(PROV) if (prov > transport) view.prompt = `Retreat: Hindered with ${prov} Provender and ${transport} Transport.` else view.prompt = `Retreat: Unladen.` view.group = game.battle.retreated if (prov > transport) { view.prompt += " Discard Provender." for (let lord of game.battle.retreated) { if (get_lord_assets(lord, PROV) > 0) gen_action_prov(lord) } } else { gen_action_locale(to) view.actions.retreat = 1 } }, prov(lord) { spoil_prov(lord) }, locale(_) { retreat_2() }, retreat() { retreat_2() }, } function retreat_2() { let to = game.battle.retreat_to let way = game.battle.retreat_way if (data.ways[way].name) log(`Retreated via W${way} to %${to}.`) else log(`Retreated to %${to}.`) for (let lord of game.battle.retreated) set_lord_locale(lord, to) if (is_trade_route(to)) conquer_trade_route(to) game.battle.retreat_to = 0 game.battle.retreat_way = 0 end_retreat() } // === ENDING THE BATTLE: REMOVE === function goto_battle_remove() { if (count_unbesieged_friendly_lords(game.battle.where) > 0) game.state = "battle_remove" else goto_battle_losses_loser() } states.battle_remove = { inactive: "Remove Lords", prompt() { view.prompt = "Battle: Remove losing Lords who cannot Retreat or Withdraw." let here = game.battle.where for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here && is_lord_unbesieged(lord)) gen_action_lord(lord) }, lord(lord) { transfer_assets_except_ships(lord) if (can_ransom_lord_battle(lord)) { goto_ransom(lord) } else { disband_lord(lord, true) goto_battle_remove() } }, } function end_ransom_battle_remove() { goto_battle_remove() } // === ENDING THE BATTLE: LOSSES === function has_battle_losses() { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (lord_has_routed_units(lord)) return true return false } function goto_battle_losses_loser() { clear_undo() set_active_loser() game.who = NOBODY if (has_battle_losses()) if (game.active === P1) log_h4("Teutonic Losses") else log_h4("Russian Losses") resume_battle_losses() } function goto_battle_losses_victor() { clear_undo() set_active_victor() game.who = NOBODY if (has_battle_losses()) if (game.active === P1) log_h4("Teutonic Losses") else log_h4("Russian Losses") resume_battle_losses() } function resume_battle_losses() { game.state = "battle_losses" if (!has_battle_losses()) goto_battle_losses_remove() } function action_losses(lord, type) { let protection = FORCE_PROTECTION[type] let evade = FORCE_EVADE[type] let target = Math.max(protection, evade) // Losers in a Battle roll vs 1 if they did not concede if (game.active === game.battle.loser && did_not_concede()) // unless they withdrow if (is_lord_unbesieged(lord)) target = 1 if (game.who !== lord) { log(`L${lord}`) game.who = lord } let die = roll_die() if (die <= target) { logi(`${FORCE_TYPE_NAME[type]} ${range(target)}: ${MISS[die]}`) add_lord_routed_forces(lord, type, -1) add_lord_forces(lord, type, 1) } else { logi(`${FORCE_TYPE_NAME[type]} ${range(target)}: ${HIT[die]}`) add_lord_routed_forces(lord, type, -1) } resume_battle_losses() } states.battle_losses = { inactive: "Losses", prompt() { view.prompt = "Losses: Determine the fate of your Routed units." for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (is_lord_on_map(lord) && lord_has_routed_units(lord)) { if (get_lord_routed_forces(lord, KNIGHTS) > 0) gen_action_routed_knights(lord) if (get_lord_routed_forces(lord, SERGEANTS) > 0) gen_action_routed_sergeants(lord) if (get_lord_routed_forces(lord, LIGHT_HORSE) > 0) gen_action_routed_light_horse(lord) if (get_lord_routed_forces(lord, ASIATIC_HORSE) > 0) gen_action_routed_asiatic_horse(lord) if (get_lord_routed_forces(lord, MEN_AT_ARMS) > 0) gen_action_routed_men_at_arms(lord) if (get_lord_routed_forces(lord, MILITIA) > 0) gen_action_routed_militia(lord) if (get_lord_routed_forces(lord, SERFS) > 0) gen_action_routed_serfs(lord) } } }, routed_knights(lord) { action_losses(lord, KNIGHTS) }, routed_sergeants(lord) { action_losses(lord, SERGEANTS) }, routed_light_horse(lord) { action_losses(lord, LIGHT_HORSE) }, routed_asiatic_horse(lord) { action_losses(lord, ASIATIC_HORSE) }, routed_men_at_arms(lord) { action_losses(lord, MEN_AT_ARMS) }, routed_militia(lord) { action_losses(lord, MILITIA) }, routed_serfs(lord) { action_losses(lord, SERFS) }, } // === ENDING THE BATTLE: LOSSES (REMOVE LORDS) === function goto_battle_losses_remove() { game.state = "battle_losses_remove" for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (is_lord_on_map(lord) && !lord_has_unrouted_units(lord)) return end_battle_losses_remove() } function end_battle_losses_remove() { game.who = NOBODY if (game.active === game.battle.loser) goto_battle_service() else goto_battle_spoils() } states.battle_losses_remove = { inactive: "Remove Lords", prompt() { view.prompt = "Losses: Remove Lords who lost all their Forces." for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (is_lord_on_map(lord) && !lord_has_unrouted_units(lord)) gen_action_lord(lord) }, lord(lord) { set_delete(game.battle.retreated, lord) if (game.active === game.battle.loser) transfer_assets_except_ships(lord) if (can_ransom_lord_battle(lord)) { goto_ransom(lord) } else { disband_lord(lord, true) goto_battle_losses_remove() } }, } function end_ransom_battle_losses_remove() { goto_battle_losses_remove() } // === ENDING THE BATTLE: SPOILS (VICTOR) === function log_spoils() { if (game.spoils[PROV] > 0) logi(game.spoils[PROV] + " Provender") if (game.spoils[COIN] > 0) logi(game.spoils[COIN] + " Coin") if (game.spoils[CART] > 0) logi(game.spoils[CART] + " Cart") if (game.spoils[SHIP] > 0) logi(game.spoils[SHIP] + " Ship") } function find_lone_friendly_lord_at(loc) { let who = NOBODY let n = 0 for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === loc) { who = lord ++n } } if (n === 1) return who return NOBODY } function goto_battle_spoils() { if (has_any_spoils() && has_friendly_lord(game.battle.where)) { log_h4("Spoils") log_spoils() game.state = "battle_spoils" game.who = find_lone_friendly_lord_at(game.battle.where) } else { end_battle_spoils() } } function end_battle_spoils() { game.who = NOBODY game.spoils = 0 goto_battle_aftermath() } states.battle_spoils = { inactive: "Spoils", prompt() { if (has_any_spoils()) { view.prompt = "Spoils: Divide " + list_spoils() + "." let here = game.battle.where for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here) prompt_select_lord(lord) if (game.who !== NOBODY) prompt_spoils() } else { view.prompt = "Spoils: All done." view.actions.end_spoils = 1 } }, lord: action_select_lord, take_prov: take_spoils_prov, take_coin: take_spoils_coin, take_cart: take_spoils_cart, end_spoils() { clear_undo() end_battle_spoils() }, } // === ENDING THE BATTLE: SERVICE (LOSER) === function goto_battle_service() { if (game.battle.retreated) { log_h4("Service") resume_battle_service() } else { end_battle_service() } } function resume_battle_service() { if (game.battle.retreated.length > 0) game.state = "battle_service" else end_battle_service() } states.battle_service = { inactive: "Service", prompt() { view.prompt = "Battle: Roll to shift Service of each Retreated Lord." for (let lord of game.battle.retreated) gen_action_service_bad(lord) }, service_bad(lord) { let die = roll_die() if (die <= 2) add_lord_service(lord, -1) else if (die <= 4) add_lord_service(lord, -2) else if (die <= 6) add_lord_service(lord, -3) log(`L${lord} ${HIT[die]}, shifted to ${get_lord_service(lord)}.`) set_delete(game.battle.retreated, lord) resume_battle_service() }, } function end_battle_service() { goto_battle_losses_victor() } // === ENDING THE BATTLE: AFTERMATH === function goto_battle_aftermath() { set_active(game.battle.attacker) // Events discard_events("hold") // Recovery spend_all_actions() if (check_campaign_victory()) return // Siege/Conquest if (game.march) { game.battle = 0 march_with_group_3() } else { game.battle = 0 resume_command() } } // === CAMPAIGN: FEED === function can_feed_from_shared(lord) { let loc = get_lord_locale(lord) return get_shared_assets(loc, PROV) > 0 } function has_friendly_lord_who_must_feed() { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (is_lord_unfed(lord)) return true return false } function can_lord_use_hillforts(lord) { return is_lord_unfed(lord) && is_lord_unbesieged(lord) && is_in_livonia(get_lord_locale(lord)) } function can_use_hillforts() { if (game.active === TEUTONS) { if (has_global_capability(AOW_TEUTONIC_HILLFORTS)) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (can_lord_use_hillforts(lord)) return true } } return false } function goto_feed() { log_br() // Count how much food each lord needs for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_moved(lord)) { if (count_lord_all_forces(lord) >= 7) set_lord_unfed(lord, 2) else set_lord_unfed(lord, 1) } else { set_lord_unfed(lord, 0) } } if (has_friendly_lord_who_must_feed()) { if (can_use_hillforts()) game.state = "hillforts" else game.state = "feed" } else { end_feed() } } states.hillforts = { inactive: "Hillforts", prompt() { view.prompt = "Hillforts: Skip Feed of one Unbesieged Lord in Livonia." for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (can_lord_use_hillforts(lord)) gen_action_lord(lord) }, lord(lord) { push_undo() log(`C${AOW_TEUTONIC_HILLFORTS} skipped L${lord}.`) feed_lord_skip(lord) if (has_friendly_lord_who_must_feed()) game.state = "feed" else end_feed() }, } states.feed = { inactive: "Feed", prompt() { view.prompt = "Feed: You must Feed Lords who Moved or Fought." let done = true prompt_held_event() // Feed from own mat if (done) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (is_lord_unfed(lord)) { if (get_lord_assets(lord, PROV) > 0) { gen_action_prov(lord) done = false } } } } // Sharing if (done) { view.prompt = "Feed: You must Feed Lords with Shared Loot or Provender." for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (is_lord_unfed(lord) && can_feed_from_shared(lord)) { gen_action_lord(lord) done = false } } } // Unfed if (done) { view.prompt = "Feed: You must shift the Service of any Unfed Lords." for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (is_lord_unfed(lord)) { gen_action_service_bad(lord) done = false } } } // All done! if (done) { view.prompt = "Feed: All done." view.actions.end_feed = 1 } }, prov(lord) { push_undo() add_lord_assets(lord, PROV, -1) feed_lord(lord) }, lord(lord) { push_undo() game.who = lord game.state = "feed_lord_shared" }, service_bad(lord) { push_undo() add_lord_service(lord, -1) log(`Unfed L${lord} to ${get_lord_service(lord)}.`) set_lord_unfed(lord, 0) }, end_feed() { push_undo() end_feed() }, card: action_held_event, } function resume_feed_lord_shared() { if (!is_lord_unfed(game.who) || !can_feed_from_shared(game.who)) { game.who = NOBODY game.state = "feed" } } states.feed_lord_shared = { inactive: "Feed", prompt() { view.prompt = `Feed: You must Feed ${lord_name[game.who]} with Shared Loot or Provender.` let loc = get_lord_locale(game.who) for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === loc) { if (get_lord_assets(lord, PROV) > 0) gen_action_prov(lord) } } }, prov(lord) { push_undo() add_lord_assets(lord, PROV, -1) feed_lord(game.who) resume_feed_lord_shared() }, } function end_feed() { goto_pay() } // === LEVY & CAMPAIGN: PAY === function can_pay_lord(lord) { if (get_lord_service(lord) > 16) return false if (game.active === RUSSIANS) { if (game.pieces.veche_coin > 0 && is_lord_unbesieged(lord)) return true } let loc = get_lord_locale(lord) if (get_shared_assets(loc, COIN) > 0) return true return false } function has_friendly_lord_who_may_be_paid() { if (game.active === TEUTONS) { // Open a window to play Heinrich sees the Curia. if (could_play_card(EVENT_TEUTONIC_HEINRICH_SEES_THE_CURIA) && can_play_heinrich_sees_the_curia()) return true } for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (is_lord_on_map(lord) && can_pay_lord(lord)) return true return false } function goto_pay() { log_br() game.state = "pay" game.who = NOBODY if (!has_friendly_lord_who_may_be_paid()) end_pay() } function resume_pay() { if (!can_pay_lord(game.who)) game.who = NOBODY } states.pay = { inactive: "Pay", prompt() { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (is_lord_on_map(lord) && can_pay_lord(lord)) { prompt_select_lord(lord) prompt_select_service(lord) } } prompt_held_event() if (game.who === NOBODY) { view.prompt = "Pay: You may Pay your Lords." } else { let here = get_lord_locale(game.who) view.prompt = `Pay: You may Pay ${lord_name[game.who]} with Coin.` if (game.active === RUSSIANS) { if (game.pieces.veche_coin > 0 && is_lord_unbesieged(game.who)) view.actions.veche_coin = 1 } for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_locale(lord) === here) { if (get_lord_assets(lord, COIN) > 0) gen_action_coin(lord) } } } view.actions.end_pay = 1 }, lord: action_select_lord, service: action_select_lord, coin(lord) { push_undo_without_who() log(`Paid L${game.who}.`) add_lord_assets(lord, COIN, -1) add_lord_service(game.who, 1) resume_pay() }, veche_coin() { push_undo_without_who() log(`Paid L${game.who} from Veche.`) game.pieces.veche_coin-- add_lord_service(game.who, 1) resume_pay() }, end_pay() { push_undo_without_who() end_pay() }, card: action_held_event, } function end_pay() { // NOTE: We can combine Pay & Disband steps because disband is mandatory only. game.who = NOBODY goto_disband() } // === LEVY & CAMPAIGN: DISBAND === function has_friendly_lord_who_must_disband() { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (is_lord_on_map(lord) && get_lord_service(lord) <= current_turn()) return true return false } function goto_disband() { game.state = "disband" if (!has_friendly_lord_who_must_disband()) end_disband() } function disband_lord(lord, permanently = false) { let here = get_lord_locale(lord) let turn = current_turn() if (permanently) { log(`Removed L${lord}.`) set_lord_locale(lord, NOWHERE) set_lord_service(lord, NEVER) } else if (get_lord_service(lord) < current_turn()) { log(`Disbanded L${lord} beyond Service limit.`) set_lord_locale(lord, NOWHERE) set_lord_service(lord, NEVER) } else { if (is_levy_phase()) set_lord_cylinder_on_calendar(lord, turn + data.lords[lord].service) else set_lord_cylinder_on_calendar(lord, turn + data.lords[lord].service + 1) set_lord_service(lord, NEVER) log(`Disbanded L${lord} to ${get_lord_calendar(lord)}.`) } if (game.scenario === "Pleskau" || game.scenario === "Pleskau (Quickstart)") { if (is_russian_lord(lord)) game.pieces.elr1 ++ else game.pieces.elr2 ++ } remove_lieutenant(lord) // Smerdi - serfs go back to card game.pieces.smerdi += get_lord_forces(lord, SERFS) discard_lord_capability_n(lord, 0) discard_lord_capability_n(lord, 1) game.pieces.assets[lord] = 0 game.pieces.forces[lord] = 0 game.pieces.routed[lord] = 0 set_lord_besieged(lord, 0) set_lord_moved(lord, 0) for (let v of data.lords[lord].vassals) game.pieces.vassals[v] = VASSAL_UNAVAILABLE } states.disband = { inactive: "Disband", prompt() { view.prompt = "Disband: You must Disband Lords at their Service limit." prompt_held_event() let done = true for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (is_lord_on_map(lord) && get_lord_service(lord) <= current_turn()) { gen_action_lord(lord) gen_action_service_bad(lord) done = false } } if (done) view.actions.end_disband = 1 }, service_bad(lord) { this.lord(lord) }, lord(lord) { if (is_lord_besieged(lord) && can_ransom_lord_siege(lord)) { clear_undo() goto_ransom(lord) } else { push_undo() disband_lord(lord) } }, end_disband() { end_disband() }, card: action_held_event, } function end_ransom_disband() { // do nothing } function end_disband() { clear_undo() if (is_campaign_phase()) { if (check_campaign_victory()) return } set_active_enemy() if (is_campaign_phase()) { if (is_active_command()) goto_remove_markers() else goto_feed() } else { if (game.active === P1) goto_levy_muster() else goto_feed() } } // === LEVY & CAMPAIGN: RANSOM === function enemy_has_ransom() { if (game.active === TEUTONS) return has_global_capability(AOW_RUSSIAN_RANSOM) else return has_global_capability(AOW_TEUTONIC_RANSOM) } function can_ransom_lord_siege(lord) { return enemy_has_ransom() && has_enemy_lord(get_lord_locale(lord)) } function has_enemy_lord_in_battle() { for (let lord = first_enemy_lord; lord <= last_enemy_lord; ++lord) if (get_lord_moved(lord)) return true return false } function can_ransom_lord_battle() { return enemy_has_ransom() && has_enemy_lord_in_battle() } function goto_ransom(lord) { clear_undo() set_active_enemy() push_state("ransom") game.who = lord game.count = data.lords[lord].service if (is_teutonic_lord(lord)) log(`L${lord} C${AOW_RUSSIAN_RANSOM}.`) else log(`L${lord} C${AOW_TEUTONIC_RANSOM}.`) } function end_ransom() { let here = get_lord_locale(game.who) if (game.battle) disband_lord(game.who, true) else disband_lord(game.who, false) pop_state() set_active_enemy() switch (game.state) { case "disband": return end_ransom_disband() case "sack": return end_ransom_sack() case "battle_remove": return end_ransom_battle_remove() case "battle_losses_remove": return end_ransom_battle_losses_remove() } } states.ransom = { inactive: "Ransom", prompt() { if (game.active === TEUTONS) view.prompt = `Ransom ${lord_name[game.who]}: Add ${game.count} Coin to a Teutonic Lord.` else view.prompt = `Ransom ${lord_name[game.who]}: Add ${game.count} Coin to a Russian Lord.` if (game.battle) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_fought(lord)) gen_action_lord(lord) } else { let here = get_lord_locale(game.who) for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (get_lord_locale(lord) === here) gen_action_lord(lord) } }, lord(lord) { add_lord_assets(lord, COIN, game.count) end_ransom() }, } // === CAMPAIGN: REMOVE MARKERS === function goto_remove_markers() { clear_lords_moved() goto_command_activation() } // === END CAMPAIGN: GROWTH === function count_enemy_ravaged() { let n = 0 for (let loc of game.pieces.ravaged) if (is_friendly_territory(loc)) ++n return n } function goto_grow() { game.count = count_enemy_ravaged() >> 1 log_br() if (game.active === TEUTONS) log("Teutonic Growth") else log("Russian Growth") if (game.count === 0) { logi("Nothing") end_growth() } else { game.state = "growth" } } function end_growth() { set_active_enemy() if (game.active === P2) goto_grow() else goto_game_end() } states.growth = { inactive: "Grow", prompt() { view.prompt = `Grow: Remove ${game.count} enemy Ravaged markers.` if (game.count > 0) { for (let loc of game.pieces.ravaged) if (is_friendly_territory(loc)) gen_action_locale(loc) } else { view.actions.end_growth = 1 } }, locale(loc) { push_undo() logi(`%${loc}`) remove_ravaged_marker(loc) game.count-- }, end_growth() { clear_undo() end_growth() }, } // === END CAMPAIGN: GAME END === function check_campaign_victory_p1() { for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord) if (is_lord_on_map(lord)) return false return true } function check_campaign_victory_p2() { for (let lord = first_p1_lord; lord <= last_p1_lord; ++lord) if (is_lord_on_map(lord)) return false return true } function check_campaign_victory() { if (check_campaign_victory_p1()) { goto_game_over(P1, `${P1} won a Campaign Victory!`) return true } if (check_campaign_victory_p2()) { goto_game_over(P2, `${P2} won a Campaign Victory!`) return true } return false } function goto_end_campaign() { log_h1("End Campaign") set_active(P1) if (current_turn() === 8 || current_turn() === 16) { goto_grow() } else { goto_game_end() } } function count_vp1() { let vp = game.pieces.elr1 << 1 vp += game.pieces.castles1.length << 1 for (let loc of game.pieces.conquered) if (is_p2_locale(loc)) vp += data.locales[loc].vp << 1 for (let loc of game.pieces.ravaged) if (is_p2_locale(loc)) vp += 1 return vp } function count_vp2() { let vp = game.pieces.elr2 << 1 vp += game.pieces.veche_vp << 1 vp += game.pieces.castles2.length << 1 for (let loc of game.pieces.conquered) if (is_p1_locale(loc)) vp += data.locales[loc].vp << 1 for (let loc of game.pieces.ravaged) if (is_p1_locale(loc)) vp += 1 return vp } function goto_game_end() { // GAME END if (current_turn() === scenario_last_turn[game.scenario]) { let vp1 = count_vp1() let vp2 = count_vp2() if (game.scenario === "Watland") { if (vp1 < 14) goto_game_over(P2, `Russians won \u2014 Teutons had less than 7 VP.`) else if (vp1 < vp2 * 2) goto_game_over(P2, `Russians won \u2014 Teutons had less than double Russian VP.`) else goto_game_over(P1, `Teutons won with ${frac(vp1)} VP vs ${frac(vp2)} VP.`) return } if (vp1 > vp2) goto_game_over(P1, `${P1} won with ${frac(vp1)} VP vs ${frac(vp2)} VP.`) else if (vp2 > vp1) goto_game_over(P2, `${P2} won with ${frac(vp2)} VP vs ${frac(vp1)} VP.`) else goto_game_over("Draw", "The game ended in a draw.") } else { goto_plow_and_reap() } } // === END CAMPAIGN: PLOW AND REAP === function goto_plow_and_reap() { let turn = current_turn() end_plow_and_reap() } function end_plow_and_reap() { goto_wastage() } // === END CAMPAIGN: WASTAGE === function goto_wastage() { clear_lords_moved() let done = true for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (check_lord_wastage(lord)) { set_lord_moved(lord, 3) done = false } } if (done) end_wastage() else game.state = "wastage" } function check_lord_wastage(lord) { if (get_lord_assets(lord, PROV) > 1) return true if (get_lord_assets(lord, COIN) > 1) return true if (get_lord_assets(lord, CART) > 1) return true if (get_lord_assets(lord, SHIP) > 1) return true if (get_lord_capability(lord, 0) !== NOTHING && get_lord_capability(lord, 1) !== NOTHING) return true return false } function prompt_wastage(lord) { if (get_lord_assets(lord, PROV) > 0) gen_action_prov(lord) if (get_lord_assets(lord, COIN) > 0) gen_action_coin(lord) if (get_lord_assets(lord, CART) > 0) gen_action_cart(lord) if (get_lord_assets(lord, SHIP) > 0) gen_action_ship(lord) for (let i = 0; i < 2; ++i) { let c = get_lord_capability(lord, i) if (c !== NOTHING) gen_action_card(c) } } function action_wastage(lord, type) { push_undo() set_lord_moved(lord, 0) add_lord_assets(lord, type, -1) } function find_lord_with_capability_card(c) { for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) if (lord_has_capability_card(lord, c)) return lord return NOBODY } states.wastage = { inactive: "Wastage", prompt() { let done = true for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) { if (get_lord_moved(lord)) { prompt_wastage(lord) done = false } } if (done) { view.prompt = "Wastage: All done." view.actions.end_wastage = 1 } else { view.prompt = "Wastage: Discard one Asset or Capability from each affected Lord." } }, card(c) { push_undo() let lord = find_lord_with_capability_card(c) set_lord_moved(lord, 0) discard_lord_capability(lord, c) }, prov(lord) { action_wastage(lord, PROV) }, coin(lord) { action_wastage(lord, COIN) }, cart(lord) { action_wastage(lord, CART) }, ship(lord) { action_wastage(lord, SHIP) }, end_wastage() { end_wastage() }, } function end_wastage() { push_undo() goto_reset() } // === END CAMPAIGN: RESET (DISCARD ARTS OF WAR) === function goto_reset() { game.state = "reset" // Unstack Lieutenants and Lower Lords for (let lord = first_friendly_lord; lord <= last_friendly_lord; ++lord) remove_lieutenant(lord) // Remove all Serfs to the Smerdi card if (game.active === RUSSIANS) { for (let lord = first_p2_lord; lord <= last_p2_lord; ++lord) set_lord_forces(lord, SERFS, 0) if (has_global_capability(AOW_RUSSIAN_SMERDI)) game.pieces.smerdi = 6 else game.pieces.smerdi = 0 } // Discard "This Campaign" events from play. discard_friendly_events("this_campaign") } states.reset = { inactive: "Reset", prompt() { view.prompt = "Reset: You may discard any Arts of War cards desired." if (game.active === P1) { for (let c = first_p1_card; c <= last_p1_card; ++c) if (can_discard_card(c)) gen_action_card(c) } if (game.active === P2) { for (let c = first_p2_card; c <= last_p2_card; ++c) if (can_discard_card(c)) gen_action_card(c) } view.actions.end_discard = 1 }, card(c) { push_undo() if (has_global_capability(c)) { log(`Discarded C${c}.`) discard_global_capability(c) } else if (set_has(game.hand1, c)) { log("Discarded Held card.") set_delete(game.hand1, c) } else if (set_has(game.hand2, c)) { log("Discarded Held card.") set_delete(game.hand2, c) } else { let lord = find_lord_with_capability_card(c) if (lord !== NOBODY) { discard_lord_capability(lord, c) } } }, end_discard() { end_reset() }, } function end_reset() { clear_undo() set_active_enemy() if (game.active === P2) goto_plow_and_reap() else goto_advance_campaign() } // === END CAMPAIGN: RESET (ADVANCE CAMPAIGN) === function goto_advance_campaign() { game.turn++ log_h1("Levy " + current_turn_name()) // First turns of late winter if (current_turn() === 5 || current_turn() === 13) goto_discard_crusade_late_winter() else goto_levy_arts_of_war() } // === GAME OVER === function goto_game_over(result, victory) { game.state = "game_over" game.active = "None" game.result = result game.victory = victory log_h1("Game Over") 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 logevent(cap) { game.log.push(`E${cap}.`) } function logcap(cap) { game.log.push(`C${cap}.`) } function logi(msg) { game.log.push(">" + msg) } function logii(msg) { game.log.push(">>" + msg) } function log_h1(msg) { log_br() log(".h1 " + msg) log_br() } function log_h2(msg) { log_br() if (game.active === TEUTONS) log(".h2t " + msg) else log(".h2r " + msg) log_br() } function log_h3(msg) { log_br() if (game.active === TEUTONS) log(".h3t " + msg) else log(".h3r " + msg) log_br() } function log_h4(msg) { log_br() log(".h4 " + msg) } function log_h5(msg) { log_br() log(".h5 " + msg) } function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_card_if_held(c) { if (has_card_in_hand(c)) gen_action_card(c) } function prompt_select_lord_on_calendar(lord) { if (lord !== game.who) { if (is_lord_on_calendar(lord)) gen_action_lord(lord) else gen_action_service(lord) } } function prompt_select_lord(lord) { if (lord !== game.who) gen_action_lord(lord) } function prompt_select_service(lord) { if (lord !== game.who) gen_action_service(lord) } function action_select_lord(lord) { if (game.who === lord) game.who = NOBODY else game.who = lord } function gen_action_calendar(calendar) { if (calendar < 0) calendar = 0 if (calendar > 17) calendar = 17 gen_action("calendar", calendar) } function gen_action_way(way) { gen_action("way", way) } function gen_action_locale(locale) { gen_action("locale", locale) } function gen_action_laden_march(locale) { gen_action("laden_march", locale) } function gen_action_lord(lord) { gen_action("lord", lord) } function gen_action_array(pos) { gen_action("array", pos) } function gen_action_service(service) { gen_action("service", service) } function gen_action_service_bad(service) { gen_action("service_bad", service) } function gen_action_vassal(vassal) { gen_action("vassal", vassal) } function gen_action_card(c) { gen_action("card", c) } function gen_action_plan(lord) { gen_action("plan", lord) } function gen_action_prov(lord) { gen_action("prov", lord) } function gen_action_coin(lord) { gen_action("coin", lord) } function gen_action_cart(lord) { gen_action("cart", lord) } function gen_action_ship(lord) { gen_action("ship", lord) } function gen_action_knights(lord) { gen_action("knights", lord) } function gen_action_sergeants(lord) { gen_action("sergeants", lord) } function gen_action_light_horse(lord) { gen_action("light_horse", lord) } function gen_action_asiatic_horse(lord) { gen_action("asiatic_horse", lord) } function gen_action_men_at_arms(lord) { gen_action("men_at_arms", lord) } function gen_action_militia(lord) { gen_action("militia", lord) } function gen_action_serfs(lord) { gen_action("serfs", lord) } function gen_action_routed_knights(lord) { gen_action("routed_knights", lord) } function gen_action_routed_sergeants(lord) { gen_action("routed_sergeants", lord) } function gen_action_routed_light_horse(lord) { gen_action("routed_light_horse", lord) } function gen_action_routed_asiatic_horse(lord) { gen_action("routed_asiatic_horse", lord) } function gen_action_routed_men_at_arms(lord) { gen_action("routed_men_at_arms", lord) } function gen_action_routed_militia(lord) { gen_action("routed_militia", lord) } function gen_action_routed_serfs(lord) { gen_action("routed_serfs", lord) } const P1_LORD_MASK = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 const P2_LORD_MASK = P1_LORD_MASK << 6 exports.view = function (state, current) { load_state(state) view = { prompt: null, actions: null, log: game.log, reveal: 0, scenario: (scenario_first_turn[game.scenario] << 5) + (scenario_last_turn[game.scenario]), turn: game.turn, events: game.events, pieces: game.pieces, battle: game.battle, held1: game.hand1.length, held2: game.hand2.length, command: game.command, hand: null, plan: null, } if (!game.hidden) view.reveal = -1 if (current === P1) { view.hand = game.hand1 view.plan = game.plan1 if (game.hidden) view.reveal |= P1_LORD_MASK } if (current === P2) { view.hand = game.hand2 view.plan = game.plan2 if (game.hidden) view.reveal |= P2_LORD_MASK } if (game.battle) { if (game.battle.array) { for (let lord of game.battle.array) if (lord !== NOBODY) view.reveal |= (1 << lord) } for (let lord of game.battle.reserves) view.reveal |= (1 << lord) } if (game.state === "game_over") { view.prompt = game.victory } else if (current === "Observer" || (game.active !== current && game.active !== BOTH)) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${game.active} \u2014 ${inactive}.` } else { view.actions = {} view.who = game.who if (states[game.state]) states[game.state].prompt(current) 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, current) } else { if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } return game } exports.is_checkpoint = function (a, b) { return a.turn !== b.turn } // === COMMON TEMPLATE === // Packed array of small numbers in one word function pack1_get(word, n) { return (word >>> n) & 1 } function pack2_get(word, n) { n = n << 1 return (word >>> n) & 3 } 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 pack2_set(word, n, x) { n = n << 1 return (word & ~(3 << n)) | (x << n) } function pack4_set(word, n, x) { n = n << 2 return (word & ~(15 << n)) | (x << n) } // === COMMON LIBRARY === function clear_undo() { if (game.undo.length > 0) game.undo = [] } function push_undo_without_who() { let save_who = game.who game.who = NOBODY push_undo() game.who = save_who } 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 = object_copy(v) copy[k] = v } game.undo.push(copy) } function pop_undo() { let save_log = game.log let save_undo = game.undo game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_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 } // Fast deep copy for objects without cycles function object_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] = object_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] = object_copy(v) else copy[i] = v } return copy } } // Array remove and insert (faster than splice) function array_remove_item(array, item) { let n = array.length for (let i = 0; i < n; ++i) if (array[i] === item) return array_remove(array, i) } function array_remove(array, index) { let n = array.length for (let i = index + 1; i < n; ++i) array[i - 1] = array[i] array.length = n - 1 } function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function array_remove_pair(array, index) { let n = array.length for (let i = index + 2; i < n; ++i) array[i - 2] = array[i] array.length = n - 2 } function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i - 2] array[i + 1] = array[i - 1] } array[index] = key array[index + 1] = value } // Set as plain sorted array function set_has(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 true } return false } 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 } array_insert(set, a, item) } function set_delete(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 } } } 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) } // Map as plain sorted array of key/value pairs function map_has(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 return true } return false } function map_get(map, key, missing) { 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 return map[(m << 1) + 1] } return missing } function map_set(map, key, value) { 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 { map[(m << 1) + 1] = value return } } array_insert_pair(map, a << 1, key, value) } function map_delete(map, item) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m << 1] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove_pair(map, m << 1) return } } }