"use strict" const data = require("./data.js") function find_scenario(n) { let ix = data.scenarios.findIndex(s => s.number === n) if (ix < 0) throw new Error("cannot find scenario " + n) return ix } function find_card(s, n) { let ix = data.cards.findIndex(c => c.scenario === s && c.name === n) if (ix < 0) throw new Error("cannot find card " + n) return ix } // for (let c of data.cards) for (let a of c.actions) console.log(a.type + ":", a.effect) // for (let c of data.cards) { if (c.rule_text_1) console.log(c.rule_text_1); if (c.rule_text_2) console.log(c.rule_text_2) } // for (let c of data.cards) console.log(c.dice) // for (let c of data.cards) for (let a of c.actions) { if (a.type === "Counterattack") console.log(c.number, a.type, a.sequence, a.target) } function check_attack_res(c, a) { if (a.choice) return if (c.rules && c.rules["attack_reserve"]) return if (c.rules && c.rules["ignore_reserve"]) return let dead = [] let last = a.target_list[a.target_list.length-1] for (let tid of a.target_list) { let t = data.cards[tid] if (!t.reserve) { // all good, targetable out of reserve dead.push(tid) } else if (Array.isArray(t.reserve)) { // in reserve for (let rid of t.reserve) if (!dead.includes(rid) && tid !== last) console.log("BLOCK (RES)", c.scenario, c.number, a.target, "(" + t.name + ")", "\n\t" + c.rule_text_1) dead.push(tid) } else { // commanded? if (tid !== last) console.log("BLOCK (CMD)", c.scenario, c.number, a.target, "(" + t.name + ")", "\n\t" + c.rule_text_1) } } } //for (let c of data.cards) for (let a of c.actions) { if (a.type === "Attack") check_attack_res(c, a) } const P1 = "First" const P2 = "Second" var states = {} var game = null var view = null const POOL = -1 const RED = 0 const PINK = 1 const BLUE = 2 const DKBLUE = 3 exports.scenarios = { "": [ "Random" ], } const scenario_roles = {} for (let s of data.scenarios) { let id = s.number + " - " + s.name let x = s.expansion if (!(x in exports.scenarios)) { exports.scenarios[""].push("Random - " + x) exports.scenarios[x] = [] } exports.scenarios[x].push(id) scenario_roles[id] = [ s.players[0].name, s.players[1].name ] } exports.is_random_scenario = function (scenario) { return scenario.startsWith("Random") } exports.select_random_scenario = function (scenario, seed) { if (scenario === "Random") { let info = data.scenarios[seed % data.scenarios.length] return info.number + " - " + info.name } if (scenario.startsWith("Random - ")) { let list = exports.scenarios[scenario.replace("Random - ", "")] return list[seed % list.length] } return scenario } exports.roles = [ P1, P2 ] exports.action = function (state, player, action, arg) { game = state let S = states[game.state] if (action in S) S[action](arg, player) else if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) return game } // JSON Schema for view data exports.VIEW_SCHEMA = { type: "object", properties: { log: { type: "array", items: { type: "string" } }, prompt: { type: "string" }, scenario: { type: "integer" }, dice: { type: "array", minItems: 24, maxItems: 24, items: { type: "integer", minimum: -1 } }, sticks: { type: "array", items: { type: "integer", minimum: 0 } }, cubes: { type: "array", items: { type: "integer", minimum: 0 } }, morale: { type: "array", minItems: 2, maxItems: 2, items: { type: "integer", minimum: 0 } }, tv1: { type: "integer", minimum: 0 }, tv2: { type: "integer", minimum: 0 }, front: { type: "array", items: { type: "array", items: { type: "integer", minimum: 0 } } }, reserve: { type: "array", items: { type: "array", items: { type: "integer", minimum: 0 } } }, selected: { type: "integer", minimum: -1 }, target: { type: "integer", minimum: -1 }, hits: { type: "integer", minimum: 0 }, self: { type: "integer", minimum: 0 }, shift: { type: "array", items: { type: "integer", minimum: 0 } }, target2: { type: "integer", minimum: 0 }, hits2: { type: "integer", minimum: 0 }, actions: { type: "object" }, }, required: [ "log", "prompt", "scenario", "dice", "sticks", "cubes", "morale", "tv1", "tv2", "front", "reserve", "selected", "target", "hits", "self", ], additionalProperties: false, } exports.view = function (state, player) { game = state view = { log: game.log, prompt: null, scenario: game.scenario, dice: game.dice, sticks: game.sticks, cubes: game.cubes, morale: game.morale, tv1: get_tactical_victory_points(0), tv2: get_tactical_victory_points(1), front: game.front, reserve: game.reserve, selected: game.selected, target: game.target, hits: game.hits, self: game.self, } if (game.target2 >= 0 && game.hits2 >= 0) { view.target2 = game.target2 view.hits2 = game.hits2 } if (game.shift) view.shift = game.shift if (game.state === "game_over") { view.prompt = game.victory } else if (player !== game.active) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${player_name(player_index())} to ${inactive}.` } else { view.actions = {} states[game.state].prompt() if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } return view } function goto_game_over(result, victory) { if (result === P1) victory = player_name(0) + " won:\n" + victory else if (result === P2) victory = player_name(1) + " won:\n" + victory else victory = result + ":\n" + victory game.state = "game_over" game.active = "None" game.result = result game.victory = victory log("") log(victory) return true } states.game_over = { prompt() { view.prompt = game.victory }, } // === SPECIAL RULES - CARD NUMBERS === const S2_MARSTON_MOOR = find_scenario(2) const S2_RUPERTS_LIFEGUARD = find_card(2, "Rupert's Lifeguard") const S2_NORTHERN_HORSE = find_card(2, "Northern Horse") const S2_TILLIERS_LEFT = find_card(2, "Tillier's Left") const S2_TILLIERS_RIGHT = find_card(2, "Tillier's Right") const S2_BYRON = find_card(2, "Byron") const S4_BOSWORTH_FIELD = find_scenario(4) const S4_THE_STANLEYS = find_card(4, "The Stanleys") const S4_NORTHUMBERLAND = find_card(4, "Northumberland") const S7_THE_DUNES = find_scenario(7) const S7_THE_ENGLISH_FLEET = find_card(7, "The English Fleet") const S7_DON_JUAN_JOSE = find_card(7, "Don Juan Jose") const S7_SPANISH_RIGHT_CAVALRY = find_card(7, "Spanish Right Cavalry") const S8_BROOKLYN_HEIGHTS = find_scenario(8) const S8_CLINTON = find_card(8, "Clinton") const S8_GRANT = find_card(8, "Grant") const S8_HESSIANS = find_card(8, "Hessians") const S36_FOURTH_LINE = find_card(36, "The Fourth Line") const S3201_GAINES_MILL = find_scenario(3201) const S3201_JACKSON = find_card(3201, "Jackson") const S3201_DH_HILL = find_card(3201, "D.H. Hill") const S3201_AP_HILL = find_card(3201, "A.P. Hill") const S3201_LONGSTREET = find_card(3201, "Longstreet") const S9_ST_ALBANS = find_scenario(9) const S9_HENRY_VI = find_card(9, "Henry VI") const S9_SHROPSHIRE_LANE = find_card(9, "Shropshire Lane") const S9_SOPWELL_LANE = find_card(9, "Sopwell Lane") const S9_ARCHERS = find_card(9, "Archers") const S11_MORTIMERS_CROSS = find_scenario(11) const S12_TOWTON = find_scenario(12) const S13_EDGECOTE_MOOR = find_scenario(13) const S15_TEWKESBURY = find_scenario(15) const S15_A_PLUMP_OF_SPEARS = find_card(15, "A Plump of Spears") const S15_SOMERSET = find_card(15, "Somerset") const S15_WENLOCK = find_card(15, "Wenlock") const S14_BARNET = find_scenario(14) const S14_TREASON = find_card(14, "\"Treason!\"") const S16_STOKE_FIELD = find_scenario(16) const S22_GABIENE = find_scenario(22) const S22_EUMENES_CAMP = find_card(22, "Eumenes's Camp") const S22_SILVER_SHIELDS = find_card(22, "The Silver Shields") const S25_WHEATFIELD = find_scenario(25) const S25_STONY_HILL = find_card(25, "Stony Hill") const S25_WOFFORD = find_card(25, "Wofford") const S25_ZOOK = find_card(25, "Zook") const S25_KELLY = find_card(25, "Kelly") const S26_PEACH_ORCHARD = find_scenario(26) const S26_FATAL_BLUNDER = find_card(26, "Fatal Blunder") const S28_CULPS_HILL = find_scenario(28) const S28_BREASTWORKS = find_card(28, "Breastworks") const S28_GEARY = find_card(28, "Geary") const S29_GETTYS_2ND = find_scenario(29) const S29_MCLAWS = find_card(29, "McLaws") const S29_ANDERSON = find_card(29, "Anderson") const S29_HOOD = find_card(29, "Hood") const S29_EARLY = find_card(29, "Early") const S29_JOHNSON = find_card(29, "Johnson") const S29_MEADE = find_card(29, "Meade") const S29_LITTLE_ROUND_TOP = find_card(29, "Little Round Top") const S30_EDGEHILL = find_scenario(30) const S30_BALFOUR = find_card(30, "Balfour") const S30_STAPLETON = find_card(30, "Stapleton") const S30_RUPERT = find_card(30, "Rupert of the Rhine") const S30_WILMOT = find_card(30, "Wilmot") const S30_ESSEX = find_card(30, "Charles Essex") const S31_NEWBURY_1ST = find_scenario(31) const S31_BYRON = find_card(31, "Byron") const S31_SKIPPON = find_card(31, "Skippon") const S31_WENTWORTH = find_card(31, "Wentworth") const S31_GERARD = find_card(31, "Gerard") const S34_TULLIBARDINE = find_card(34, "Tullibardine") const S35_AULDEARN = find_scenario(35) const S35_MONTROSE = find_card(35, "Montrose") const S35_GORDON = find_card(35, "Gordon") const S37_INKERMAN = find_scenario(37) const S37_PAULOFFS_LEFT = find_card(37, "Pauloff's Left") const S37_BRITISH_TROOPS = find_card(37, "British Troops") const S37_FRENCH_TROOPS = find_card(37, "French Troops") const S37_SOIMONOFF = find_card(37, "Soimonoff") const S37_THE_FOG = find_card(37, "The Fog") const S38_FLEURUS = find_scenario(38) const S38_WALDECK = find_card(38, "Waldeck") const S38_RETREAT_TO_NIVELLES = find_card(38, "Retreat to Nivelles") const S38_LUXEMBOURGS_HORSE = find_card(38, "Luxembourg's Horse") const S38_GOURNAYS_HORSE = find_card(38, "Gournay's Horse") const S38_DUTCH_LEFT_FOOT = find_card(38, "Dutch Left Foot") const S38_DUTCH_HORSE = find_card(38, "Dutch Horse") const S39_MARSAGLIA = find_scenario(39) const S39_CANNONS = find_card(39, "Cannons") const S39_EUGENE = find_card(39, "Eugene") const S39_DUKE_OF_SAVOY = find_card(39, "Duke of Savoy") const S39_BAYONETS = find_card(39, "Bayonets!") const S39_CATINAT = find_card(39, "Catinat") const S39_HOGUETTE = find_card(39, "Hoguette") const S40_CHIARI = find_scenario(40) const S40_CASSINES_I = find_card(40, "Cassines I") const S40_NIGRELLI = find_card(40, "Nigrelli") const S40_KRIECHBAUM = find_card(40, "Kriechbaum") const S40_CASSINES_II = find_card(40, "Cassines II") const S40_MANNSFELDT = find_card(40, "Mannsfeldt") const S40_GUTTENSTEIN = find_card(40, "Guttenstein") const S41_BLENHEIM_SCENARIO = find_scenario(41) const S41_CLERAMBAULT = find_card(41, "Clerambault") const S41_BLENHEIM_CARD = find_card(41, "Blenheim") const S41_PRINCE_EUGENE = find_card(41, "Prince Eugene") const S42_RAMILLIES = find_scenario(42) const S42_MARLBOROUGH = find_card(42, "Marlborough") const S42_DUTCH_GUARDS = find_card(42, "Dutch Guards") const S43_DENAIN = find_scenario(43) const S43_DUTCH_HORSE = find_card(43, "Dutch Horse") const S43_VILLARS_LEFT = find_card(43, "Villars's Left") const S43_BROGLIE = find_card(43, "Broglie") const S43_PRINCE_DE_TINGRY = find_card(43, "Prince de Tingry") const S44_HOHENFRIEDBERG = find_scenario(44) const S44_CHARLES = find_card(44, "Charles") const S44_FREDERICK_II = find_card(44, "Frederick II") const S44_BAYREUTH_DRAGOONS = find_card(44, "Bayreuth Dragoons") const S44_LEOPOLDS_L = find_card(44, "Leopold's Left") const S44_LEOPOLDS_C = find_card(44, "Leopold's Center") const S44_LEOPOLDS_R = find_card(44, "Leopold's Right") const S44_DU_MOULIN = find_card(44, "Du Moulin") const S44_SAXON_HORSE = find_card(44, "Saxon Horse") const S45_SOOR = find_scenario(45) const S45_AUSTRIAN_GUNS = find_card(45, "Austrian Guns") const S45_CUIRASSIERS = find_card(45, "Cuirassiers") const S46_ROCOUX = find_scenario(46) const S46_AUSTRIANS = find_card(46, "Austrians") const S46_THE_MOUTH_OF_HELL = find_card(46, "The Mouth of Hell") const S47_PRAGUE = find_scenario(47) const S47_BROWNE = find_card(47, "Browne") const S47_SCHWERIN = find_card(47, "Schwerin") const S47_CHARLES_LORRAINE = find_card(47, "Charles Lorraine") const S48_BRESLAU = find_scenario(48) const S48_AUSTRIAN_GUNS = find_card(48, "Austrian Guns") const S48_PRUSSIAN_GUNS = find_card(48, "Prussian Guns") const S48_BEVERN = find_card(48, "Bevern") const S48_GRENZERS = find_card(48, "Grenzers") const S49_LEUTHEN = find_scenario(49) const S49_RETZOW = find_card(49, "Retzow") const S49_COLLOREDO = find_card(49, "Colloredo") const S49_NADASDY = find_card(49, "Nadasdy") const S49_BORNE = find_card(49, "Borne") const S49_FEINT = find_card(49, "Feint") const S49_POOR_CHARLES = find_card(49, "Poor Charles :-(") const S49_THE_LEUTHEN_CHORALE = find_card(49, "The Leuthen Chorale") // === SETUP === exports.setup = function (seed, scenario, options) { scenario = parseInt(scenario) scenario = data.scenarios.findIndex(s => s.number === scenario) if (scenario < 0) throw Error("cannot find scenario: " + scenario) let info = data.scenarios[scenario] game = { seed: seed, scenario: scenario, log: [], undo: [], active: P1, state: "roll", reacted: -1, // dice value and position dice: [ 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, ], // sticks (map normal formation -> count) sticks: [], // cubes (map special formation -> count) cubes: [], morale: [ info.players[0].morale, info.players[1].morale ], lost: [ 0, 0 ], front: [ [], [], ], reserve: [ [], [] ], // dice value placed on what card rolled: 0, placed: [], summary: 0, // current action routed: [ 0, 0 ], selected: -1, target: -1, hits: 0, self: 0, // for breastworks etc attack_target: -1, // original target self2: 0, target2: -1, hits2: 0, } // Charles Alexander of Lorraine -- shift special if (info.number >= 44 && info.number <= 49) game.shift = [] function setup_formation(front, reserve, c) { let card = data.cards[c] if (card.reserve) set_add(reserve, c) else set_add(front, c) if (card.special) { if (card_has_rule(c, "start_with_no_cubes")) add_cubes(c, 0) else add_cubes(c, 1) } else { set_sticks(c, card.strength) } } for (let p = 0; p < 2; ++p) { for (let c of info.players[p].cards) setup_formation(game.front[p], game.reserve[p], c) } log(".h1 " + info.name) log(".h2 " + info.date) log(".img") log("") if (info.players[0].tactical > 0 || info.players[1].tactical > 0) { if (game.scenario === S38_FLEURUS) log("Tactical Draw:") else log("Tactical Victory:") if (info.players[0].tactical > 0) log(">" + player_name(0) + ": " + info.players[0].tactical) if (info.players[1].tactical > 0) log(">" + player_name(1) + ": " + info.players[1].tactical) log("") } if (info.lore_text) { for (let line of info.lore_text.split("

