"use strict" // TODO: manual take hits? // TODO: manual "enter reserves" ? // TODO: manual "pursuit" ? const data = require("./data.js") const P1 = "First" const P2 = "Second" var states = {} var game = null var view = null const POOL = -1 exports.scenarios = [ ... data.scenarios.map(s => s.number + " - " + s.name) ] 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, } 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()} 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) goto_game_over(P2, P1 + " resigned.") if (player === P2) goto_game_over(P1, P2 + " resigned.") } return game } 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 }, } // === 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", // dice value and position dice: [ 1, POOL, 1, POOL, 1, POOL, 1, POOL, 1, POOL, 1, POOL, 2, POOL, 2, POOL, 2, POOL, 2, POOL, 2, POOL, 2, 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: [], } function setup_formation(front, reserve, c) { let card = data.cards[c] if (card.reserve) reserve.push(c) else front.push(c) if (card.special) 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("") goto_action_phase() return game } // === XXX === 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.min(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) } function eliminate_card(c) { remove_dice(c) remove_cubes(c, 3) array_remove_item(game.front[0], c) array_remove_item(game.front[1], c) } function pay_for_action(c) { if (data.cards[c].special) remove_cubes(c, 1) else remove_dice(c) } // === ROLL PHASE === 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 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 = { "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 = { "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 = { "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) throw Error("bad card definition: " + data.cards[c].number) let pred = place_dice_check[pattern] if (!pred) throw Error("bad pattern definition: " + pattern) let wing = data.cards[c].wing for (let i = 0; i < game.placed.length; i += 2) { let x = game.placed[i] if (x !== c) { // TODO: place_2_on_WING ability let i_wing = data.cards[x].wing if (i_wing === wing) return false } } if (place_dice_once[pattern]) { if (map_has(game.placed, c)) 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() { 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() { 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 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() { for (let v = 1; v <= 6; ++v) if (check_single_count(c, v, 3)) gen_pool_die(v) } 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_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 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) game.state = "roll" } 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.selected = 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.state = "place" }, end_turn() { end_roll_phase() }, } function end_roll_phase() { clear_undo() map_clear(game.placed) // 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) } } set_opponent_active() goto_action_phase() } // === ACTION PHASE === 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, v) { 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 undefined: return map_get(game.cubes, c, 0) >= 1 default: throw new Error("invalid action requirement: " + req) } } function check_dice_requirement(c, req) { switch (req) { case "Full House": return require_full_house(c) case "Pair": case "Pair, Voluntary": 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" && find_target_of_attack(a) < 0) return false if (a.type === "Command" && find_target_of_command(a) < 0) 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) } 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)) return true } return false } function goto_action_phase() { if (game.reacted) { game.reacted = 0 goto_roll_phase() } else { if (can_take_any_action()) game.state = "action" else goto_roll_phase() } } states.action = { prompt() { view.prompt = "Take an action." view.actions.pass = 1 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) }, pass() { push_undo() goto_roll_phase() }, roll() { push_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": game.state = "attack" break case "Bombard": game.state = "bombard" break case "Command": game.state = "command" break } } function current_action() { return data.cards[game.selected].actions[game.action] } function find_target_of_attack(a) { for (let c of a.target_list) { if (game.front[0].includes(c)) return c if (game.front[1].includes(c)) return c } return -1 } function find_target_of_command(a) { for (let c of a.target_list) { if (game.reserve[0].includes(c)) return c if (game.reserve[1].includes(c)) return c } } 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() }, } states.attack = { prompt() { let t = find_target_of_attack(current_action()) view.prompt = "Attack " + card_name(t) + "." view.selected = game.selected gen_action_card(t) }, card(c) { log(card_name(game.selected) + " attacked " + card_name(c) + ".") game.target = c if (can_opponent_react()) { clear_undo() set_opponent_active() game.state = "react" } else { resume_attack() } }, } function resume_attack() { apply_attack(current_action()) pay_for_action(game.selected) end_action_phase() } states.command = { prompt() { let t = find_target_of_command(current_action()) view.prompt = "Bring " + card_name(t) + " out of reserve." view.selected = game.selected gen_action_card(t) }, card(c) { log(card_name(game.selected) + " commanded " + card_name(c) + " out of reserve.") let p = player_index() array_remove_item(game.reserve[p], c) // TODO: insert where? game.front[p].push(p) pay_for_action(game.selected) end_action_phase() }, } function has_reserve_target_routed(reserve) { for (let c of reserve) if (!game.front[0].includes(c) && !game.front[1].includes(c)) return true return false } function end_action_phase() { // Bring on reinforcements (on both sides). for (let p = 0; p <= 1; ++p) { for (let i = 0; i < game.reserve[p].length; ++i) { let c = game.reserve[p][i] if (has_reserve_target_routed(data.cards[c].reserve)) { console.log("COMING OUT!", c) log(card_name(c) + " came out of reserve.") game.front[p].push(c) array_remove(game.reserve[p], i) --i } } } goto_roll_phase() } // === REACTION === function can_opponent_react() { let p = 1 - player_index() for (let c of game.front[p]) if (can_card_react(c)) return true return false } function can_card_react(c) { 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])) 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])) return true } return false } function can_take_reaction(c, a) { switch (a.type) { default: throw new Error("invalid reaction: " + a.type) case "Screen": if (!a.target_list.includes(game.selected)) return false break case "Counterattack": // if _this_ formation is attacked if (game.target !== c) return false if (find_target_of_attack(a) < 0) return false break case "Absorb": if (!a.target_list.includes(game.target)) return false break } if (data.cards[c].special) return check_cube_requirement(c, a.requirement) else return check_dice_requirement(c, a.requirement) } states.react = { prompt() { view.prompt = "React to " + card_name(game.selected) + " attack!" view.selected = game.selected let voluntary = true 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_reaction(c, data.cards[c].actions[0])) { if (can_take_reaction(c, data.cards[c].actions[0])) { if (is_mandatory_reaction(c, data.cards[c].actions[0])) voluntary = false gen_action_action1(c) } } } 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])) { if (is_mandatory_reaction(c, data.cards[c].actions[1])) voluntary = false gen_action_action2(c) } } } } } if (voluntary) view.actions.pass = 1 }, a1(c) { goto_take_reaction(c, 0) }, a2(c) { 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 goto_screen(c, a) { log(card_name(c) + " screened.") game.reacted = 1 pay_for_action(c) set_opponent_active() pay_for_action(game.selected) end_action_phase() } function goto_absorb(c, a) { log(card_name(c) + " absorbed.") game.reacted = 1 pay_for_action(c) game.target = c set_opponent_active() // TODO: absorb effect! end_action_phase() } function goto_counterattack(c, a) { pay_for_action(c) game.reacted = 1 log(card_name(c) + " counterattacked.") let save_selected = game.selected let save_target = game.target game.selected = c game.target = find_target_of_attack(a) apply_attack(a.effect) game.selected = save_selected game.target = save_target set_opponent_active() resume_attack() } // === ATTACK EFFECTS === function apply_self() { remove_sticks(game.selected, 1) } function apply_hit() { remove_sticks(game.target, 1) } function apply_hit_plus_hit_per_die() { remove_sticks(game.target, 1 + count_dice_on_card(game.selected)) } function apply_hit_per_die() { remove_sticks(game.target, count_dice_on_card(game.selected)) } function apply_hit_per_pair() { remove_sticks(game.target, count_dice_on_card(game.selected) >> 1) } function apply_attack(a) { switch (a.effect) { default: throw new Error("invalid attack effect: " + text) break case "1 hit.": apply_hit() break case "1 hit. 1 self per action.": apply_hit() apply_self() break case "1 hit per die.": apply_hit_per_die() break case "1 hit per die. 1 self per action.": apply_hit_per_die() apply_self() break case "1 hit per pair.": apply_hit_per_pair() break case "1 hit per pair. 1 self per action.": apply_hit_per_pair() apply_self() break case "1 hit, PLUS 1 hit per die. 1 self per action.": apply_hit_plus_hit_per_die() apply_self() break } } // === COMMON LIBRARY === function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [ argument ] else view.actions[action].push(argument) } 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 } function shuffle(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } // 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_remove_item(array, item) { let n = array.length for (let i = 0; i < n; ++i) if (array[i] === item) return array_remove(array, i) } function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function array_remove_pair(array, index) { let n = array.length for (let i = index + 2; i < n; ++i) array[i - 2] = array[i] array.length = n - 2 } function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i-2] array[i+1] = array[i-1] } array[index] = key array[index+1] = value } // Set as plain sorted array function set_clear(set) { set.length = 0 } function set_has(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return true } return false } function set_add(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return } array_insert(set, a, item) } function set_delete(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } } function set_toggle(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } array_insert(set, a, item) } // Map as plain sorted array of key/value pairs function map_clear(map) { map.length = 0 } function map_has(map, key) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return true } return false } function map_get(map, key, missing) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return map[(m<<1)+1] } return missing } function map_set(map, key, value) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { map[(m<<1)+1] = value return } } array_insert_pair(map, a<<1, key, value) } function map_delete(map, item) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove_pair(map, m<<1) return } } } function object_diff(a, b) { if (a === b) return false if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { if (Array.isArray(a)) { if (!Array.isArray(b)) return true let a_length = a.length if (b.length !== a_length) return true for (let i = 0; i < a_length; ++i) if (object_diff(a[i], b[i])) return true return false } for (let key in a) if (object_diff(a[key], b[key])) return true for (let key in b) if (!(key in a)) return true return false } return true }