diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 424 |
1 files changed, 308 insertions, 116 deletions
@@ -1,11 +1,19 @@ "use strict" +// TODO: game end check (no possible attack, no morale left) + +// TODO: track routed cards explicitly (separate from retired and pursuit) + // TODO: manual take hits? // TODO: manual "enter reserves" ? // TODO: manual "pursuit" ? +// TODO: allow placing dice on full special formations? + const data = require("./data.js") +// for (let c of data.cards) for (let a of c.actions) console.log(a.type, a.effect) + const P1 = "First" const P2 = "Second" @@ -15,9 +23,38 @@ var view = null const POOL = -1 -exports.scenarios = [ - ... data.scenarios.map(s => s.number + " - " + s.name) -] +exports.scenarios = { + "": [ "Random" ], +} + +const scenario_roles = {} + +for (let s of data.scenarios) { + let id = s.number + " - " + s.name + let x = s.expansion + if (!(x in exports.scenarios)) { + exports.scenarios[""].push("Random - " + x) + exports.scenarios[x] = [] + } + exports.scenarios[x].push(id) + scenario_roles[id] = [ s.players[0].name, s.players[1].name ] +} + +exports.is_random_scenario = function (scenario) { + return scenario.startsWith("Random") +} + +exports.select_random_scenario = function (scenario, seed) { + if (scenario === "Random") { + let info = data.scenarios[seed % data.scenarios.length] + return info.number + " - " + info.name + } + if (scenario.startsWith("Random - ")) { + let list = exports.scenarios[scenario.replace("Random - ", "")] + return list[seed % list.length] + } + return scenario +} exports.roles = [ P1, P2 ] @@ -69,11 +106,10 @@ exports.resign = function (state, player) { game = state if (game.state !== 'game_over') { if (player === P1) - goto_game_over(P2, P1 + " resigned.") + return goto_game_over(P2, P1 + " resigned.") if (player === P2) - goto_game_over(P1, P2 + " resigned.") + return goto_game_over(P1, P2 + " resigned.") } - return game } function goto_game_over(result, victory) { @@ -97,6 +133,8 @@ states.game_over = { exports.setup = function (seed, scenario, options) { // TODO: "Random" + console.log("SETUP", scenario) + scenario = parseInt(scenario) scenario = data.scenarios.findIndex(s => s.number === scenario) if (scenario < 0) @@ -115,8 +153,8 @@ exports.setup = function (seed, scenario, options) { // 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, + 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, + 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, 0, POOL, ], // sticks (map normal formation -> count) @@ -136,9 +174,9 @@ exports.setup = function (seed, scenario, options) { function setup_formation(front, reserve, c) { let card = data.cards[c] if (card.reserve) - reserve.push(c) + set_add(reserve, c) else - front.push(c) + set_add(front, c) if (card.special) add_cubes(c, 1) else @@ -159,7 +197,7 @@ exports.setup = function (seed, scenario, options) { return game } -// === XXX === +// === GAME STATE ACCESSORS === function card_number(c) { return data.cards[c].number @@ -209,9 +247,12 @@ function remove_sticks(c, n) { } function remove_dice(c) { - for (let i = 0; i < 12; ++i) - if (get_dice_location(i) === c) + for (let i = 0; i < 12; ++i) { + if (get_dice_location(i) === c) { set_dice_location(i, POOL) + set_dice_value(i, 0) + } + } } function eliminate_card(c) { @@ -228,8 +269,6 @@ function pay_for_action(c) { remove_dice(c) } -// === ROLL PHASE === - function get_player_dice_value(p, i) { return game.dice[p * 12 + i * 2] } @@ -258,6 +297,48 @@ function set_dice_location(d, v) { game.dice[d * 2 + 1] = v } +function set_dice_value(d, v) { + game.dice[d * 2] = v +} + +// === HOW TO WIN === + +function is_card_in_play(c) { + return ( + set_has(game.players[0].front, c) || + set_has(game.players[1].front, c) || + set_has(game.players[0].reserve, c) || + set_has(game.players[1].reserve, c) + ) +} + +function is_card_attack_with_target_in_play(c) { + for (let a of data.cards[c].actions) { + if (a.type === "Attack") { + for (let t of a.target_list) + if (is_card_in_play(c)) + return false + } + } +} + +function check_impossible_to_attack_victory() { + let p = player_index() + for (let c of game.players[p].front) + if (is_card_attack_with_target_in_play(c)) + return false + for (let c of game.players[p].reserve) + if (is_card_attack_with_target_in_play(c)) + return false + return true +} + +function check_morale_loss(p) { + return game.players[0].morale === 0 +} + +// === ROLL PHASE === + function is_pool_die(i, v) { let p = player_index() return get_player_dice_location(p, i) < 0 && get_player_dice_value(p, i) === v @@ -443,7 +524,7 @@ function can_place_dice(c) { let pattern = data.cards[c].dice if (!pattern) - throw Error("bad card definition: " + data.cards[c].number) + return false let pred = place_dice_check[pattern] if (!pred) @@ -538,7 +619,7 @@ function check_straight_4(c) { ) } -function check_doubles() { +function check_doubles(c) { return ( check_single_count(c, 1, 2) || check_single_count(c, 2, 2) || @@ -549,7 +630,7 @@ function check_doubles() { ) } -function check_triples() { +function check_triples(c) { return ( check_single_count(c, 1, 3) || check_single_count(c, 2, 3) || @@ -658,12 +739,6 @@ 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" } @@ -757,6 +832,12 @@ function end_roll_phase() { } } + // Blank out unused dice. + let p = player_index() + for (let i = 0; i < 6; ++i) + if (get_player_dice_location(p, i) < 0) + set_player_dice_value(p, i, 0) + set_opponent_active() goto_action_phase() @@ -890,13 +971,21 @@ function can_take_any_action() { for (let c of game.front[p]) { if (has_any_dice_on_card(c)) return true - if (has_any_cubes_on_card(c)) + if (has_any_cubes_on_card(c)) // TODO: check requirements! return true } return false } function goto_action_phase() { + if (check_impossible_to_attack_victory()) { + if (player === P1) + goto_game_over(P2, P1 + " has no more attacks!") + else + goto_game_over(P1, P2 + " has no more attacks!") + return + } + if (game.reacted) { game.reacted = 0 goto_roll_phase() @@ -908,10 +997,16 @@ function goto_action_phase() { } } +function end_action_phase() { + check_routing() + check_pursuit() + check_reserve() + goto_roll_phase() +} + states.action = { prompt() { view.prompt = "Take an action." - view.actions.pass = 1 view.actions.roll = 1 let p = player_index() @@ -962,10 +1057,6 @@ states.action = { push_undo() goto_fizzle(c) }, - pass() { - push_undo() - goto_roll_phase() - }, roll() { push_undo() goto_roll_phase() @@ -1002,9 +1093,9 @@ function current_action() { function find_target_of_attack(a) { for (let c of a.target_list) { - if (game.front[0].includes(c)) + if (set_has(game.front[0], c)) return c - if (game.front[1].includes(c)) + if (set_has(game.front[1], c)) return c } return -1 @@ -1012,9 +1103,9 @@ function find_target_of_attack(a) { function find_target_of_command(a) { for (let c of a.target_list) { - if (game.reserve[0].includes(c)) + if (set_has(game.reserve[0], c)) return c - if (game.reserve[1].includes(c)) + if (set_has(game.reserve[1], c)) return c } } @@ -1033,16 +1124,34 @@ states.bombard = { }, } +function format_attack_result() { + let a = current_action() + let hits = get_attack_hits(game.selected, a) + let self = get_attack_self(game.selected, a) + if (hits !== 1 && self > 0) + return ` ${hits} hits. ${self} self.` + if (hits === 1 && self > 0) + return ` ${hits} hit. ${self} self.` + if (hits !== 1) + return ` ${hits} hits.` + if (hits === 1) + return ` ${hits} hit.` + return "" +} + states.attack = { prompt() { let t = find_target_of_attack(current_action()) - view.prompt = "Attack " + card_name(t) + "." + view.prompt = "Attack " + card_name(t) + "." + format_attack_result() view.selected = game.selected gen_action_card(t) }, card(c) { log(card_name(game.selected) + " attacked " + card_name(c) + ".") + let a = current_action() game.target = c + game.hits = get_attack_hits(game.selected, a) + game.self = get_attack_self(game.selected, a) if (can_opponent_react()) { clear_undo() set_opponent_active() @@ -1054,7 +1163,9 @@ states.attack = { } function resume_attack() { - apply_attack(current_action()) + apply_hits(game.hits) + apply_self(game.self) + game.hits = game.self = 0 pay_for_action(game.selected) end_action_phase() } @@ -1068,40 +1179,23 @@ states.command = { }, card(c) { log(card_name(game.selected) + " commanded " + card_name(c) + " out of reserve.") + console.log("PRE COMMAND", JSON.stringify(game)) let p = player_index() array_remove_item(game.reserve[p], c) - // TODO: insert where? - game.front[p].push(p) + set_add(game.front[p], c) pay_for_action(game.selected) + console.log("POST COMMAND", JSON.stringify(game)) 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)) + if (!set_has(game.front[0], c) && !set_has(game.front[1], 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() { @@ -1133,17 +1227,21 @@ function can_take_reaction(c, a) { default: throw new Error("invalid reaction: " + a.type) case "Screen": + // if any formation is attacked + // ... by one of the listed targets if (!a.target_list.includes(game.selected)) return false break case "Counterattack": - // if _this_ formation is attacked + // if this formation is attacked if (game.target !== c) return false - if (find_target_of_attack(a) < 0) + // ... by one of the listed targets + if (!a.target_list.includes(game.selected)) return false break case "Absorb": + // if attack target is listed on absorb action if (!a.target_list.includes(game.target)) return false break @@ -1157,8 +1255,9 @@ function can_take_reaction(c, a) { states.react = { prompt() { - view.prompt = "React to " + card_name(game.selected) + " attack!" + view.prompt = card_name(game.selected) + " attacks " + card_name(game.target) + "! " + format_attack_result() view.selected = game.selected + view.target = game.target let voluntary = true let p = player_index() @@ -1228,12 +1327,27 @@ function goto_screen(c, a) { function goto_absorb(c, a) { log(card_name(c) + " absorbed.") + + game.target = c game.reacted = 1 pay_for_action(c) - game.target = c + + switch (a.effect) + { + default: + throw new Error("invalid absorb effect: " + a.effect) + case "When target suffers Hits, this card suffers them instead.": + break + case "When target suffers Hits, this card suffers 1 hit ONLY instead.": + game.hits = 1 + break + case "When target suffers Hits, this card suffers 1 less hit per die.": + game.hits = Math.max(0, game.hits - count_dice_on_card(c)) + break + } + set_opponent_active() - // TODO: absorb effect! - end_action_phase() + resume_attack() } function goto_counterattack(c, a) { @@ -1241,16 +1355,37 @@ function goto_counterattack(c, a) { 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 + switch (a.effect) + { + default: + throw new Error("invalid counterattack effect: " + a.effect) + case "1 hit per die.": + game.self += count_dice_on_card(c) + break + case "1 hit.": + game.self += 1 + break + case "1 hit. Additionally, this unit only suffers one hit.": + game.self += 1 + game.hits = 1 + break + case "1 hit. Additionally, this unit suffers one less hit per die.": + game.self += 1 + game.hits = Math.max(0, game.hits - count_dice_on_card(c)) + break + case "1 hit. Additionally, this unit suffers one less hit.": + game.self += 1 + game.hits -= 1 + break + case "This unit suffers ONE less hit and never more than one.": + game.self += 1 + game.hits = Math.max(0, Math.min(1, game.hits - 1)) + break + case "This unit suffers TWO less hits and never more than one.": + game.self += 1 + game.hits = Math.max(0, Math.min(1, game.hits - 2)) + break + } set_opponent_active() resume_attack() @@ -1258,67 +1393,124 @@ function goto_counterattack(c, a) { // === ATTACK EFFECTS === -function apply_self() { - remove_sticks(game.selected, 1) +function apply_self(n) { + remove_sticks(game.selected, n) } -function apply_hit() { - remove_sticks(game.target, 1) +function apply_hits(n) { + remove_sticks(game.target, n) } -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 get_attack_hits(c, a) { + switch (a.effect) { + default: + throw new Error("invalid attack effect: " + a.effect) + case "1 hit.": + case "1 hit. 1 self per action.": + return 1 + case "1 hit per die.": + case "1 hit per die. 1 self per action.": + return count_dice_on_card(c) + case "1 hit per pair.": + case "1 hit per pair. 1 self per action.": + return count_dice_on_card(c) >> 1 + case "1 hit, PLUS 1 hit per die. 1 self per action.": + return 1 + count_dice_on_card(c) + } } -function apply_attack(a) { +function get_attack_self(c, a) { switch (a.effect) { default: - throw new Error("invalid attack effect: " + text) - break - + throw new Error("invalid attack effect: " + a.effect) case "1 hit.": - apply_hit() - break - + case "1 hit per die.": + case "1 hit per pair.": + return 0 case "1 hit. 1 self per action.": - apply_hit() - apply_self() - break + case "1 hit per die. 1 self per action.": + case "1 hit per pair. 1 self per action.": + case "1 hit, PLUS 1 hit per die. 1 self per action.": + return 1 + } +} - case "1 hit per die.": - apply_hit_per_die() - break +// === ROUTING === - case "1 hit per die. 1 self per action.": - apply_hit_per_die() - apply_self() - break +function check_routing() { + // Rout cards with no sticks. + let routed = [ 0, 0 ] + for (let p = 0; p <= 1; ++p) { + for (let i = 0; i < game.front[p].length; ++i) { + let c = game.front[p][i] + if (!data.cards[c].special) { + if (map_get(game.sticks, c, 0) === 0) { + log(card_name(c) + " routed.") + if (data.cards[c].star) + routed[p] = 2 + else + routed[p] = 1 + eliminate_card(c) + --i; + } + } + } + } - case "1 hit per pair.": - apply_hit_per_pair() - break + // Morale loss + if ((routed[0] > 0 && !routed[1]) || (routed[1] > 0 && !routed[0])) { + if (routed[0]) { + routed[0] = Math.min(routed[0], game.morale[0]) + game.morale[0] -= routed[0] + game.morale[1] += routed[0] + } else { + routed[1] = Math.min(routed[1], game.morale[1]) + game.morale[1] -= routed[1] + game.morale[0] += routed[1] + } + } - case "1 hit per pair. 1 self per action.": - apply_hit_per_pair() - apply_self() - break + if (check_morale_loss(0)) + return goto_game_over(P2, P1 + " has run out of morale!") + if (check_morale_loss(1)) + return goto_game_over(P1, P2 + " has run out of morale!") +} - case "1 hit, PLUS 1 hit per die. 1 self per action.": - apply_hit_plus_hit_per_die() - apply_self() - break +// === PURSUIT === + +function check_pursuit() { + // Remove pursuing cards. + for (let p = 0; p <= 1; ++p) { + for (let i = 0; i < game.front[p].length; ++i) { + let c = game.front[p][i] + let pursuit = data.cards[c].pursuit + if (pursuit !== undefined) { + if (!set_has(game.front[1-p], pursuit)) { + log(card_name(c) + " pursued.") + eliminate_card(c) + --i + } + } + } } } +// === RESERVE === +function check_reserve() { + // Bring on reserves (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)) { + log(card_name(c) + " came out of reserve.") + set_add(game.front[p], c) + array_remove(game.reserve[p], i) + --i + } + } + } +} // === COMMON LIBRARY === @@ -1326,7 +1518,7 @@ function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [ argument ] else - view.actions[action].push(argument) + set_add(view.actions[action], argument) } function gen_action_card(c) { |