")) log(".lore " + line) log("") } if (info.rule_text) { for (let line of info.rule_text.split("

")) log(".rule " + line) log("") } if (game.scenario === S37_INKERMAN) { map_set(game.cubes, S37_THE_FOG, 3) } if (game.scenario === S38_FLEURUS) { map_set(game.cubes, S38_RETREAT_TO_NIVELLES, 3) } goto_start_turn() return game } // === GAME STATE ACCESSORS === function count_total_cubes() { let n = game.morale[0] + game.morale[1] for (let i = 1; i < game.cubes.length; i += 2) n += game.cubes[i] return n } function is_infantry(c) { return !!data.cards[c].infantry } function is_cavalry(c) { return !!data.cards[c].cavalry } function card_has_rule(c, name) { let rules = data.cards[c].rules if (rules) return rules[name] return false } function card_name(c) { return data.cards[c].name } function player_name(p) { return data.scenarios[game.scenario].players[p].name } function set_opponent_active() { if (game.active === P1) game.active = P2 else game.active = P1 } function player_index() { if (game.active === P1) return 0 return 1 } function get_cubes(c) { return map_get(game.cubes, c, 0) } function add_cubes(c, n) { let limit = data.cards[c].special let old = get_cubes(c) map_set(game.cubes, c, Math.min(limit, old + n)) } function remove_cubes(c, n) { let old = get_cubes(c) map_set(game.cubes, c, Math.max(0, old - n)) } function get_sticks(c) { return map_get(game.sticks, c, 0) } function set_sticks(c, n) { map_set(game.sticks, c, n) } function move_sticks(from, to, n) { // log(`Moved ${n} sticks from C${from} to C${to}.`) // TODO? set_sticks(from, get_sticks(from) - n) set_sticks(to, get_sticks(to) + n) } function move_all_sticks(from, to) { move_sticks(from, to, get_sticks(from)) } function get_shift_sticks(c) { if (game.shift) return map_get(game.shift, c, 0) return 0 } function set_shift_sticks(c, n) { if (game.shift) { if (n) map_set(game.shift, c, n) else map_delete(game.shift, c) } } function remove_sticks(c, n) { let p = find_card_owner(c) let old = get_sticks(c) n = Math.min(n, old) game.lost[p] += n set_sticks(c, old - n) } function remove_dice(c) { for (let i = 0; i < 12; ++i) { if (get_dice_location(i) === c) { set_dice_location(i, POOL) set_dice_value(i, 0) } } } function take_all_dice(from, to) { log("Dice from C" + from) for (let i = 0; i < 12; ++i) { if (get_dice_location(i) === from) { set_dice_location(i, to) } } } function take_one_die(from, to) { log("Die from C" + from) for (let i = 0; i < 12; ++i) { if (get_dice_location(i) === from) { set_dice_location(i, to) if (to === POOL) set_dice_value(i, 0) to = POOL } } } function take_wild_die(from, to) { log("Wild die from C" + from) for (let i = 0; i < 12; ++i) { if (get_dice_location(i) === from) { set_dice_location(i, to) set_dice_value(i, 0) to = POOL } } } function eliminate_card(c) { remove_dice(c) map_delete(game.cubes, c) map_delete(game.sticks, c) set_delete(game.front[0], c) set_delete(game.front[1], c) set_delete(game.reserve[0], c) set_delete(game.reserve[1], c) } function rout_card(c) { let p = find_card_owner(c) game.lost[p] += get_sticks(c) game.lost[p] += get_shift_sticks(c) log("C" + c + " routed.") eliminate_card(c) } function pursue_card(c) { let p = find_card_owner(c) game.lost[p] += get_shift_sticks(c) // TODO ? eliminate_card(c) } function retire_card(c) { let p = find_card_owner(c) game.lost[p] += get_shift_sticks(c) // TODO ? eliminate_card(c) } function remove_card(c) { let p = find_card_owner(c) log("C" + c + " removed.") game.lost[p] += get_shift_sticks(c) // TODO ? if (game.scenario === S49_LEUTHEN) { if (c === S49_BORNE) game.lost[0] += get_sticks(S49_BORNE) } eliminate_card(c) } function pay_for_action(c) { if (game.scenario === S46_ROCOUX) { if (c === S46_THE_MOUTH_OF_HELL) { // icky test for second reaction... if (game.state === "screen") { remove_cubes(c, 2) return } } } if (data.cards[c].special) remove_cubes(c, 1) else remove_dice(c) } function get_dice_value(d) { return game.dice[d * 2] } function get_dice_location(i) { return game.dice[i * 2 + 1] } function set_dice_location(d, v) { game.dice[d * 2 + 1] = v } function set_dice_value(d, v) { game.dice[d * 2] = v } // === HOW TO WIN === function is_card_in_play(c) { return ( set_has(game.front[0], c) || set_has(game.front[1], c) ) } function is_card_in_reserve(c) { return ( set_has(game.reserve[0], c) || set_has(game.reserve[1], c) ) } function is_card_in_play_or_reserve(c) { return ( set_has(game.front[0], c) || set_has(game.front[1], c) || set_has(game.reserve[0], c) || set_has(game.reserve[1], c) ) } function is_removed_from_play(c) { return !is_card_in_play_or_reserve(c) } function card_has_active_link(c) { // Ignores Link. if (game.scenario === S48_BRESLAU) { if (game.selected === S48_GRENZERS) return false } let link = data.cards[c].link if (link) { for (let t of link) if (is_card_in_play(t)) return true } return false } function card_has_attack_with_valid_target(c) { for (let a of data.cards[c].actions) { if (a.type === "Attack") { let attack_reserve = card_has_rule(c, "attack_reserve") for (let t of a.target_list) { if (is_card_in_play(t)) return true if (attack_reserve && is_card_in_reserve(t)) return true } } } return false } function is_impossible_to_attack() { let p = player_index() for (let c of game.front[p]) if (card_has_attack_with_valid_target(c)) return false return true } function check_impossible_to_attack_victory() { if (is_impossible_to_attack()) { if (player_index() === 0) return goto_game_over(P2, player_name(0) + " has no more attacks!") else return goto_game_over(P1, player_name(1) + " has no more attacks!") } return false } function get_tactical_victory_points(p) { let n = game.lost[1-p] if (game.scenario === S46_ROCOUX) { if (p === 0) n += get_sticks(S46_AUSTRIANS) } if (game.scenario === S47_PRAGUE) { if (p === 1) n += get_sticks(S47_CHARLES_LORRAINE) } return n } function check_victory() { let info = data.scenarios[game.scenario] if (game.scenario === S39_MARSAGLIA) { if (is_removed_from_play(S39_HOGUETTE) && is_removed_from_play(S39_CATINAT)) return goto_game_over(P1, player_name(1) + " lost both linked formations!") if (is_removed_from_play(S39_DUKE_OF_SAVOY) && is_removed_from_play(S39_EUGENE)) return goto_game_over(P2, player_name(0) + " lost both linked formations!") } if (game.scenario === S43_DENAIN) { if (is_removed_from_play(S43_PRINCE_DE_TINGRY)) return goto_game_over(P1, "Eugene is able to cross!") } if (game.scenario === S49_LEUTHEN) { if (is_removed_from_play(S49_NADASDY) && is_card_in_reserve(S49_COLLOREDO)) return goto_game_over(P1, "Nadasdy routed before Colloredo entered play!") } if (game.morale[0] === 0) return goto_game_over(P2, player_name(0) + " ran out of morale!") if (game.morale[1] === 0) return goto_game_over(P1, player_name(1) + " ran out of morale!") let tc1 = info.players[0].tactical let tc2 = info.players[1].tactical let tv1 = get_tactical_victory_points(0) let tv2 = get_tactical_victory_points(1) if (game.scenario === S38_FLEURUS) { if (tv2 >= 22) return goto_game_over("Draw", player_name(1) + " secured a draw!") } if (tc1 > 0 && tc2 > 0 && tv1 >= tc1 && tv2 >= tc2) return goto_game_over("Draw", player_name(0) + " and " + player_name(1) + " achieved tactical victory at the same time.") if (tc1 > 0 && tv1 >= tc1) return goto_game_over(P1, player_name(0) + " won a tactical victory!") if (tc2 > 0 && tv2 >= tc2) return goto_game_over(P2, player_name(1) + " won a tactical victory!") return false } // === ROLL PHASE === function is_pool_die(i, v) { let p = player_index() return get_dice_location(p * 6 + i) < 0 && get_dice_value(p * 6 + i) === v } function is_pool_die_range(i, lo, hi) { let p = player_index() if (get_dice_location(p * 6 + i) < 0) { let v = get_dice_value(p * 6 + i) return v >= lo && v <= hi } return false } function placed_any_dice() { return game.placed.length > 0 } function placed_any_dice_on_wing(w) { for (let i = 0; i < game.placed.length; i += 2) { let c = game.placed[i] if (data.cards[c].wing === w) return true } return false } function is_straight_4_or_3(_) { if (game.scenario === S28_CULPS_HILL) { if (game.rolled >= 5) return 4 else return 3 } if (game.scenario === S31_NEWBURY_1ST) { if (is_card_in_play(S31_SKIPPON)) return 4 else return 3 } if (game.scenario === S48_BRESLAU) { if (game.rolled >= 5) return 4 else return 3 } throw new Error("Missing rule for Straight 3/4 choice") } const place_dice_once = { "(1)": true, "(2)": true, "(3)": true, "(4)": true, "(5)": true, "(6)": true, "(1/2)": true, "(1-3)": true, "(1-4)": true, "(1-5)": true, "(2/3)": true, "(2-4)": true, "(2-5)": true, "(2-6)": true, "(3/4)": true, "(3-5)": true, "(3-6)": true, "(4/5)": true, "(4-6)": true, "(5/6)": true, } const place_dice_check = { "Full House": check_full_house, "Straight 4/3": check_straight_4_or_3, "Straight 3": check_straight_3, "Straight 4": check_straight_4, "Doubles": check_doubles, "Triples": check_triples, "1": (c) => check_single(c, 1), "2": (c) => check_single(c, 2), "3": (c) => check_single(c, 3), "4": (c) => check_single(c, 4), "5": (c) => check_single(c, 5), "6": (c) => check_single(c, 6), "(1)": (c) => check_single(c, 1), "(2)": (c) => check_single(c, 2), "(3)": (c) => check_single(c, 3), "(4)": (c) => check_single(c, 4), "(5)": (c) => check_single(c, 5), "(6)": (c) => check_single(c, 6), "Any": (c) => check_range(c, 1, 6), "1/2": (c) => check_range(c, 1, 2), "1-3": (c) => check_range(c, 1, 3), "1-4": (c) => check_range(c, 1, 4), "1-5": (c) => check_range(c, 1, 5), "2/3": (c) => check_range(c, 2, 3), "2-4": (c) => check_range(c, 2, 4), "2-5": (c) => check_range(c, 2, 5), "2-6": (c) => check_range(c, 2, 6), "3/4": (c) => check_range(c, 3, 4), "3-5": (c) => check_range(c, 3, 5), "3-6": (c) => check_range(c, 3, 6), "4/5": (c) => check_range(c, 4, 5), "4-6": (c) => check_range(c, 4, 6), "5/6": (c) => check_range(c, 5, 6), "(1/2)": (c) => check_range(c, 1, 2), "(1-3)": (c) => check_range(c, 1, 3), "(1-4)": (c) => check_range(c, 1, 4), "(1-5)": (c) => check_range(c, 1, 5), "(2/3)": (c) => check_range(c, 2, 3), "(2-4)": (c) => check_range(c, 2, 4), "(2-5)": (c) => check_range(c, 2, 5), "(2-6)": (c) => check_range(c, 2, 6), "(3/4)": (c) => check_range(c, 3, 4), "(3-5)": (c) => check_range(c, 3, 5), "(3-6)": (c) => check_range(c, 3, 6), "(4/5)": (c) => check_range(c, 4, 5), "(4-6)": (c) => check_range(c, 4, 6), "(5/6)": (c) => check_range(c, 5, 6), } const place_dice_gen = { "Full House": gen_full_house, "Straight 4/3": gen_straight_4_or_3, "Straight 3": gen_straight_3, "Straight 4": gen_straight_4, "Doubles": gen_doubles, "Triples": gen_triples, "1": (c) => gen_single(c, 1), "2": (c) => gen_single(c, 2), "3": (c) => gen_single(c, 3), "4": (c) => gen_single(c, 4), "5": (c) => gen_single(c, 5), "6": (c) => gen_single(c, 6), "(1)": (c) => gen_single(c, 1), "(2)": (c) => gen_single(c, 2), "(3)": (c) => gen_single(c, 3), "(4)": (c) => gen_single(c, 4), "(5)": (c) => gen_single(c, 5), "(6)": (c) => gen_single(c, 6), "Any": (c) => gen_range(c, 1, 6), "1/2": (c) => gen_range(c, 1, 2), "1-3": (c) => gen_range(c, 1, 3), "1-4": (c) => gen_range(c, 1, 4), "1-5": (c) => gen_range(c, 1, 5), "2/3": (c) => gen_range(c, 2, 3), "2-4": (c) => gen_range(c, 2, 4), "2-5": (c) => gen_range(c, 2, 5), "2-6": (c) => gen_range(c, 2, 6), "3/4": (c) => gen_range(c, 3, 4), "3-5": (c) => gen_range(c, 3, 5), "3-6": (c) => gen_range(c, 3, 6), "4/5": (c) => gen_range(c, 4, 5), "4-6": (c) => gen_range(c, 4, 6), "5/6": (c) => gen_range(c, 5, 6), "(1/2)": (c) => gen_range(c, 1, 2), "(1-3)": (c) => gen_range(c, 1, 3), "(1-4)": (c) => gen_range(c, 1, 4), "(1-5)": (c) => gen_range(c, 1, 5), "(2/3)": (c) => gen_range(c, 2, 3), "(2-4)": (c) => gen_range(c, 2, 4), "(2-5)": (c) => gen_range(c, 2, 5), "(2-6)": (c) => gen_range(c, 2, 6), "(3/4)": (c) => gen_range(c, 3, 4), "(3-5)": (c) => gen_range(c, 3, 5), "(3-6)": (c) => gen_range(c, 3, 6), "(4/5)": (c) => gen_range(c, 4, 5), "(4-6)": (c) => gen_range(c, 4, 6), "(5/6)": (c) => gen_range(c, 5, 6), } const place_dice_take = { "Full House": take_full_house, "Straight 4/3": take_straight_4_or_3, "Straight 3": take_straight_3, "Straight 4": take_straight_4, "Doubles": take_doubles, "Triples": take_triples, "1": take_single, "2": take_single, "3": take_single, "4": take_single, "5": take_single, "6": take_single, "(1)": take_single, "(2)": take_single, "(3)": take_single, "(4)": take_single, "(5)": take_single, "(6)": take_single, "Any": (c, d) => take_single(c, d), "1/2": (c, d) => take_single(c, d), "1-3": (c, d) => take_single(c, d), "1-4": (c, d) => take_single(c, d), "1-5": (c, d) => take_single(c, d), "2/3": (c, d) => take_single(c, d), "2-4": (c, d) => take_single(c, d), "2-5": (c, d) => take_single(c, d), "2-6": (c, d) => take_single(c, d), "3/4": (c, d) => take_single(c, d), "3-5": (c, d) => take_single(c, d), "3-6": (c, d) => take_single(c, d), "4/5": (c, d) => take_single(c, d), "4-6": (c, d) => take_single(c, d), "5/6": (c, d) => take_single(c, d), "(1/2)": (c, d) => take_single(c, d), "(1-3)": (c, d) => take_single(c, d), "(1-4)": (c, d) => take_single(c, d), "(1-5)": (c, d) => take_single(c, d), "(2/3)": (c, d) => take_single(c, d), "(2-4)": (c, d) => take_single(c, d), "(2-5)": (c, d) => take_single(c, d), "(2-6)": (c, d) => take_single(c, d), "(3/4)": (c, d) => take_single(c, d), "(3-5)": (c, d) => take_single(c, d), "(3-6)": (c, d) => take_single(c, d), "(4/5)": (c, d) => take_single(c, d), "(4-6)": (c, d) => take_single(c, d), "(5/6)": (c, d) => take_single(c, d), } function can_place_dice(c) { let pattern = data.cards[c].dice if (!pattern) return false let pred = place_dice_check[pattern] if (!pred) throw Error("bad pattern definition: " + pattern) // At per card limit? if (place_dice_once[pattern]) { if (map_has(game.placed, c)) return false } // At cube limit? if (data.cards[c].special) { // Max on card if (get_cubes(c) >= data.cards[c].special) return false // Max available let n_cubes = count_total_cubes() if (n_cubes >= 10) return false if (game.scenario === S12_TOWTON) { if (n_cubes >= 8) return false } } // At per wing limit? let wing = data.cards[c].wing let n_wing = 0 for (let i = 0; i < game.placed.length; i += 2) { let x = game.placed[i] if (x !== c) { let i_wing = data.cards[x].wing if (i_wing === wing) n_wing ++ } } if (n_wing >= game.place_max[wing]) return false if (game.scenario === S8_BROOKLYN_HEIGHTS) { if (c === S8_CLINTON) { if (is_removed_from_play(S8_GRANT)) return false if (is_removed_from_play(S8_HESSIANS)) return false } } if (game.scenario === S3201_GAINES_MILL) { if (c === S3201_JACKSON) { if (!has_any_dice_on_card(S3201_DH_HILL)) return false } } if (game.scenario === S29_GETTYS_2ND) { if (c === S29_MCLAWS) { if (!has_any_dice_on_card(S29_HOOD)) return false } if (c === S29_ANDERSON) { if (!has_any_dice_on_card(S29_MCLAWS)) return false } if (c === S29_EARLY || c === S29_JOHNSON) { let red = 0 if (has_any_dice_on_card(S29_HOOD)) ++red if (has_any_dice_on_card(S29_MCLAWS)) ++red if (has_any_dice_on_card(S29_ANDERSON)) ++red if (red < 2) return false } } return pred(c) } function can_place_value(c, v) { let old_v = map_get(game.placed, c, 0) return old_v === 0 || old_v === v } function pool_has_single(v) { for (let i = 0; i < 6; ++i) if (is_pool_die(i, v)) return true return false } function check_single_count(c, v, x) { if (!can_place_value(c, v)) return false let n = 0 for (let i = 0; i < 6; ++i) if (is_pool_die(i, v) && ++n >= x) return true return false } function check_single(c, v) { if (!can_place_value(c, v)) return false for (let i = 0; i < 6; ++i) if (is_pool_die(i, v)) return true return false } function check_range(c, lo, hi) { let old_v = map_get(game.placed, c, 0) if (old_v > 0) return pool_has_single(old_v) for (let i = 0; i < 6; ++i) if (is_pool_die_range(i, lo, hi)) return true return false } function check_all_3(c, x, y, z) { if (!can_place_value(c, x)) return false return pool_has_single(x) && pool_has_single(y) && pool_has_single(z) } function check_all_4(c, x, y, z, w) { if (!can_place_value(c, x)) return false return pool_has_single(x) && pool_has_single(y) && pool_has_single(z) && pool_has_single(w) } function check_straight_4_or_3(c) { if (is_straight_4_or_3(c) === 4) return check_straight_4(c) else return check_straight_3(c) } function check_straight_3(c) { return ( check_all_3(c, 1, 2, 3) || check_all_3(c, 2, 3, 4) || check_all_3(c, 3, 4, 5) || check_all_3(c, 4, 5, 6) ) } function check_straight_4(c) { return ( check_all_4(c, 1, 2, 3, 4) || check_all_4(c, 2, 3, 4, 5) || check_all_4(c, 3, 4, 5, 6) ) } function check_doubles(c) { return ( check_single_count(c, 1, 2) || check_single_count(c, 2, 2) || check_single_count(c, 3, 2) || check_single_count(c, 4, 2) || check_single_count(c, 5, 2) || check_single_count(c, 6, 2) ) } function check_triples(c) { return ( check_single_count(c, 1, 3) || check_single_count(c, 2, 3) || check_single_count(c, 3, 3) || check_single_count(c, 4, 3) || check_single_count(c, 5, 3) || check_single_count(c, 6, 3) ) } function check_full_house(c) { for (let x = 1; x <= 6; ++x) { for (let y = 1; y <= 6; ++y) { if (x !== y) { if (check_single_count(c, x, 3) && check_single_count(c, y, 2)) return true } } } return false } function gen_pool_die(v) { let p = player_index() for (let i = 0; i < 6; ++i) if (get_dice_location(p * 6 + i) < 0 && get_dice_value(p * 6 + i) === v) gen_action_die(p * 6 + i) } function gen_single(c, v) { if (!can_place_value(c, v)) return false gen_pool_die(v) } function gen_range(c, lo, hi) { for (let v = lo; v <= hi; ++v) gen_single(c, v) } function gen_straight_4_or_3(c) { if (is_straight_4_or_3(c) === 4) gen_straight_4(c) else gen_straight_3(c) } function gen_straight_3(c) { if (check_all_3(c, 1, 2, 3)) gen_pool_die(1) if (check_all_3(c, 2, 3, 4)) gen_pool_die(2) if (check_all_3(c, 3, 4, 5)) gen_pool_die(3) if (check_all_3(c, 4, 5, 6)) gen_pool_die(4) } function gen_straight_4(c) { if (check_all_4(c, 1, 2, 3, 4)) gen_pool_die(1) if (check_all_4(c, 2, 3, 4, 5)) gen_pool_die(2) if (check_all_4(c, 3, 4, 5, 6)) gen_pool_die(3) } function gen_doubles(c) { for (let v = 1; v <= 6; ++v) if (check_single_count(c, v, 2)) gen_pool_die(v) } function gen_triples(c) { for (let v = 1; v <= 6; ++v) if (check_single_count(c, v, 3)) gen_pool_die(v) } function gen_full_house(c) { for (let x = 1; x <= 6; ++x) { for (let y = 1; y <= 6; ++y) { if (x !== y) { if (check_single_count(c, x, 3) && check_single_count(c, y, 2)) gen_pool_die(x) } } } } function find_and_take_single(c, v) { let p = player_index() for (let i = 0; i < 6; ++i) { let d = p * 6 + i if (get_dice_location(d) < 0 && get_dice_value(d) === v) { set_dice_location(d, c) game.summary |= (1 << d) return } } throw new Error("cannot find die of value " + v) } function take_single(c, d) { set_dice_location(d, c) game.summary |= (1 << d) map_set(game.placed, c, get_dice_value(d)) } function take_doubles(c, d) { let v = get_dice_value(d) take_single(c, d) find_and_take_single(c, v) } function take_triples(c, d) { let v = get_dice_value(d) take_single(c, d) find_and_take_single(c, v) find_and_take_single(c, v) } function take_full_house(c, d) { let x = get_dice_value(d) for (let y = 1; y <= 6; ++y) { if (x !== y) { if (check_single_count(c, x, 3) && check_single_count(c, y, 2)) { find_and_take_single(c, x) find_and_take_single(c, x) find_and_take_single(c, x) find_and_take_single(c, y) find_and_take_single(c, y) } } } } function take_straight_4_or_3(c, d) { if (is_straight_4_or_3(c) === 4) take_straight_4(c, d) else take_straight_3(c, d) } function take_straight_3(c, d) { let v = get_dice_value(d) take_single(c, d) find_and_take_single(c, v+1) find_and_take_single(c, v+2) } function take_straight_4(c, d) { let v = get_dice_value(d) take_single(c, d) find_and_take_single(c, v+1) find_and_take_single(c, v+2) find_and_take_single(c, v+3) } function goto_roll_phase() { game.selected = -1 game.attack_target = -1 game.target = -1 game.target2 = -1 game.action = 0 game.state = "roll" game.place_max = [ 1, 1, 1, 1 ] let p = player_index() for (let c of game.front[p]) { if (card_has_rule(c, "place_2_blue")) game.place_max[BLUE] = 2 if (card_has_rule(c, "place_2_red")) game.place_max[RED] = 2 if (card_has_rule(c, "place_2_red_if_dice") && has_any_dice_on_card(c)) game.place_max[RED] = 2 if (card_has_rule(c, "place_2_dkblue")) game.place_max[DKBLUE] = 2 /* // NOT USED (YET) if (card_has_rule(c, "place_2_pink")) game.place_max[PINK] = 2 if (card_has_rule(c, "place_2_blue_if_dice") && has_any_dice_on_card(c)) game.place_max[BLUE] = 2 if (card_has_rule(c, "place_2_dkblue_if_dice") && has_any_dice_on_card(c)) game.place_max[DKBLUE] = 2 if (card_has_rule(c, "place_2_pink_if_dice") && has_any_dice_on_card(c)) game.place_max[PINK] = 2 */ } if (game.scenario === S38_FLEURUS) { if (p === 1) { if (get_sticks(S38_RETREAT_TO_NIVELLES)) { if (game.starting_on_your_next_turn++) { set_sticks(S38_RETREAT_TO_NIVELLES, get_sticks(S38_RETREAT_TO_NIVELLES) - 1) game.lost[0] += 1 if (check_victory()) return } } } } } states.skip_action = { inactive: "roll", prompt() { view.prompt = "Skipped action phase; roll the dice in your pool." if (can_shift()) view.actions.shift = 1 if (count_dice_in_pool() > 0) { view.actions.roll = 1 view.actions.end_turn = 0 } else { view.actions.roll = 0 view.actions.end_turn = 1 } }, shift() { push_undo() game.state = "shift_from" }, roll() { clear_undo() goto_roll_phase() roll_dice_in_pool() }, end_turn() { goto_roll_phase() roll_dice_in_pool() end_roll_phase() }, } states.roll = { inactive: "roll", prompt() { view.prompt = "Roll the dice in your pool." view.actions.roll = 1 view.actions.end_turn = 0 }, roll() { clear_undo() roll_dice_in_pool() }, } function roll_dice_in_pool() { let rolled = [] if (game.reacted === player_index()) game.reacted = -1 let p = player_index() for (let i = 0; i < 6; ++i) { if (get_dice_location(p * 6 + i) < 0) { let v = random(6) + 1 set_dice_value(p * 6 + i, v) rolled.push(v) } } log("Roll\n" + rolled.map(d => "D" + d).join(" ")) game.rolled = rolled.length game.summary = 0 game.state = "place" } function gen_place_dice_select_card() { let p = player_index() for (let c of game.front[p]) { if (c === game.selected) continue if (can_place_dice(c)) gen_action_card(c) } } states.place = { inactive: "place dice", prompt() { view.prompt = "Place dice on your formations." gen_place_dice_select_card() view.actions.end_turn = 1 }, card(c) { push_undo() game.selected = c game.state = "place_on_card" }, end_turn() { end_roll_phase() }, } states.place_on_card = { inactive: "place dice", prompt() { let card = data.cards[game.selected] view.prompt = "Place dice on " + card.name + "." gen_place_dice_select_card() place_dice_gen[card.dice](game.selected) view.actions.end_turn = 1 }, card(c) { if (c === game.selected) { game.selected = -1 game.state = "place" } else { game.selected = c game.state = "place_on_card" } }, die(d) { push_undo() place_dice_take[data.cards[game.selected].dice](game.selected, d) if (!can_place_dice(game.selected)) { game.selected = -1 game.state = "place" } }, end_turn() { end_roll_phase() }, } function end_roll_phase() { push_undo() game.selected = -1 if (game.placed.length > 0) { for (let i = 0; i < game.placed.length; i += 2) { let c = game.placed[i] let s = [] for (let d = 0; d < 12; ++d) if (game.summary & (1 << d)) if (get_dice_location(d) === c) s.push("D" + get_dice_value(d)) log("C" + c + "\n" + s.join(" ")) } } // Remove placed dice to add cube on special cards. for (let c of game.front[player_index()]) { let s = data.cards[c].special if (s && has_any_dice_on_card(c)) { map_set(game.cubes, c, Math.min(s, get_cubes(c) + 1)) remove_dice(c) } } // Blank out unused dice. let p = player_index() for (let i = 0; i < 6; ++i) if (get_dice_location(p * 6 + i) < 0) set_dice_value(p * 6 + i, 0) if (game.scenario === S26_PEACH_ORCHARD) { if (player_index() === 0) { if (is_card_in_play(S26_FATAL_BLUNDER)) { if (!placed_any_dice_on_wing(PINK)) { game.state = "s26_fatal_blunder" return } } } } if (game.scenario === S28_CULPS_HILL) { if (get_cubes(S28_GEARY) === 5) return goto_game_over(P2, "Geary's Division arrived!") } if (game.scenario === S41_BLENHEIM_SCENARIO) { if (player_index() === 1) { if (is_card_in_play(S41_CLERAMBAULT)) { if (!placed_any_dice_on_wing(DKBLUE)) { game.state = "s41_clerambault" return } } } } if (game.scenario === S47_PRAGUE) { if (player_index() === 0) { if (is_card_in_play(S47_SCHWERIN) && !placed_any_dice()) { game.target2 = S47_SCHWERIN game.state = "s47_browne_and_schwerin" return } } if (player_index() === 1) { if (is_card_in_play(S47_BROWNE) && !placed_any_dice()) { game.target2 = S47_BROWNE game.state = "s47_browne_and_schwerin" return } } } if (game.scenario === S48_BRESLAU) { if (player_index() === 0) { if (is_card_in_play(S48_AUSTRIAN_GUNS)) { if (get_cubes(S48_AUSTRIAN_GUNS) === 0 && get_cubes(S48_PRUSSIAN_GUNS) === 2) { game.target2 = S48_AUSTRIAN_GUNS game.state = "s48_artillery_duel" return } } } if (player_index() === 1) { if (is_card_in_play(S48_PRUSSIAN_GUNS)) { if (get_cubes(S48_PRUSSIAN_GUNS) === 0 && get_cubes(S48_AUSTRIAN_GUNS) === 2) { game.target2 = S48_PRUSSIAN_GUNS game.state = "s48_artillery_duel" return } } } } end_turn() } states.s26_fatal_blunder = { inactive: "remove cards", prompt() { view.prompt = "Fatal Blunder!" if (is_card_in_play(S26_FATAL_BLUNDER)) { gen_action_card(S26_FATAL_BLUNDER) } else { let done = true for (let c of game.front[0]) { if (data.cards[c].wing === PINK) { gen_action_card(c) done = false } } if (done) view.actions.end_turn = 1 } }, card(c) { if (c === S26_FATAL_BLUNDER) { log("Fatal Blunder!") remove_card(S26_FATAL_BLUNDER) game.morale[0] ++ } else { rout_card(c) game.morale[0] -- game.morale[1] ++ } }, end_turn() { if (check_victory()) return end_turn() } } states.s41_clerambault = { inactive: "rout cards", prompt() { view.prompt = "Rout Clerambault and add his sticks to Blenheim!" if (is_card_in_play(S41_BLENHEIM_CARD) && get_sticks(S41_CLERAMBAULT)) { view.selected = S41_CLERAMBAULT gen_action_card(S41_BLENHEIM_CARD) } else { gen_action_card(S41_CLERAMBAULT) } }, card(c) { if (c === S41_BLENHEIM_CARD) move_all_sticks(S41_CLERAMBAULT, S41_BLENHEIM_CARD) else set_sticks(S41_CLERAMBAULT, 0) if (c === S41_CLERAMBAULT) { rout_card(S41_CLERAMBAULT) game.morale[0]++ game.morale[1]-- end_turn() } }, } states.s47_browne_and_schwerin = { inactive: "remove cards", prompt() { view.prompt = "Remove " + card_name(game.target2) + "." gen_action_card(game.target2) }, card(_) { remove_card(game.target2) game.target2 = -1 end_turn() }, } states.s48_artillery_duel = { inactive: "remove cards", prompt() { view.prompt = "Artillery Duel: Remove " + card_name(game.target2) + "." gen_action_card(game.target2) }, card(_) { remove_card(game.target2) game.target2 = -1 end_turn() }, } function end_turn() { clear_undo() map_clear(game.placed) game.place_max = null set_opponent_active() goto_start_turn() } // === ACTION PHASE === function side_get_wild_die_card(p) { if (game.scenario === S11_MORTIMERS_CROSS || game.scenario === S12_TOWTON || game.scenario === S16_STOKE_FIELD) { for (let c of game.front[p]) if (card_has_rule(c, "wild") && has_any_dice_on_card(c)) return c } return -1 } function has_any_red_normal_cards() { for (let c of game.front[0]) if (data.cards[c].wing === RED && !data.cards[c].special) return true for (let c of game.front[1]) if (data.cards[c].wing === RED && !data.cards[c].special) return true } function side_has_wild_die(p) { return side_get_wild_die_card(p) >= 0 } function has_any_dice_on_card(c) { for (let i = 0; i < 12; ++i) if (get_dice_location(i) === c) return true return false } function has_any_cubes_on_card(c) { return get_cubes(c) >= 1 } function count_dice_in_pool() { let n = 0 let p = player_index() for (let i = 0; i < 6; ++i) if (get_dice_location(p * 6 + i) < 0) ++n return n } function count_dice_on_card(c) { let n = 0 for (let i = 0; i < 12; ++i) if (get_dice_location(i) === c) ++n return n } function count_dice_on_card_with_value(c, v) { let n = 0 for (let i = 0; i < 12; ++i) if (get_dice_location(i) === c && get_dice_value(i) === v) ++n return n } function require_pair(c) { for (let v = 1; v <= 6; ++v) if (count_dice_on_card_with_value(c, v) >= 2) return true return false } function require_triplet(c) { for (let v = 1; v <= 6; ++v) if (count_dice_on_card_with_value(c, v) >= 3) return true return false } function require_full_house(c) { let n3 = 0 let n2 = 0 for (let v = 1; v <= 6; ++v) { let n = count_dice_on_card_with_value(c, v) if (n >= 3) ++n3 else if (n >= 2) ++n2 } return (n3 >= 2) || (n3 >= 1 && n2 >= 1) } function require_two_pairs(c) { let n = 0 for (let v = 1; v <= 6; ++v) if (count_dice_on_card_with_value(c, v) >= 2) ++n return n >= 2 } function check_cube_requirement(c, req) { switch (req) { case "3 cubes": case "Three Cubes": return get_cubes(c) >= 3 case "Two Cubes": return get_cubes(c) >= 2 case "One Cube*": case "Voluntary": case undefined: return get_cubes(c) >= 1 default: throw new Error("invalid action requirement: " + req) } } function check_dice_requirement(c, req, wild) { switch (req) { case "Three Dice": return count_dice_on_card(c) >= 3 case "Full House": return require_full_house(c) case "Pair": case "Pair, Voluntary": // NOTE: Only requirement needed for Wild die scenarios. if (wild) return has_any_dice_on_card(c) return require_pair(c) case "Triplet": return require_triplet(c) case "Two Pairs": return require_two_pairs(c) case "Voluntary": case undefined: return has_any_dice_on_card(c) default: throw new Error("invalid action requirement: " + req) } } function card_has_any_actions(c) { for (let a of data.cards[c].actions) if (is_action(c, a)) return true return false } function is_action(c, a) { return (a.type === "Bombard" || a.type === "Attack" || a.type === "Command") } function is_reaction(c, a) { return (a.type === "Screen" || a.type === "Counterattack" || a.type === "Absorb") } function is_mandatory_reaction(c, a) { if (game.scenario === S44_HOHENFRIEDBERG) { if (c === S44_DU_MOULIN && game.selected === S44_SAXON_HORSE) return false } return ( a.requirement !== "Voluntary" && a.requirement !== "Pair, Voluntary" ) } function can_take_action(c, a, ix) { if (a.type === "Attack") { if (find_target_of_attack(c, a) < 0) return false } if (a.type === "Command") { if (find_first_target_of_command(c, a) < 0) return false } if (a.type === "Bombard") { // cannot Bombard last morale cube let p = player_index() if (game.morale[1-p] === 1) return false } if (game.scenario === S8_BROOKLYN_HEIGHTS) { if (c === S8_CLINTON) { // Clinton - may only attack if both Grant and Hessians have dice on them. if (!has_any_dice_on_card(S8_GRANT) || !has_any_dice_on_card(S8_HESSIANS)) return false } } if (game.scenario === S3201_GAINES_MILL) { if (c === S3201_JACKSON) { // Jackson - may only attack if D.H. Hill and one other formation have dice if (!has_any_dice_on_card(S3201_DH_HILL)) return false if (!has_any_dice_on_card(S3201_AP_HILL) && !has_any_dice_on_card(S3201_LONGSTREET)) return false } } if (game.scenario === S35_AULDEARN) { if (c === S35_MONTROSE) { // May only perform the second action after having previously performed the first. if (ix === 1) { if (is_card_in_reserve(S35_GORDON)) return false } } } if (game.scenario === S45_SOOR) { if (c === S45_AUSTRIAN_GUNS) { // May only attack Buddenbrock if Cuirassiers still in play if (ix === 0) { if (is_removed_from_play(S45_CUIRASSIERS)) return false } } } if (game.scenario === S49_LEUTHEN) { if (c === S49_POOR_CHARLES) { // Cannot command if dice on Feint if (has_any_dice_on_card(S49_FEINT)) return false } } if (a.type === "Bombard" || a.type === "Attack" || a.type === "Command") { if (data.cards[c].special > 0) return check_cube_requirement(c, a.requirement) else return check_dice_requirement(c, a.requirement, false) } return false } function s40_can_take_cassines_action(c, a, b) { return (get_sticks(c) < 3) && (is_card_in_play(a) || is_card_in_play(b)) } function can_take_any_action() { let p = player_index() for (let c of game.front[p]) { if (card_has_any_actions(c)) { if (has_any_dice_on_card(c)) return true if (has_any_cubes_on_card(c)) // TODO: check requirements! return true } } if (game.scenario === S40_CHIARI) { if (player_index() === 1) { if (s40_can_take_cassines_action(S40_CASSINES_I, S40_NIGRELLI, S40_KRIECHBAUM)) return true if (s40_can_take_cassines_action(S40_CASSINES_II, S40_MANNSFELDT, S40_GUTTENSTEIN)) return true } } if (game.scenario === S41_BLENHEIM_SCENARIO) { if (player_index() === 0) { if (has_any_dice_on_card(S41_PRINCE_EUGENE) && has_any_red_normal_cards()) return true } } if (game.scenario === S42_RAMILLIES) { if (player_index() === 0) { if (has_any_dice_on_card(S42_MARLBOROUGH)) return true } } if (can_shift()) return true return false } function count_cards_remaining_from_wing(w) { let n = 0 for (let c of game.front[0]) if (data.cards[c].wing === w) ++n for (let c of game.front[1]) if (data.cards[c].wing === w) ++n for (let c of game.reserve[0]) if (data.cards[c].wing === w) ++n for (let c of game.reserve[1]) if (data.cards[c].wing === w) ++n return n } function goto_start_turn() { let p = player_index() log(".p" + (p + 1)) if (check_impossible_to_attack_victory()) return // TODO: manual step to shift? if (game.shift) { for (let c of game.front[p]) { let n = get_shift_sticks(c) if (n > 0) { set_sticks(c, get_sticks(c) + n) set_shift_sticks(c, 0) } } } if (game.scenario === S25_WHEATFIELD) { // Rout Stony Hill at start of Union turn if it is the only Blue card left. if (p === 1) { if (is_card_in_play(S25_STONY_HILL)) { if (count_cards_remaining_from_wing(BLUE) === 1) { game.state = "s25_stony_hill" return } } } } if (game.scenario === S44_HOHENFRIEDBERG) { if (p === 1) { let have_inf_or_cav = false for (let c of game.front[1]) if (is_infantry(c) || is_cavalry(c)) have_inf_or_cav = true if (!have_inf_or_cav) return goto_game_over(P1, "Frederick had no Infantry or Cavalry in play!") } } goto_action_phase() } states.s25_stony_hill = { inactive: "rout cards", prompt() { view.prompt = "Rout Stony Hill!" gen_action_card(S25_STONY_HILL) }, card(_) { rout_card(S25_STONY_HILL) game.morale[0] ++ game.morale[1] -- if (check_victory()) return goto_action_phase() }, } function goto_action_phase() { if (game.reacted === player_index()) { game.state = "skip_action" } else { if (can_take_any_action()) game.state = "action" else end_action_phase() } } function end_action_phase() { game.hits = 0 game.self = 0 game.hits2 = 0 game.self2 = 0 game.selected = -1 game.target = -1 game.target2 = -1 goto_routing() } states.action = { inactive: "take an action", prompt() { view.prompt = "Take an action." if (count_dice_in_pool() > 0) { view.actions.roll = 1 view.actions.end_turn = 0 } else { view.actions.roll = 0 view.actions.end_turn = 1 } let p = player_index() for (let c of game.front[p]) { let has_dice = has_any_dice_on_card(c) let has_cube = has_any_cubes_on_card(c) if (has_dice || has_cube) { if (data.cards[c].actions.length >= 1) { if (is_action(c, data.cards[c].actions[0])) { if (can_take_action(c, data.cards[c].actions[0], 0)) gen_action_action1(c) else if (has_dice) gen_action_null1(c) } } if (data.cards[c].actions.length >= 2) { if (is_action(c, data.cards[c].actions[1])) { if (can_take_action(c, data.cards[c].actions[1], 1)) gen_action_action2(c) else if (has_dice) gen_action_null2(c) } } if (data.cards[c].retire) gen_action_retire(c) } } if (can_shift()) view.actions.shift = 1 if (game.scenario === S40_CHIARI) { if (player_index() === 1) { if (s40_can_take_cassines_action(S40_CASSINES_I, S40_NIGRELLI, S40_KRIECHBAUM)) gen_action_card(S40_CASSINES_I) if (s40_can_take_cassines_action(S40_CASSINES_II, S40_MANNSFELDT, S40_GUTTENSTEIN)) gen_action_card(S40_CASSINES_II) } } if (game.scenario === S41_BLENHEIM_SCENARIO) { if (player_index() === 0) { if (has_any_dice_on_card(S41_PRINCE_EUGENE) && has_any_red_normal_cards()) gen_action_card(S41_PRINCE_EUGENE) } } if (game.scenario === S42_RAMILLIES) { if (player_index() === 0) { if (has_any_dice_on_card(S42_MARLBOROUGH)) gen_action_card(S42_MARLBOROUGH) } } }, retire(c) { push_undo() log("Retire\nC" + c) if (game.scenario === S38_FLEURUS) { if (c === S38_LUXEMBOURGS_HORSE) { if (is_card_in_play(S38_GOURNAYS_HORSE)) move_all_sticks(S38_LUXEMBOURGS_HORSE, S38_GOURNAYS_HORSE) } } retire_card(c) end_action_phase() }, a1(c) { push_undo() goto_take_action(c, 0) }, a2(c) { push_undo() goto_take_action(c, 1) }, n1(c) { push_undo() goto_null(c) }, n2(c) { push_undo() goto_null(c) }, roll() { clear_undo() goto_roll_phase() roll_dice_in_pool() }, shift() { push_undo() game.state = "shift_from" }, card(c) { push_undo() game.selected = c if (game.scenario === S40_CHIARI) { game.state = "s40_cassines" return } if (game.scenario === S41_BLENHEIM_SCENARIO) { game.target2 = -1 game.self = Math.min(get_sticks(S41_PRINCE_EUGENE), count_dice_on_card(S41_PRINCE_EUGENE)) game.state = "s41_prince_eugene" game.prince_eugene = 0 return } if (game.scenario === S42_RAMILLIES) { game.state = "s42_marlborough" return } throw new Error("missing rule for special action: " + card_name(c)) }, end_turn() { goto_roll_phase() roll_dice_in_pool() end_roll_phase() }, } function can_shift() { if (!game.shift) return false if (game.scenario === S49_LEUTHEN) { if (player_index() === 1) { if (has_any_dice_on_card(S49_FEINT)) return false } } return can_shift_any_infantry() || can_shift_any_cavalry() } function can_shift_any_infantry() { let n = 0, m = 0 for (let c of game.front[player_index()]) { if (is_infantry(c)) { if (get_sticks(c) > 1) ++m if (game.scenario === S46_ROCOUX) { // cannot shift to Austrians if (c === S46_AUSTRIANS) continue } ++n } } return n > 1 && m > 0 } function can_shift_any_cavalry() { let n = 0, m = 0 for (let c of game.front[player_index()]) { if (is_cavalry(c)) { if (get_sticks(c) > 1) ++m ++n } } return n > 1 && m > 0 } states.shift_from = { inactive: "shift sticks", prompt() { view.prompt = "Shift sticks from one Formation to another." let p = player_index() if (can_shift_any_infantry()) for (let c of game.front[p]) if (is_infantry(c) && get_sticks(c) > 1) gen_action_card(c) if (can_shift_any_cavalry()) for (let c of game.front[p]) if (is_cavalry(c) && get_sticks(c) > 1) gen_action_card(c) }, card(c) { game.selected = c game.target2 = -1 game.state = "shift_to" }, } states.shift_to = { inactive: "shift sticks", prompt() { view.prompt = "Shift sticks from " + card_name(game.selected) + "." let p = player_index() if (game.target2 < 0) { if (is_infantry(game.selected)) for (let c of game.front[p]) if (c !== game.selected && is_infantry(c) && c !== S46_AUSTRIANS) gen_action_card(c) if (is_cavalry(game.selected)) for (let c of game.front[p]) if (c !== game.selected && is_cavalry(c)) gen_action_card(c) } else { gen_action_card(game.target2) view.actions.next = 1 } }, card(c) { game.target2 = c set_sticks(game.selected, get_sticks(game.selected) - 1) set_shift_sticks(game.target2, get_shift_sticks(game.target2) + 1) if (get_sticks(game.selected) === 1) this.next() }, next() { // TODO: skip action phase? log("Shift\nC" + game.selected + "\nC" + game.target2 + "\n" + get_shift_sticks(game.target2) + " sticks.") end_action_phase() }, } states.s40_cassines = { inactive: "move sticks", prompt() { view.prompt = "Cassines: Move one unit stick to this card." if (game.selected === S40_CASSINES_I) { if (is_card_in_play(S40_NIGRELLI)) gen_action_card(S40_NIGRELLI) if (is_card_in_play(S40_KRIECHBAUM)) gen_action_card(S40_KRIECHBAUM) } if (game.selected === S40_CASSINES_II) { if (is_card_in_play(S40_MANNSFELDT)) gen_action_card(S40_MANNSFELDT) if (is_card_in_play(S40_GUTTENSTEIN)) gen_action_card(S40_GUTTENSTEIN) } }, card(c) { log("Move\nC" + c + "\nC" + game.selected + "\n1 stick.") move_sticks(c, game.selected, 1) end_action_phase() }, } states.s41_prince_eugene = { inactive: "move sticks", prompt() { if (game.target2 < 0) { view.prompt = `Prince Eugene: Move up to ${game.self} unit sticks to any Red card.` for (let c of game.front[0]) if (data.cards[c].wing === RED && !data.cards[c].special) gen_action_card(c) } else { view.prompt = `Prince Eugene: Move up to ${game.self} unit sticks to ${card_name(game.target2)}.` gen_action_card(game.target2) view.actions.next = 1 } }, card(c) { game.target2 = c move_sticks(game.selected, game.target2, 1) ++game.prince_eugene if (--game.self === 0) this.next() }, next() { log("Move\nC" + game.selected + "\nC" + game.target2 + "\n" + game.prince_eugene + " sticks.") delete game.prince_eugene pay_for_action(game.selected) end_action_phase() }, } states.s42_marlborough = { inactive: "move sticks", prompt() { view.prompt = "Marlborough: Move 4 sticks to any red or pink card except the Dutch Guards." for (let c of game.front[0]) if (c !== S42_MARLBOROUGH && c !== S42_DUTCH_GUARDS) gen_action_card(c) }, card(c) { log("Move\nC" + game.selected + "\nC" + c + "\n4 sticks.") move_sticks(S42_MARLBOROUGH, c, 4) pay_for_action(S42_MARLBOROUGH) if (get_sticks(S42_MARLBOROUGH) === 0) remove_card(S42_MARLBOROUGH) end_action_phase() }, } function goto_null(c) { log("Null\nC" + c) pay_for_action(c) end_action_phase() } function goto_take_action(c, ix) { let a = data.cards[c].actions[ix] game.selected = c game.action = ix switch (a.type) { case "Attack": if (a.choice) goto_attack_choose_target() else goto_attack(find_target_of_attack(c, a)) break case "Bombard": game.state = "bombard" break case "Command": goto_command() break } } function current_action() { return data.cards[game.selected].actions[game.action] } function find_target_of_attack(c, a) { if (card_has_rule(c, "attack_reserve")) { for (let t of a.target_list) if (is_card_in_play_or_reserve(t)) return t return -1 } if (card_has_rule(c, "ignore_reserve")) { for (let t of a.target_list) if (is_card_in_play(t)) return t return -1 } for (let t of a.target_list) { if (is_card_in_play(t)) return t if (is_card_in_reserve(t)) return -1 } return -1 } function find_target_of_counterattack(a) { for (let t of a.target_list) { if (set_has(game.front[0], t)) return t if (set_has(game.front[1], t)) return t } return -1 } function find_target_of_absorb(a) { for (let t of a.target_list) { if (set_has(game.front[0], t)) return t if (set_has(game.front[1], t)) return t } return -1 } function find_first_target_of_command(c, a) { if (game.scenario === S37_INKERMAN) { if (c === S37_THE_FOG) return S37_THE_FOG } if (game.scenario === S38_FLEURUS) { if (c === S38_WALDECK) return S38_RETREAT_TO_NIVELLES } if (game.scenario === S44_HOHENFRIEDBERG) { if (c === S44_CHARLES) { if (game.reserve[1].length > 0) return game.reserve[1] return -1 } } if (!a.target_list) throw new Error("no rule for Command target: " + a.target) for (let t of a.target_list) { if (is_card_in_reserve(t)) return t } return -1 } function find_all_targets_of_command(c, a) { if (game.scenario === S44_HOHENFRIEDBERG) { if (c === S44_FREDERICK_II) { for (let t of a.target_list) if (is_card_in_reserve(t)) return [ t ] } } let list = [] for (let t of a.target_list) { if (is_card_in_reserve(t)) list.push(t) } return list } states.bombard = { inactive: "bombard", prompt() { view.prompt = "Bombard." view.actions.bombard = 1 }, bombard() { log("Bombard\nC" + game.selected) let opp = 1 - player_index() game.morale[opp] -- pay_for_action(game.selected) end_action_phase() }, } function goto_attack_choose_target() { let a = current_action() let candidates = [] for (let c of a.target_list) { if (set_has(game.front[0], c)) candidates.push(c) if (set_has(game.front[1], c)) candidates.push(c) } if (candidates.length > 1) game.state = "attack_choose_target" else goto_attack(candidates[0]) } states.attack_choose_target = { inactive: "attack", prompt() { view.prompt = "Choose the target of your attack." let a = current_action() for (let c of a.target_list) { if (set_has(game.front[0], c)) gen_action_card(c) if (set_has(game.front[1], c)) gen_action_card(c) } }, card(c) { goto_attack(c) }, } function goto_attack(target) { let take_from = card_has_rule(game.selected, "take_from") if (take_from) { for (let from of take_from) if (has_any_dice_on_card(from)) take_all_dice(from, game.selected) } let take_1_from = card_has_rule(game.selected, "take_1_from") if (take_1_from) { for (let from of take_1_from) if (has_any_dice_on_card(from)) take_one_die(from, game.selected) } game.state = "attack" game.attack_target = target game.target = target update_attack1() update_attack2() } // Update hits and self hits. function update_attack1() { let a = current_action() let n = count_dice_on_card(game.selected) let target = game.attack_target switch (a.effect) { default: throw new Error("invalid attack effect: " + a.effect) case "1 hit per die.": game.self = 0 game.hits = n break case "1 hit per die. 1 self.": game.self = 1 game.hits = n break case "1 hit per die. 1 self. If reduced to one stick, no self hits.": if (get_sticks(game.selected) === 1) game.self = 0 else game.self = 1 game.hits = n break case "1 hit per pair.": game.self = 0 game.hits = n / 2 break case "1 hit per pair. 1 self.": game.self = 1 game.hits = n / 2 break case "1 hit plus 1 hit per die.": game.self = 0 game.hits = 1 + n break case "1 hit plus 1 hit per die. 1 self.": game.self = 1 game.hits = 1 + n break case "1 hit. 1 self.": game.self = 1 game.hits = 1 break case "2 hits per die.": game.self = 0 game.hits = 2 * n break case "2 hits plus 1 hit per die. 1 self.": game.self = 1 game.hits = 2 + n break case "1 hit.": game.self = 0 game.hits = 1 break case "2 hits.": game.self = 0 game.hits = 2 break case "5 hits.": game.self = 0 game.hits = 5 break case "1 hit per die (2 hits per die versus Blenheim). 1 self.": game.self = 1 game.hits = n if (target === S41_BLENHEIM_CARD) game.hits = 2 * n break case "1 hit per die (2 hits per die versus Villars's Left). 1 self.": game.self = 1 game.hits = n if (target === S43_VILLARS_LEFT) game.hits = 2 * n break case "1 hit per die versus Driesen. 2 hits per die versus Retzow.": game.self = 0 if (target === S49_RETZOW) game.hits = 2 * n else game.hits = n break case "1 hit per die. 1 extra hit if Fourth Line is in play.": game.self = 0 game.hits = n if (is_card_in_play(S36_FOURTH_LINE)) game.hits += 1 break case "1 hit per die. 1 self. 1 extra hit if Dutch Horse routed.": game.self = 1 game.hits = n if (is_removed_from_play(S38_DUTCH_HORSE)) game.hits += 1 break case "1 hit per die. 1 self. 1 extra vs Dutch Left Foot.": game.self = 1 game.hits = n if (target === S38_DUTCH_LEFT_FOOT) game.hits += 1 break case "1 hit per die. 1 self. 1 extra vs Essex.": game.self = 1 game.hits = n if (target === S30_ESSEX) game.hits += 1 break case "1 hit per die. 1 self. 1 extra vs Tullibardine.": game.self = 1 game.hits = n if (target === S34_TULLIBARDINE) game.hits += 1 break case "Oxford immediately routs. This attack cannot be screened.": game.self = 0 game.hits = 8 break } if (game.scenario === S2_MARSTON_MOOR) { if (is_card_in_play(S2_RUPERTS_LIFEGUARD)) { if (target === S2_TILLIERS_LEFT) game.hits -= 1 if (target === S2_TILLIERS_RIGHT) game.hits -= 1 } } if (game.scenario === S9_ST_ALBANS) { // Defensive Works (negated by Archers) if (target === S9_SHROPSHIRE_LANE || target === S9_SOPWELL_LANE) { if (is_card_in_play(S9_HENRY_VI)) if (!has_any_cubes_on_card(S9_ARCHERS)) game.hits = Math.min(1, game.hits) } } if (game.scenario === S15_TEWKESBURY) { if (target === S15_SOMERSET) { if (has_any_dice_on_card(S15_A_PLUMP_OF_SPEARS)) game.hits += 1 } } if (game.scenario === S22_GABIENE) { if (target === S22_SILVER_SHIELDS) { if (is_card_in_play(S22_EUMENES_CAMP)) { game.hits = Math.min(1, game.hits - 1) } } } if (game.scenario === S31_NEWBURY_1ST) { if (game.selected === S31_WENTWORTH) { if (has_any_dice_on_card(S31_BYRON) && is_card_in_play(S31_SKIPPON)) { game.hits += 1 } } if (target === S31_WENTWORTH) { if (has_any_dice_on_card(S31_BYRON) && is_card_in_play(S31_SKIPPON)) { game.hits -= 1 } } } if (game.scenario === S37_INKERMAN) { // Until the first Fog Cube is lifted. if (target === S37_SOIMONOFF && get_cubes(S37_THE_FOG) === 3) { game.hits -= 1 } } if (game.scenario === S39_MARSAGLIA) { if (game.selected === S39_EUGENE) { if (has_any_dice_on_card(S39_CANNONS)) { game.self = 0 } } if (game.selected === S39_CATINAT && game.target2 === S39_BAYONETS) { game.self = 0 } } if (game.scenario === S44_HOHENFRIEDBERG) { if (target === S44_CHARLES) { if (game.selected === S44_LEOPOLDS_L || game.selected === S44_LEOPOLDS_C || game.selected === S44_LEOPOLDS_R) game.self = 0 } } // Oblique Attack (CAL expansion rule) if (is_infantry(game.selected)) { if (get_sticks(game.selected) >= get_sticks(target) + 3) game.hits += 1 } let extra = card_has_rule(game.selected, "extra_hit_if_dice_on") if (extra && has_any_dice_on_card(extra[0])) game.hits += 1 // Linked Formations (TGA and CAL expansion rule) if (card_has_active_link(target)) game.hits = Math.max(0, game.hits - 1) if (card_has_rule(target, "suffer_1_less")) game.hits = Math.max(0, game.hits - 1) if (card_has_rule(target, "suffer_1_less_1_max")) game.hits = Math.max(0, Math.min(1, game.hits - 1)) } // Update hits and self hits for defensive abilities that redirect or steal hits. function update_attack2() { if (game.scenario === S28_CULPS_HILL) { if (is_card_in_play(S28_BREASTWORKS)) { if (data.cards[game.target].wing === DKBLUE) { if (game.hits > 0) { game.target2 = S28_BREASTWORKS if (game.hits > 1) { game.hits2 = game.hits - 1 game.hits = 1 } else { game.hits2 = 1 game.hits = 0 } } } } } } states.attack = { inactive: "attack", prompt() { view.prompt = "Attack " + card_name(game.target) + "." gen_action_card(game.target) let w = side_get_wild_die_card(player_index()) if (w >= 0) gen_action_dice_on_card(w) let may_take_from = card_has_rule(game.selected, "may_take_from") if (may_take_from) { for (let from of may_take_from) gen_action_dice_on_card(from) } let may_take_from_extra = card_has_rule(game.selected, "may_take_from_extra_self") if (may_take_from_extra) { for (let from of may_take_from_extra) gen_action_dice_on_card(from) } if (game.scenario === S39_MARSAGLIA) { if (game.selected === S39_CATINAT) { gen_action_dice_on_card(S39_BAYONETS) } } if (game.scenario === S49_LEUTHEN) { if (player_index() === 0) gen_action_dice_on_card(S49_THE_LEUTHEN_CHORALE) } view.actions.attack = 1 }, attack() { log("Attack\nC" + game.selected + "\nC" + game.target) if (can_opponent_react()) { clear_undo() set_opponent_active() game.state = "react" } else { resume_attack() } }, card(_) { this.attack() }, die(d) { let from = get_dice_location(d) let w = side_get_wild_die_card(player_index()) if (w === from) { take_wild_die(w, game.selected) update_attack1() update_attack2() return } let may_take_from = card_has_rule(game.selected, "may_take_from") if (may_take_from) { take_all_dice(from, game.selected) update_attack1() update_attack2() return } let may_take_from_extra = card_has_rule(game.selected, "may_take_from_extra_self") if (may_take_from_extra) { take_all_dice(from, game.selected) game.self2 = 1 update_attack1() update_attack2() return } if (game.scenario === S39_MARSAGLIA) { if (game.selected === S39_CATINAT && from === S39_BAYONETS) { take_all_dice(from, game.selected) game.target2 = S39_BAYONETS update_attack1() update_attack2() return } } if (game.scenario === S49_LEUTHEN) { if (from === S49_THE_LEUTHEN_CHORALE) { take_all_dice(from, game.selected) game.target2 = S49_THE_LEUTHEN_CHORALE update_attack1() update_attack2() return } } throw new Error("no handler for taking dice from other card") } } function resume_attack() { pay_for_action(game.selected) if (game.hits === 1) { if (game.self > 0) log(">1 hit. " + game.self + " self.") else log(">1 hit.") } else { if (game.hits > 0 && game.self > 0) log(">" + game.hits + " hits. " + game.self + " self.") else if (game.hits > 0) log(">" + game.hits + " hits.") else if (game.self > 0) log(">" + game.self + " self.") } if (game.hits > 0) remove_sticks(game.target, game.hits) if (game.self > 0) remove_sticks(game.selected, game.self) if (game.target2 >= 0 && game.hits2 > 0) { log(">" + game.hits2 + " hits on C" + game.target2) remove_sticks(game.target2, game.hits2) } if (game.scenario === S44_HOHENFRIEDBERG) { // remove after first attack if (game.selected === S44_BAYREUTH_DRAGOONS) { remove_card(S44_BAYREUTH_DRAGOONS) } } if (game.scenario === S49_LEUTHEN) { // remove after using if (game.target2 === S49_THE_LEUTHEN_CHORALE) { remove_card(S49_THE_LEUTHEN_CHORALE) } } end_action_phase() } // === COMMAND === function goto_command() { if (game.scenario === S37_INKERMAN && game.selected === S37_THE_FOG) { log("The Fog Lifts...") remove_cubes(S37_THE_FOG, 1) remove_dice(game.selected) end_action_phase() return } if (game.scenario === S38_FLEURUS && game.selected === S38_WALDECK) { remove_cubes(S38_RETREAT_TO_NIVELLES, 1) remove_dice(game.selected) if (!has_any_cubes_on_card(S38_RETREAT_TO_NIVELLES)) { game.selected = S38_RETREAT_TO_NIVELLES game.state = "s38_retreat_to_nivelles_1" game.starting_on_your_next_turn = 0 return } end_action_phase() return } game.state = "command" } states.command = { inactive: "command", prompt() { let list = find_all_targets_of_command(game.selected, current_action()) view.prompt = "Bring " + list.map(c => card_name(c)).join(" and ") + " out of reserve." for (let t of list) gen_action_card(t) }, card(c) { log("Command\nC" + game.selected + "\nC" + c) if (game.scenario === S4_BOSWORTH_FIELD) { if (c === S4_THE_STANLEYS) { if (is_card_in_play_or_reserve(S4_NORTHUMBERLAND)) { log("The Stanleys rout Northumberland.") set_sticks(S4_NORTHUMBERLAND, 0) } } } if (game.scenario === S13_EDGECOTE_MOOR) { if (game.reserve[0].length === 2 && game.reserve[1].length === 1) { let p = player_index() log("Gained a second morale cube.") game.morale[p] += 1 } } if (game.scenario === S37_INKERMAN) { if (c === S37_PAULOFFS_LEFT) { log("Morale Cube added to Russian side.") game.morale[0] += 1 } } let p = player_index() set_delete(game.reserve[p], c) set_add(game.front[p], c) if (game.scenario === S44_HOHENFRIEDBERG) { // one at a time if (game.selected === S44_CHARLES || game.selected === S44_FREDERICK_II) { pay_for_action(game.selected) end_action_phase() return } } if (find_first_target_of_command(game.selected, current_action()) < 0) { pay_for_action(game.selected) end_action_phase() } }, } states.s38_retreat_to_nivelles_1 = { inactive: "remove cards", prompt() { view.prompt = "Retreat to Nivelles: Remove all in-play Grand Alliance cards!" for (let c of game.front[1]) if (c !== S38_RETREAT_TO_NIVELLES) gen_action_card(c) }, card(c) { move_all_sticks(c, S38_RETREAT_TO_NIVELLES) remove_card(c) if (game.front[1].length === 1) game.state = "s38_retreat_to_nivelles_2" }, } states.s38_retreat_to_nivelles_2 = { inactive: "enter reserves", prompt() { view.prompt = "Retreat to Nivelles: Bring van Aylva & van Weibnom out of reserve." for (let c of game.reserve[1]) gen_action_card(c) }, card(c) { bring_out_of_reserve(c) if (game.reserve[1].length === 0) end_action_phase() }, } // === REACTION === function can_opponent_react() { if (game.scenario === S39_MARSAGLIA) { if (game.selected === S39_CATINAT && game.target2 === S39_BAYONETS) return false } if (game.scenario === S14_BARNET) { if (game.selected === S14_TREASON) return false } let p = 1 - player_index() let wild = side_has_wild_die(p) for (let c of game.front[p]) if (can_card_react(c, wild)) return true return false } function can_card_react(c, wild) { let has_dice = has_any_dice_on_card(c) let has_cube = has_any_cubes_on_card(c) if (has_dice || has_cube) { if (data.cards[c].actions.length >= 1) if (is_reaction(c, data.cards[c].actions[0])) if (can_take_reaction(c, data.cards[c].actions[0], wild)) return true if (data.cards[c].actions.length >= 2) if (is_reaction(c, data.cards[c].actions[1])) if (can_take_reaction(c, data.cards[c].actions[1], wild)) return true } return false } function can_take_reaction(c, a, wild) { switch (a.type) { default: throw new Error("invalid reaction: " + a.type) case "Screen": // if a friendly formation is attacked by a listed enemy formation // ... or a listed formation is attacked (Wheatfield Road Artillery, etc) if (!a.target_list.includes(game.selected) && !a.target_list.includes(game.target)) return false break case "Counterattack": // if this formation is attacked if (game.target !== c) return false // ... by one of the listed targets if (a.choice) { // if "any" or "or" choice if (!a.target_list.includes(game.selected)) return false } else { // if strict order if (find_target_of_counterattack(a) !== game.selected) return false } break case "Absorb": // if attack target is listed on absorb action if (a.choice) { if (!a.target_list.includes(game.target)) return false } else { if (find_target_of_absorb(a) !== game.target) return false } break } if (game.scenario === S7_THE_DUNES) { if (has_any_cubes_on_card(S7_THE_ENGLISH_FLEET)) { if (c === S7_DON_JUAN_JOSE) return false if (c === S7_SPANISH_RIGHT_CAVALRY) return false } } if (game.scenario === S15_TEWKESBURY) { if (c === S15_WENLOCK) { if (is_removed_from_play(S15_SOMERSET)) return false } } if (game.scenario === S31_NEWBURY_1ST) { if (c === S31_GERARD) { // London Trained Bands enter play when Skippon routs if (is_removed_from_play(S31_SKIPPON)) return false } } if (game.scenario === S43_DENAIN) { if (c === S43_DUTCH_HORSE) { // May only screen Villars's Left if Broglie has routed if (game.target === S43_VILLARS_LEFT) if (is_card_in_play(S43_BROGLIE)) return false } } if (data.cards[c].special) return check_cube_requirement(c, a.requirement) else return check_dice_requirement(c, a.requirement, wild) } function take_wild_die_if_needed_for_reaction(c, ix) { let w = side_get_wild_die_card(player_index()) if (w >= 0) { let a = data.cards[c].actions[ix] if (!can_take_reaction(c, a, false)) { take_wild_die(w, c) } } } states.react = { inactive: "react", prompt() { view.prompt = card_name(game.selected) + " attacks " + card_name(game.target) + "!" let voluntary = true let p = player_index() let wild = side_has_wild_die(p) for (let c of game.front[p]) { let has_dice = has_any_dice_on_card(c) let has_cube = has_any_cubes_on_card(c) if (has_dice || has_cube) { for (let i = 0; i < data.cards[c].actions.length; ++i) { let a = data.cards[c].actions[i] if (is_reaction(c, a)) { let must = false let may = false if (is_mandatory_reaction(c, a)) { must = can_take_reaction(c, a, false) if (!must && wild && can_take_reaction(c, a, true)) may = true } else { may = can_take_reaction(c, a, wild) } if (must) voluntary = false if (must || may) { if (i === 0) gen_action_action1(c) if (i === 1) gen_action_action2(c) } } } } } if (voluntary) view.actions.pass = 1 }, a1(c) { push_undo() take_wild_die_if_needed_for_reaction(c, 0) goto_take_reaction(c, 0) }, a2(c) { push_undo() take_wild_die_if_needed_for_reaction(c, 1) goto_take_reaction(c, 1) }, pass() { set_opponent_active() resume_attack() }, } function goto_take_reaction(c, ix) { let a = data.cards[c].actions[ix] switch (a.type) { case "Screen": goto_screen(c, a) break case "Absorb": goto_absorb(c, a) break case "Counterattack": goto_counterattack(c, a) break } } function end_reaction() { clear_undo() set_opponent_active() resume_attack() } // === SCREEN === function goto_screen(c, a) { game.reacted = player_index() game.target = c update_attack1() switch (a.effect) { default: throw new Error("invalid screen effect: " + a.effect) case undefined: game.hits = 0 game.self = 0 break case "If either Chariot formation is screened, it suffers 1 hit!": game.hits = 0 if (card_has_rule(game.selected, "is_chariot")) game.self = 1 else game.self = 0 break } update_attack2() game.state = "screen" } states.screen = { inactive: "screen", prompt() { view.prompt = "Screen attack from " + card_name(game.selected) + "." view.actions.screen = 1 gen_action_card(game.selected) }, screen() { log("Screen\nC" + game.target) pay_for_action(game.target) if (card_has_rule(game.target, "remove_after_screen")) remove_card(game.target) end_reaction() }, card(_) { this.screen() }, } // === ABSORB === function goto_absorb(c, a) { game.reacted = player_index() if (game.scenario === S29_GETTYS_2ND) { if (c === S29_MEADE) { game.state = "s29_meade" return } } game.target = c update_attack1() switch (a.effect) { default: throw new Error("invalid absorb effect: " + a.effect) case "Suffers hits.": break case "Suffers 1 hit only.": game.hits = 1 break case "Suffers 1 less hit.": game.hits = Math.max(0, game.hits - 1) break case "Suffers 1 less hit per die.": game.hits = Math.max(0, game.hits - count_dice_on_card(c)) break } update_attack2() game.state = "absorb" } states.s29_meade = { inactive: "absorb", prompt() { view.prompt = "Choose any friendly Formation except Little Round Top to absorb the hits instead." let p = player_index() for (let c of game.front[p]) { if (c !== S29_MEADE && c !== S29_LITTLE_ROUND_TOP && c !== game.target) gen_action_card(c) } }, card(c) { remove_dice(S29_MEADE) game.target = c update_attack1() update_attack2() game.state = "absorb" } } states.absorb = { inactive: "absorb", prompt() { view.prompt = "Absorb attack from " + card_name(game.selected) + "." view.actions.absorb = 1 gen_action_card(game.selected) }, absorb() { log("Absorb\nC" + game.target) pay_for_action(game.target) end_reaction() }, card(_) { this.absorb() }, } // === COUNTERATTACK === function goto_counterattack(c, a) { game.reacted = player_index() update_attack1() switch (a.effect) { default: throw new Error("invalid counterattack effect: " + a.effect) case "1 hit.": game.self += 1 break case "1 hit per die.": game.self += count_dice_on_card(c) break case "1 hit. Suffers 1 hit only.": game.self += 1 game.hits = 1 break case "Suffers 1 less hit.": game.hits = Math.max(0, game.hits - 1) break case "1 hit. Suffers 1 less hit.": game.self += 1 game.hits = Math.max(0, game.hits - 1) break case "1 hit. Suffers 1 less hit per die.": game.self += 1 game.hits = Math.max(0, game.hits - count_dice_on_card(c)) break case "1 hit. Suffers 1 less hit and never more than 1.": game.self += 1 game.hits = Math.max(0, Math.min(1, game.hits - 1)) break case "1 hit. Suffers 2 less hits and never more than 1.": game.self += 1 game.hits = Math.max(0, Math.min(1, game.hits - 2)) break } if (game.scenario === S48_BRESLAU) { if (player_index() === 1) { if (has_any_dice_on_card(S48_BEVERN)) game.self += 1 } } update_attack2() game.state = "counterattack" } states.counterattack = { inactive: "counterattack", prompt() { view.prompt = "Counterattack " + card_name(game.selected) + "." view.actions.counterattack = 1 gen_action_card(game.selected) }, counterattack() { log("Counterattack\nC" + game.target) pay_for_action(game.target) end_reaction() }, card(_) { this.counterattack() }, } // === ROUTING/PURSUIT/REMOVE/FORCE-RETIRE === function find_card_owner(c) { if (set_has(game.front[0], c) || set_has(game.reserve[0], c)) return 0 if (set_has(game.front[1], c) || set_has(game.reserve[1], c)) return 1 throw new Error("card not found in any player area") } function should_rout_card(c) { if (!data.cards[c].special) { if (get_sticks(c) === 0) return true } let rout_with = card_has_rule(c, "rout_with") if (rout_with) { for (let other of rout_with) if (!is_removed_from_play(other)) return false return true } return false } function should_pursue(c) { let pursuit = data.cards[c].pursuit if (pursuit !== undefined) return !set_has(game.front[0], pursuit) && !set_has(game.front[1], pursuit) return false } function should_remove_card(c) { let remove_with = card_has_rule(c, "remove_with") if (remove_with) { for (let other of remove_with) if (!is_removed_from_play(other)) return false return true } return false } function should_retire_card(c) { let retire_with = card_has_rule(c, "retire_with") if (retire_with) { for (let other of retire_with) if (!is_removed_from_play(other)) return false return true } if (game.scenario === S25_WHEATFIELD) { if (c === S25_ZOOK || c === S25_KELLY) { if (is_card_in_play(S25_WOFFORD)) return true } } return false } function goto_routing() { game.routed = [ 0, 0 ] if (game.scenario === S2_MARSTON_MOOR) { if (is_card_in_play(S2_RUPERTS_LIFEGUARD)) { if (is_card_in_play(S2_NORTHERN_HORSE) && should_rout_card(S2_NORTHERN_HORSE)) { game.state = "s2_ruperts_lifeguard" game.selected = S2_NORTHERN_HORSE return } if (is_card_in_play(S2_BYRON) && should_rout_card(S2_BYRON)) { game.state = "s2_ruperts_lifeguard" game.selected = S2_BYRON return } } } resume_routing() } states.s2_ruperts_lifeguard = { inactive: "remove cards", prompt() { view.prompt = "Rupert's Lifeguard: Add the Lifeguard's Unit to " + card_name(game.selected) gen_action_card(S2_RUPERTS_LIFEGUARD) }, card(c) { log(`Move\nC${S2_RUPERTS_LIFEGUARD}\nC${game.selected}\n1 stick.`) move_all_sticks(S2_RUPERTS_LIFEGUARD, game.selected) remove_card(S2_RUPERTS_LIFEGUARD) game.selected = -1 resume_routing() } } function resume_routing() { game.state = "routing" for (let p = 0; p <= 1; ++p) { for (let c of game.front[p]) if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c) || should_pursue(c)) return for (let c of game.reserve[p]) if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c)) return } end_routing() } states.routing = { inactive: "remove routing and pursuing cards", prompt() { view.prompt = "Routing: Remove routing and pursuing cards from play!" for (let p = 0; p <= 1; ++p) { for (let c of game.front[p]) if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c) || should_pursue(c)) gen_action_card(c) for (let c of game.reserve[p]) if (should_rout_card(c) || should_remove_card(c) || should_retire_card(c)) gen_action_card(c) } }, card(c) { if (should_rout_card(c)) { let p = find_card_owner(c) game.routed[p] += data.cards[c].morale rout_card(c) } else if (should_retire_card(c)) { log("Retire\nC" + c) retire_card(c) } else if (should_pursue(c)) { log("Pursuit\nC" + c) pursue_card(c) } else { remove_card(c) } resume_routing() }, } function end_routing() { // Normal morale loss and gain if (game.morale[0] > 0 && game.morale[1] > 0) { if ((game.routed[0] > 0 && !game.routed[1]) || (game.routed[1] > 0 && !game.routed[0])) { if (game.routed[0]) { game.routed[0] = Math.min(game.routed[0], game.morale[0]) game.morale[0] -= game.routed[0] // do not gain for special scenarios if (game.morale[1] > 0) game.morale[1] += game.routed[0] } else { game.routed[1] = Math.min(game.routed[1], game.morale[1]) game.morale[1] -= game.routed[1] // do not gain for special scenarios if (game.morale[0] > 0) game.morale[0] += game.routed[1] } } if (check_victory()) return } else { // SPECIAL: S3 - Plains of Abraham // SPECIAL: S34 - Tippermuir - Royalists // SPECIAL: S35 - Auldearn - Royalists // Instant loss if any card routs for side at 0 morale (S3, S34, S35). if (game.morale[0] === 0 && game.routed[0]) return goto_game_over(P2, player_name(0) + " routed!") if (game.morale[1] === 0 && game.routed[1]) return goto_game_over(P1, player_name(1) + " routed!") // Remove instead of take cubes for side at 0 morale if (game.routed[0]) { game.morale[0] -= Math.min(game.routed[0], game.morale[0]) if (game.morale[0] === 0) return goto_game_over(P2, player_name(0) + " ran out of morale!") } if (game.routed[1]) { game.morale[1] -= Math.min(game.routed[1], game.morale[1]) if (game.morale[1] === 0) return goto_game_over(P1, player_name(1) + " ran out of morale!") } } game.routed = null goto_reserve() } // === RESERVE === function should_enter_reserve(c) { let reserve = data.cards[c].reserve if (game.scenario === S37_INKERMAN) { if (c === S37_BRITISH_TROOPS) return get_cubes(S37_THE_FOG) === 1 if (c === S37_FRENCH_TROOPS) return get_cubes(S37_THE_FOG) === 0 } if (Array.isArray(reserve)) { for (let t of reserve) { if (is_removed_from_play(t)) return true } } if (game.scenario === S30_EDGEHILL) { if (c === S30_BALFOUR || c === S30_STAPLETON) { return is_removed_from_play(S30_RUPERT) && is_removed_from_play(S30_WILMOT) } } return false } function goto_reserve() { resume_reserve() } function resume_reserve() { game.state = "reserve" for (let p = 0; p <= 1; ++p) for (let c of game.reserve[p]) if (should_enter_reserve(c)) return end_reserve() } function end_reserve() { goto_roll_phase() } function bring_out_of_reserve(c) { log("Reserve\nC" + c) let p = find_card_owner(c) set_delete(game.reserve[p], c) set_add(game.front[p], c) } states.reserve = { inactive: "enter reserves", prompt() { view.prompt = "Enter reserves!" for (let p = 0; p <= 1; ++p) for (let c of game.reserve[p]) if (should_enter_reserve(c)) gen_action_card(c) }, card(c) { bring_out_of_reserve(c) resume_reserve() }, } // === COMMON LIBRARY === function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [ argument ] else set_add(view.actions[action], argument) } function gen_action_dice_on_card(c) { for (let d = 0; d < 12; ++d) { if (get_dice_location(d) === c) { gen_action_die(d) return } } } function gen_action_card(c) { gen_action("card", c) } function gen_action_die(d) { gen_action("die", d) } function gen_action_action1(c) { gen_action("a1", c) } function gen_action_action2(c) { gen_action("a2", c) } function gen_action_null1(c) { gen_action("n1", c) } function gen_action_null2(c) { gen_action("n2", c) } function gen_action_retire(c) { gen_action("retire", c) } function log(msg) { game.log.push(msg) } function clear_undo() { if (game.undo) { game.undo.length = 0 } } function push_undo() { if (game.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() { if (game.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) { // Largest MLCG that will fit its state in a double. // Uses BigInt for arithmetic, so is an order of magnitude slower. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**53 - 111 return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % 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(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 } } } // Map as plain sorted array of key/value pairs function map_clear(map) { map.length = 0 } 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 } } }