diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 1414 |
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 +} |