"use strict" /* Special scenario rules implemented: zero morale - instant loss Special card rules implemented: place_2_blue place_2_red place_2_blue_if_dice place_2_red_if_dice remove_after_screen suffer_1_less_1_max suffer_1_less start_with_no_cubes take_from rout_with remove_with wild TODO: attack_reserve attack_choose_target take_from TODO: extra input steps may_take_from */ // TODO: morale cube limit (cannot place on special if maxed) // TODO: fizzle when action says to take cards from other dice? 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) console.log(c.dice) 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 } 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, front: game.front, reserve: game.reserve, selected: game.selected, target: game.target, hits: game.hits, self: game.self, } 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 } exports.resign = function (state, player) { game = state if (game.state !== 'game_over') { if (player === P1) return goto_game_over(P2, P1 + " resigned.") if (player === P2) return goto_game_over(P1, P2 + " resigned.") } } function goto_game_over(result, victory) { game.state = "game_over" game.active = "None" game.result = result game.victory = victory log("") log(game.victory) return false } 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 S37_INKERMAN = find_scenario(37) const S37_PAULOFFS_LEFT = find_card(37, "Pauloff's Left") const S37_PAULOFFS_RIGHT = find_card(37, "Pauloff's Right") 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 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 S9_WARWICK = find_card(9, "Warwick") 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 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") // === SETUP === exports.setup = function (seed, scenario, options) { // TODO: "Random" 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: 0, // 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 ], front: [ [], [], ], reserve: [ [], [] ], // dice value placed on what card placed: [], // current action routed: [ 0, 0 ], selected: -1, target: -1, hits: 0, self: 0, } 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 { add_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("") if (info.rule_text) log(info.rule_text) if (info.players[0].tactical > 0 || info.players[1].tactical > 0) { 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) } if (game.scenario === S37_INKERMAN) { map_set(game.cubes, S37_THE_FOG, 3) } goto_action_phase() 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 card_has_rule(c, name) { let rules = data.cards[c].rules if (rules) return rules[name] return false } function card_number(c) { return data.cards[c].number } function card_name(c) { return data.cards[c].name } function player_name() { let p = player_index() 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 add_cubes(c, n) { let limit = data.cards[c].special let old = map_get(game.cubes, c, 0) map_set(game.cubes, c, Math.min(limit, old + n)) } function remove_cubes(c, n) { let old = map_get(game.cubes, c, 0) map_set(game.cubes, c, Math.max(0, old - n)) } function add_sticks(c, n) { let old = map_get(game.sticks, c, 0) map_set(game.sticks, c, old + n) } function remove_sticks(c, n) { let old = map_get(game.sticks, c, 0) map_set(game.sticks, c, Math.max(0, 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 move_dice(from, to) { for (let i = 0; i < 12; ++i) { if (get_dice_location(i) === from) { set_dice_location(i, to) } } } function take_one_die(from, to) { 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) { 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) remove_cubes(c, 3) 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 pay_for_action(c) { if (data.cards[c].special) remove_cubes(c, 1) else remove_dice(c) } function get_player_dice_value(p, i) { return game.dice[p * 12 + i * 2] } function get_player_dice_location(p, i) { return game.dice[p * 12 + i * 2 + 1] } function set_player_dice_value(p, i, v) { game.dice[p * 12 + i * 2] = v } function set_player_dice_location(p, i, v) { game.dice[p * 12 + i * 2 + 1] = v } 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_front(c) { return ( set_has(game.front[0], c) || set_has(game.front[1], c) ) } function is_card_in_play(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_routed(c) { return !is_card_in_play(c) } function is_card_attack_with_target_in_play(c) { for (let a of data.cards[c].actions) { if (a.type === "Attack") { for (let t of a.target_list) if (is_card_in_play(t)) return true } } return false } function check_impossible_to_attack_victory() { let p = player_index() for (let c of game.front[p]) if (is_card_attack_with_target_in_play(c)) return false return true } // === ROLL PHASE === function is_pool_die(i, v) { let p = player_index() return get_player_dice_location(p, i) < 0 && get_player_dice_value(p, i) === v } function is_pool_die_range(i, lo, hi) { let p = player_index() if (get_player_dice_location(p, i) < 0) { let v = get_player_dice_value(p, i) return v >= lo && v <= hi } return false } 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 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, 2), "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, 2), "(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 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, 2), "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, 2), "(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 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 (map_get(game.cubes, c, 0) >= 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_routed(S8_GRANT)) return false if (is_routed(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 } } 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_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_player_dice_location(p, i) < 0 && get_player_dice_value(p, 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_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) { if (get_player_dice_location(p, i) < 0 && get_player_dice_value(p, i) === v) { set_player_dice_location(p, i, c) return } } throw new Error("cannot find die of value " + v) } function take_single(c, d) { set_dice_location(d, c) 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_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.target = -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 /* // NOT USED (YET) if (card_has_rule(c, "place_2_dkblue")) game.place_max[DKBLUE] = 2 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 */ } } states.roll = { prompt() { view.prompt = "Roll the dice in your pool." view.actions.roll = 1 }, roll() { clear_undo() roll_dice_in_pool() }, } function roll_dice_in_pool() { let p = player_index() for (let i = 0; i < 6; ++i) if (get_player_dice_location(p, i) < 0) set_player_dice_value(p, i, random(6) + 1) 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 = { 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 = { 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() { clear_undo() map_clear(game.placed) game.place_max = null // 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, map_get(game.cubes, c, 0) + 1)) remove_dice(c) } } // Blank out unused dice. let p = player_index() for (let i = 0; i < 6; ++i) if (get_player_dice_location(p, i) < 0) set_player_dice_value(p, i, 0) set_opponent_active() goto_action_phase() } // === 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 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 map_get(game.cubes, c, 0) >= 1 } 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": return map_get(game.cubes, c, 0) >= 3 case "Voluntary": case undefined: return map_get(game.cubes, c, 0) >= 1 default: throw new Error("invalid action requirement: " + req) } } function check_dice_requirement(c, req, wild) { switch (req) { 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 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) { return ( a.requirement !== "Voluntary" && a.requirement !== "Pair, Voluntary" ) } function can_take_action(c, a) { 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 (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 (a.type === "Bombard" || a.type === "Attack" || a.type === "Command") { if (data.cards[c].special) return check_cube_requirement(c, a.requirement) else return check_dice_requirement(c, a.requirement, false) } return false } function can_take_any_action() { let p = player_index() for (let c of game.front[p]) { if (has_any_dice_on_card(c)) return true if (has_any_cubes_on_card(c)) // TODO: check requirements! return true } return false } function goto_action_phase() { if (check_impossible_to_attack_victory()) { if (player_index() === 0) goto_game_over(P2, P1 + " has no more attacks!") else goto_game_over(P1, P2 + " has no more attacks!") return } if (game.reacted) { game.reacted = 0 goto_roll_phase() } else { if (can_take_any_action()) game.state = "action" else goto_roll_phase() } } function end_action_phase() { game.hits = game.self = 0 game.selected = -1 game.target = -1 goto_routing() } states.action = { prompt() { view.prompt = "Take an action." view.actions.roll = 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])) gen_action_action1(c) else if (has_dice) gen_action_fizzle1(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])) gen_action_action2(c) else if (has_dice) gen_action_fizzle2(c) } } if (data.cards[c].retire) gen_action_retire(c) } } }, retire(c) { push_undo() log(card_name(c) + " retired.") eliminate_card(c) end_action_phase() }, a1(c) { push_undo() goto_take_action(c, 0) }, a2(c) { push_undo() goto_take_action(c, 1) }, f1(c) { push_undo() goto_fizzle(c) }, f2(c) { push_undo() goto_fizzle(c) }, roll() { clear_undo() goto_roll_phase() roll_dice_in_pool() }, } function goto_fizzle(c) { log("Fizzled " + card_number(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 (card_has_rule(game.selected, "attack_choose_target")) 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) { let in_res = card_has_rule(c, "attack_reserve") for (let c of a.target_list) { if (set_has(game.front[0], c)) return c if (set_has(game.front[1], c)) return c if (in_res) { if (set_has(game.reserve[0], c)) return c if (set_has(game.reserve[1], c)) return c } } return -1 } function find_first_target_of_command(c, a) { if (game.scenario === S37_INKERMAN) { if (c === S37_THE_FOG) return S37_THE_FOG } for (let t of a.target_list) { if (set_has(game.reserve[0], t)) return t if (set_has(game.reserve[1], t)) return t } return -1 } function find_all_targets_of_command(a) { let list = [] for (let c of a.target_list) { if (set_has(game.reserve[0], c)) list.push(c) if (set_has(game.reserve[1], c)) list.push(c) } return list } states.bombard = { prompt() { view.prompt = "Bombard." view.actions.bombard = 1 }, bombard() { log(card_name(game.selected) + " bombarded.") 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 = { 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 a = current_action() 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)) move_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.target = target game.hits = get_attack_hits(game.selected, a) game.self = get_attack_self(game.selected, a) if (game.scenario === S2_MARSTON_MOOR) { if (is_card_in_play(S2_RUPERTS_LIFEGUARD)) { if (game.target === S2_TILLIERS_LEFT) game.hits -= 1 if (game.target === S2_TILLIERS_RIGHT) game.hits -= 1 } } if (game.scenario === S37_INKERMAN) { // Until the first Fog Cube is lifted. if (map_get(game.cubes, S37_THE_FOG, 0) === 3) { game.hits -= 1 } } if (game.scenario === S9_ST_ALBANS) { // Defensive Works (negated by Archers) if (game.target === S9_SHROPSHIRE_LANE || game.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 (game.target === S15_SOMERSET) { if (has_any_dice_on_card(S15_A_PLUMP_OF_SPEARS)) game.hits += 1 } } if (game.scenario === S22_GABIENE) { if (game.target === S22_SILVER_SHIELDS) { if (is_card_in_play(S22_EUMENES_CAMP)) { game.hits = Math.min(1, game.hits - 1) } } } if (card_has_rule(game.target, "suffer_1_less_1_max")) game.hits = Math.max(0, Math.min(1, game.hits - 1)) if (card_has_rule(game.target, "suffer_1_less")) game.hits = Math.max(0, game.hits - 1) } states.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) } view.actions.attack = 1 }, attack() { log(card_name(game.selected) + " attacked " + card_name(game.target) + ".") if (can_opponent_react()) { clear_undo() set_opponent_active() game.state = "react" } else { resume_attack() } }, card(_) { this.attack() }, die(d) { let w = side_get_wild_die_card(player_index()) if (w === get_dice_location(d)) { log("Wild die from C" + w + ".") take_wild_die(w, game.selected) } let may_take_from = card_has_rule(game.selected, "may_take_from") if (may_take_from) { move_dice(get_dice_location(d), game.selected) goto_attack(game.target) // recompute hits } } } function resume_attack() { apply_hits(game.hits) apply_self(game.self) pay_for_action(game.selected) 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 } game.state = "command" } states.command = { prompt() { let list = find_all_targets_of_command(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(card_name(game.selected) + " commanded " + card_name(c) + " out of reserve.") let p = player_index() set_delete(game.reserve[p], c) set_add(game.front[p], c) if (game.scenario === S4_BOSWORTH_FIELD) { if (c === S4_THE_STANLEYS) { if (is_card_in_play(S4_NORTHUMBERLAND)) { log("The Stanleys rout Northumberland.") map_set(game.sticks, S4_NORTHUMBERLAND, 0) } } } if (game.scenario === S37_INKERMAN) { if (c === S37_PAULOFFS_LEFT) { log("Morale Cube added to Russian side.") game.morale[0] += 1 } } if (game.scenario === S13_EDGECOTE_MOOR) { // TODO: pay all 3 cubes? remove cards from play? if (game.reserve[0].length === 0 && game.reserve[1].length > 0) { log("Gained a second morale cube.") game.morale[0] += 1 } if (game.reserve[1].length === 0 && game.reserve[0].length > 0) { log("Gained a second morale cube.") game.morale[1] += 1 } } if (find_first_target_of_command(game.selected, current_action()) < 0) { pay_for_action(game.selected) end_action_phase() } }, } // === REACTION === function can_opponent_react() { 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.target_list.includes(game.selected)) return false break case "Absorb": // if attack target is listed on absorb action if (!a.target_list.includes(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_routed(S15_SOMERSET)) 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)) { log("Wild die from C" + w + ".") take_wild_die(w, c) } } } states.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() { set_opponent_active() resume_attack() } // === SCREEN === function goto_screen(c, a) { game.reacted = 1 game.target = c 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 one Hit!": game.hits = 0 if (card_has_rule(game.selected, "is_chariot")) game.self = 1 else game.self = 0 break } game.state = "screen" } states.screen = { prompt() { view.prompt = "Screen attack from " + card_name(game.selected) + "." view.actions.screen = 1 }, screen() { log(card_name(game.target) + " screened.") pay_for_action(game.target) if (card_has_rule(game.target, "remove_after_screen")) eliminate_card(game.target) end_reaction() }, } // === ABSORB === function goto_absorb(c, a) { game.reacted = 1 game.target = c switch (a.effect) { default: throw new Error("invalid absorb effect: " + a.effect) case "When target suffers Hits, this card suffers them instead.": case "When target suffers Hits, this unit suffers them instead.": break case "When target suffers Hits, this card suffers 1 hit ONLY instead.": case "When target suffers Hits, this unit suffers 1 hit ONLY instead.": game.hits = 1 break case "When target suffers Hits, this card suffers 1 less hit per die.": game.hits = Math.max(0, game.hits - count_dice_on_card(c)) break } game.state = "absorb" } states.absorb = { prompt() { view.prompt = "Absorb attack from " + card_name(game.selected) + "." view.actions.absorb = 1 }, absorb() { log(card_name(game.target) + " absorbed.") pay_for_action(game.target) end_reaction() }, } // === COUNTERATTACK === function goto_counterattack(c, a) { game.reacted = 1 switch (a.effect) { default: throw new Error("invalid counterattack effect: " + a.effect) case "1 hit per die.": game.self += count_dice_on_card(c) break case "1 hit.": game.self += 1 break case "1 hit. Additionally, this unit only suffers one hit.": game.self += 1 game.hits = 1 break case "1 hit. Additionally, this unit suffers one less hit per die.": game.self += 1 game.hits = Math.max(0, game.hits - count_dice_on_card(c)) break case "1 hit. Additionally, this unit suffers one less hit.": game.self += 1 game.hits -= 1 break case "This unit suffers ONE less hit and never more than one.": game.self += 1 game.hits = Math.max(0, Math.min(1, game.hits - 1)) break case "This unit suffers TWO less hits and never more than one.": game.self += 1 game.hits = Math.max(0, Math.min(1, game.hits - 2)) break } game.state = "counterattack" } states.counterattack = { prompt() { view.prompt = "Counterattack " + card_name(game.selected) + "." view.actions.counterattack = 1 }, counterattack() { log(card_name(game.target) + " counterattacked.") pay_for_action(game.target) end_reaction() }, } // === ATTACK EFFECTS === function apply_self(n) { remove_sticks(game.selected, n) } function apply_hits(n) { remove_sticks(game.target, n) } function get_attack_hits(c, a) { switch (a.effect) { default: throw new Error("invalid attack effect: " + a.effect) case "1 hit.": case "1 hit. Warwick Retires upon completing this Attack Action.": case "1 hit. You CHOOSE the target.": case "1 hit. 1 self per action.": case "1 hit per action. 1 self per action.": return 1 case "1 hit per die.": case "1 hit per die. 1 self per action.": case "1 hit per die. Ignore first target until it comes out of Reserve.": case "1 hit per die (but see below). 1 self per action.": case "1 hit per die (plus dice from E. Phalanx).": case "1 hit per die. 1 self per action. (But see Sharpshooters.)": case "1 hit per die. 1 self per action. (But see 4th Alabama.)": return count_dice_on_card(c) case "1 hit per pair.": case "1 hit per pair. 1 self per action.": return count_dice_on_card(c) >> 1 case "1 hit, PLUS 1 hit per die. 1 self per action.": return 1 + count_dice_on_card(c) case "5 hits.": return 5 } } function get_attack_self(c, a) { switch (a.effect) { default: throw new Error("invalid attack effect: " + a.effect) case "1 hit.": case "1 hit. Warwick Retires upon completing this Attack Action.": case "1 hit. You CHOOSE the target.": case "1 hit per die.": case "1 hit per die. Ignore first target until it comes out of Reserve.": case "1 hit per die (plus dice from E. Phalanx).": case "1 hit per pair.": case "5 hits.": return 0 case "1 hit. 1 self per action.": case "1 hit per action. 1 self per action.": case "1 hit per die. 1 self per action.": case "1 hit per die (but see below). 1 self per action.": case "1 hit per die. 1 self per action. (But see Sharpshooters.)": case "1 hit per die. 1 self per action. (But see 4th Alabama.)": case "1 hit per pair. 1 self per action.": case "1 hit, PLUS 1 hit per die. 1 self per action.": return 1 } } // === ROUTING === 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 (map_get(game.sticks, c, 0) === 0) return true let rout_with = card_has_rule(c, "rout_with") if (rout_with) { for (let other of rout_with) if (is_card_in_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_card_in_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_card_in_play(other)) return false return true } return false } function goto_routing() { game.routed = [ 0, 0 ] if (game.scenario === S2_MARSTON_MOOR) { // TODO: pause with separate state? if (is_card_in_play(S2_RUPERTS_LIFEGUARD)) { if (should_rout_card(S2_NORTHERN_HORSE)) { log("Rupert's Lifeguard added to Northern Horse.") map_set(game.sticks, S2_NORTHERN_HORSE, 1) eliminate_card(S2_RUPERTS_LIFEGUARD) } if (should_rout_card(S2_BYRON)) { log("Rupert's Lifeguard added to Byron.") map_set(game.sticks, S2_BYRON, 1) eliminate_card(S2_RUPERTS_LIFEGUARD) } } } 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)) return for (let c of game.reserve[p]) if (should_rout_card(c)) return } end_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 (game.morale[0] === 0) return goto_game_over(P2, P1 + " has run out of morale!") if (game.morale[1] === 0) return goto_game_over(P1, P2 + " has run out of morale!") } 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, P1 + " card routed!") if (game.morale[1] === 0 && game.routed[1]) return goto_game_over(P1, P2 + " card 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, P1 + " has run 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, P2 + " has run out of morale!") } } game.routed = null goto_pursuit() } states.routing = { prompt() { view.prompt = "Rout cards!" for (let p = 0; p <= 1; ++p) { for (let c of game.front[p]) if (should_rout_card(c)) gen_action_card(c) for (let c of game.reserve[p]) if (should_rout_card(c)) gen_action_card(c) } }, card(c) { if (should_rout_card(c)) { log(card_name(c) + " routed.") let p = find_card_owner(c) game.routed[p] += data.cards[c].morale } else if (should_retire_card(c)) { log(card_name(c) + " retired.") } else { log(card_name(c) + " removed.") } eliminate_card(c) resume_routing() }, } // === PURSUIT === function goto_pursuit() { resume_pursuit() } function resume_pursuit() { game.state = "pursuit" for (let p = 0; p <= 1; ++p) for (let c of game.front[p]) if (should_pursue(c)) return end_pursuit() } function end_pursuit() { goto_removing() } states.pursuit = { prompt() { view.prompt = "Pursue cards!" for (let p = 0; p <= 1; ++p) for (let c of game.front[p]) if (should_pursue(c)) gen_action_card(c) }, card(c) { log(card_name(c) + " pursued.") eliminate_card(c) resume_pursuit() }, } // === REMOVING === function goto_removing() { resume_removing() } function resume_removing() { game.state = "removing" for (let p = 0; p <= 1; ++p) { for (let c of game.front[p]) if (should_remove_card(c) || should_retire_card(c)) return for (let c of game.reserve[p]) if (should_remove_card(c) || should_retire_card(c)) return } end_removing() } function end_removing() { goto_reserve() } states.removing = { prompt() { view.prompt = "Remove cards!" for (let p = 0; p <= 1; ++p) { for (let c of game.front[p]) if (should_remove_card(c) || should_retire_card(c)) gen_action_card(c) for (let c of game.reserve[p]) if (should_remove_card(c) || should_retire_card(c)) gen_action_card(c) } }, card(c) { log(card_name(c) + " removed.") eliminate_card(c) resume_removing() }, } // === RESERVE === function should_enter_reserve(c) { let reserve = data.cards[c].reserve if (game.scenario === S37_INKERMAN) { if (c === S37_BRITISH_TROOPS) return map_get(game.cubes, S37_THE_FOG, 0) === 1 if (c === S37_FRENCH_TROOPS) return map_get(game.cubes, S37_THE_FOG, 0) === 0 } for (let t of reserve) if (!set_has(game.front[0], t) && !set_has(game.front[1], t)) return true 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() } states.reserve = { 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) { log(card_name(c) + " came out of reserve.") let p = find_card_owner(c) set_delete(game.reserve[p], c) set_add(game.front[p], 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_fizzle1(c) { gen_action("f1", c) } function gen_action_fizzle2(c) { gen_action("f2", 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_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) }