summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js424
1 files changed, 308 insertions, 116 deletions
diff --git a/rules.js b/rules.js
index 5cad2c4..3438f70 100644
--- a/rules.js
+++ b/rules.js
@@ -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) {