"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 attack_reserve attack_choose_target take_from may_take_from */ // TODO: morale cube limit (cannot place on special if maxed) // TODO: null action when action says to take cards from other dice? const data = require("./data.js") function clamp(x, min, max) { return Math.max(min, Math.min(max, x)) } 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, lost: game.lost, 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.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 = result + " victory! " + victory log("") log(result + " victory!") 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 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") 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 S30_GERARD = find_card(30, "Gerard") 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_ROYALIST_GUNS = find_card(31, "Royalist Guns") const S31_GERARD = find_card(31, "Gerard") const S31_STAPLETON = find_card(31, "Stapleton") const S31_LONDON_TRAINED_BANDS = find_card(31, "London Trained Bands") const S35_AULDEARN = find_scenario(35) const S35_MONTROSE = find_card(35, "Montrose") const S35_GORDON = find_card(35, "Gordon") 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_CUTTS_COLUMN = find_card(41, "Cutt's Column") 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") // === 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: -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: [], // current action routed: [ 0, 0 ], selected: -1, target: -1, hits: 0, self: 0, // for breastworks etc self2: 0, target2: -1, hits2: 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 { 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("") 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) log("") } if (info.lore_text) { for (let line of info.lore_text.split("

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

")) log(line) log("") } if (game.scenario === S37_INKERMAN) { map_set(game.cubes, S37_THE_FOG, 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_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 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 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("Take dice from " + from + " to " + 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) { log("One die from " + from + " to " + 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) { log("Wild die from " + from + " to " + 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 rout_card(c) { let p = find_card_owner(c) game.lost[p] += get_sticks(c) log(c + " routed.") eliminate_card(c) } function pursue_card(c) { log(c + " pursued.") eliminate_card(c) } function retire_card(c) { log(c + " retired.") eliminate_card(c) } function remove_card(c) { log(c + " removed.") eliminate_card(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_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) { 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, P1 + " has no more attacks!") else return goto_game_over(P1, P2 + " has no more attacks!") } return false } 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, P2 + " lost both linked formations.") if (is_removed_from_play(S39_DUKE_OF_SAVOY) && is_removed_from_play(S39_EUGENE)) return goto_game_over(P2, P1 + " 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.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!") let tv0 = info.players[1].lost let tv1 = info.players[0].lost if (info.players[0].tactical > 0 && tv0 >= info.players[0].tactical) return goto_game_over(P2, P2 + " tactical victory!") if (info.players[1].tactical > 0 && tv1 >= info.players[1].tactical) return goto_game_over(P1, P1 + " tactical victory!") return false } // === 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 } 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(c) { 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 } 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/2/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/3/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), "4/5/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 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/2/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/3/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/4/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), "4/5/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 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/2/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/3/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/4/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), "4/5/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_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_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) { 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_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.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 /* // 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() { if (game.reacted === player_index()) view.prompt = "Skipped action phase; roll the dice in your pool." else 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() { game.rolled = 0 if (game.reacted === player_index()) game.reacted = -1 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.rolled++ } } 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() { push_undo() game.selected = -1 // 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_player_dice_location(p, i) < 0) set_player_dice_value(p, 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 } } } } end_turn() } states.s26_fatal_blunder = { 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 = { 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) set_sticks(S41_BLENHEIM_CARD, get_sticks(S41_BLENHEIM_CARD) + get_sticks(S41_CLERAMBAULT)) set_sticks(S41_CLERAMBAULT, 0) if (c === S41_CLERAMBAULT) { rout_card(S41_CLERAMBAULT) game.morale[0]++ game.morale[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 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_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 "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) { 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 (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 (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 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)) return true } } if (game.scenario === S42_RAMILLIES) { if (player_index() === 0) { if (has_any_dice_on_card(S42_MARLBOROUGH)) 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() { if (check_impossible_to_attack_victory()) return if (game.scenario === S25_WHEATFIELD) { // Rout Stony Hill at start of Union turn if it is the only Blue card left. if (player_index() === 1) { if (is_card_in_play(S25_STONY_HILL)) { if (count_cards_remaining_from_wing(BLUE) === 1) { game.state = "s25_stony_hill" return } } } } goto_action_phase() } states.s25_stony_hill = { prompt() { view.prompt = "Rout Stony Hill!" gen_action_card(S25_STONY_HILL) }, card(c) { 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()) { end_action_phase() } 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 = { prompt() { view.prompt = "Take an action." view.actions.roll = 1 view.actions.end_turn = 0 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 (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)) 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() 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() }, 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" return } if (game.scenario === S42_RAMILLIES) { game.state = "s42_marlborough" return } throw new Error("missing rule for special action: " + card_name(c)) } } states.s40_cassines = { 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(game.selected + " moved one stick from " + c) set_sticks(c, get_sticks(c) - 1) set_sticks(game.selected, get_sticks(game.selected) + 1) end_action_phase() }, } states.s41_prince_eugene = { 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 log(game.target2 + " moved one stick from " + game.selected) set_sticks(game.selected, get_sticks(game.selected) - 1) set_sticks(game.target2, get_sticks(game.target2) + 1) if (--game.self === 0) { pay_for_action(game.selected) end_action_phase() } }, next() { pay_for_action(game.selected) end_action_phase() }, } states.s42_marlborough = { 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(game.selected + " moved 4 sticks to " + c) set_sticks(c, get_sticks(c) + 4) set_sticks(S42_MARLBOROUGH, get_sticks(S42_MARLBOROUGH) - 4) pay_for_action(S42_MARLBOROUGH) if (get_sticks(S42_MARLBOROUGH) === 0) remove_card(S42_MARLBOROUGH) end_action_phase() }, } function goto_null(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 } 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) { let list = [] for (let t of a.target_list) { if (is_card_in_reserve(t)) list.push(t) } 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)) 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.target = target update_attack1() update_attack2() } // Update hits and self hits. function update_attack1() { let a = current_action() game.hits = get_attack_hits(game.selected, a) game.self = get_attack_self(game.selected, a) + game.self2 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 (get_cubes(S37_THE_FOG) === 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 (game.scenario === S30_EDGEHILL) { if (game.selected === S30_GERARD && game.target === S30_ESSEX) 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 (game.target === S31_WENTWORTH) { if (has_any_dice_on_card(S31_BYRON) && is_card_in_play(S31_SKIPPON)) { 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 === S41_BLENHEIM_SCENARIO) { // TODO: Original attack only, or also when Blenheim absorbs from Clerambault? if (game.selected === S41_CUTTS_COLUMN && game.target === S41_BLENHEIM_CARD) game.hits *= 2 } if (game.scenario === S43_DENAIN) { if (game.selected === S43_DUTCH_HORSE && game.target === S43_VILLARS_LEFT) game.hits *= 2 } // Oblique Attack (CAL expansion rule) if (is_infantry(game.selected)) { if (get_sticks(game.selected) >= get_sticks(game.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(game.target)) game.hits = Math.max(0, game.hits - 1) if (card_has_rule(game.target, "suffer_1_less")) game.hits = Math.max(0, game.hits - 1) if (card_has_rule(game.target, "suffer_1_less_1_max")) game.hits = clamp(game.hits - 1, 0, 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 = { 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) } } 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 from = get_dice_location(d) let w = side_get_wild_die_card(player_index()) if (w === from) { take_wild_die(w, game.selected) 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 } } throw new Error("no handler for taking dice from other card") } } function resume_attack() { pay_for_action(game.selected) remove_sticks(game.selected, game.self) remove_sticks(game.target, game.hits) if (game.target2 >= 0) remove_sticks(game.target2, game.hits2) 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(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(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_or_reserve(S4_NORTHUMBERLAND)) { log("The Stanleys rout Northumberland.") set_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() { if (game.scenario === S39_MARSAGLIA) { if (game.selected === S39_CATINAT && game.target2 === S39_BAYONETS) 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.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_removed_from_play(S15_SOMERSET)) return false } } if (game.scenario === S31_NEWBURY_1ST) { if (c === S31_GERARD) { 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 = { 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 one 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 = { 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")) remove_card(game.target) end_reaction() }, } // === 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 "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 one less Hit instead.": game.hits = Math.max(0, game.hits - 1) break case "When target suffers Hits, this card suffers 1 less hit per die.": case "When target suffers Hits, this unit 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 = { prompt() { view.prompt = "Choosy 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 = { 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 = player_index() update_attack1() 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 = clamp(game.hits - 1, 0, 1) break case "This unit suffers TWO less hits and never more than one.": game.self += 1 game.hits = clamp(game.hits - 2, 0, 1) break } update_attack2() 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 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.)": case "1 hit per die. 1 self per action. (But see Semmes.)": case "1 hit per die (also take dice from 141st Pennsylvania). 1 self per action.": case "1 hit per die (also take dice from 68th Pennsylvania). 1 self per action.": case "1 hit per die. 1 self per action. (But see William Fielding.)": case "1 hit per die (1 extra vs Essex). 1 self per action. (See W. Fielding.)": case "1 hit per die. 1 self per action (but see Cannons).": case "1 hit per die. 1 self per action (but see Bayonets!).": case "1 hit per die (2 hits per die vs. Blenheim). 1 self per action.": case "1 hit per die (two per die vs. Villars's Left). 1 self per action.": case "1 hit per die. 1 self per action. If reduced to one stick, no self hits.": case "1 hit per die. 1 self per action. You CHOOSE the target.": 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.": case "1 hit, PLUS 1 hit per die. 1 self per action. Fightin' Irish!": return 1 + count_dice_on_card(c) case "2 hits, PLUS 1 hit per die. 1 self per action.": return 2 + count_dice_on_card(c) case "2 hits.": return 2 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 "2 hits.": 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 die. 1 self per action. (But see Semmes.)": case "1 hit per die (also take dice from 141st Pennsylvania). 1 self per action.": case "1 hit per die (also take dice from 68th Pennsylvania). 1 self per action.": case "1 hit per die. 1 self per action. (But see William Fielding.)": case "1 hit per die (1 extra vs Essex). 1 self per action. (See W. Fielding.)": case "1 hit per die. 1 self per action (but see Cannons).": case "1 hit per die. 1 self per action (but see Bayonets!).": case "1 hit per die (2 hits per die vs. Blenheim). 1 self per action.": case "1 hit per die (two per die vs. Villars's Left). 1 self per action.": case "1 hit per die. 1 self per action. You CHOOSE the target.": case "1 hit per pair. 1 self per action.": case "1 hit, PLUS 1 hit per die. 1 self per action.": case "1 hit, PLUS 1 hit per die. 1 self per action. Fightin' Irish!": case "2 hits, PLUS 1 hit per die. 1 self per action.": return 1 case "1 hit per die. 1 self per action. If reduced to one stick, no self hits.": return (get_sticks(c) > 1) ? 1 : 0 } } // === 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) { // 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.") set_sticks(S2_NORTHERN_HORSE, 1) remove_card(S2_RUPERTS_LIFEGUARD) } if (should_rout_card(S2_BYRON)) { log("Rupert's Lifeguard added to Byron.") set_sticks(S2_BYRON, 1) remove_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) || 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 = { 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)) { log(card_name(c) + " routed.") let p = find_card_owner(c) game.routed[p] += data.cards[c].morale rout_card(c) } else if (should_retire_card(c)) { log(card_name(c) + " retired.") retire_card(c) } else if (should_pursue(c)) { log(card_name(c) + " pursued.") pursue_card(c) } else { log(card_name(c) + " removed.") 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, 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_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() } 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_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_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) }