summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-11-22 01:55:38 +0100
committerTor Andersson <tor@ccxvii.net>2024-01-08 16:36:47 +0100
commitb2c68b84481e4393fd13d78184318bbe22ff427c (patch)
tree214a3baf9e01ea171d69331355b72ab360753eda /rules.js
parent95b90a9b4c21d6fd0805177d5193a8a62fa217ad (diff)
downloadtable-battles-b2c68b84481e4393fd13d78184318bbe22ff427c.tar.gz
lotsa stuff
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js1414
1 files changed, 1414 insertions, 0 deletions
diff --git a/rules.js b/rules.js
new file mode 100644
index 0000000..c9b5d56
--- /dev/null
+++ b/rules.js
@@ -0,0 +1,1414 @@
+"use strict"
+
+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_roll_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_cube(c) {
+ let old = map_get(game.cubes, c, 0)
+ map_set(game.cubes, c, Math.min(0, old - 1))
+}
+
+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 pay_for_action(c) {
+ if (data.cards[c].special)
+ remove_cube(c)
+ 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)
+
+console.log("can_place_dice", data.cards[c].number, pattern, pred(c))
+
+ let wing = data.cards[c].wing
+console.log(" wing " + wing, game.placed)
+ 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
+ console.log(" placed on", x, i_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)
+ console.log("can_place_value", c, v, old_v)
+ if (c === undefined) throw new Error("boo")
+ 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(c, 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()
+ console.log("gen_pool_die", v)
+ 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()
+
+ 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 can_take_action(c, a) {
+ 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 goto_action_phase() {
+ game.state = "action"
+}
+
+states.action = {
+ prompt() {
+ view.prompt = "Take an action."
+ view.actions.pass = 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)
+ }
+ }
+ },
+ 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()
+ },
+}
+
+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() {
+ let a = current_action()
+ for (let c of a.target_list) {
+ if (game.front[0].includes(c))
+ return c
+ if (game.front[1].includes(c))
+ return c
+ }
+}
+
+function find_target_of_command(c) {
+ let a = current_action()
+ 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() {
+ 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()
+ view.prompt = "Attack " + card_name(t) + "."
+ view.selected = game.selected
+ gen_action_card(t)
+ },
+ card(c) {
+ game.target = c
+ apply_attack(current_action())
+ pay_for_action(game.selected)
+ end_action_phase()
+ },
+}
+
+states.command = {
+ prompt() {
+ let t = find_target_of_command()
+ view.prompt = "Bring " + card_name(t) + " out of reserve."
+ view.selected = game.selected
+ gen_action_card(t)
+ },
+ card(c) {
+ 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 end_action_phase() {
+ goto_roll_phase()
+}
+
+// === ATTACK EFFECTS ===
+
+function apply_self() {
+ remove_sticks(game.selected, 1)
+}
+
+function apply_hit() {
+ remove_sticks(game.target, 1)
+}
+
+function apply_hit_per_die() {
+ console.log("HIT PER DIE", game.selected, count_dice_on_card(game.selected))
+ 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) {
+console.log("ATTACK", a.effect)
+ 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()
+ apply_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
+}