summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-05-18 00:38:35 +0200
committerTor Andersson <tor@ccxvii.net>2023-07-07 18:39:23 +0200
commitbb756e1f3d0014cc8d4eb3666849264daf108bdb (patch)
tree7f6d8544965169b6d880140cf21daaa3f624d180 /rules.js
parent18de9c65450661610d29f151e6ef31ab05905ac3 (diff)
downloadtime-of-crisis-bb756e1f3d0014cc8d4eb3666849264daf108bdb.tar.gz
Add Solo play.
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js412
1 files changed, 326 insertions, 86 deletions
diff --git a/rules.js b/rules.js
index 2ed6982..f1b960e 100644
--- a/rules.js
+++ b/rules.js
@@ -2,67 +2,6 @@
// === CONSTANTS AND DATA ===
-// Barbarian possible locations:
-/*
-FRANKS
- BRITANNIA
- GALLIA
- HISPANIA
- PANNONIA
- ITALIA
-
-ALAMANNI
- PANNONIA
- ITALIA
- THRACIA
- MACEDONIA
-
-GOTHS
- THRACIA
- MACEDONIA
- ASIA
- GALATIA
- SYRIA
-
-
-SASSANIDS
- GALATIA
- ASIA
- SYRIA
- AEGYPTUS
-
-NOMADS
- AFRCIA
- HISPANIA
- AEGYPTUS
- SYRIA
-
-NOMAD STACKS in PROVINCES
- AEGYPTUS NOMADS
- AEGYPTUS SASSANIDS
- AFRICA NOMADS
- ASIA GOTHS
- ASIA SASSANIDS
- BRITANNIA FRANKS
- GALATIA GOTHS
- GALATIA SASSANIDS
- GALLIA FRANKS
- HISPANIA FRANKS
- HISPANIA NOMADS
- ITALIA ALAMANNI
- ITALIA FRANKS
- MACEDONIA ALAMANNI
- MACEDONIA GOTHS
- PANNONIA ALAMANNI
- PANNONIA FRANKS
- SYRIA GOTHS
- SYRIA NOMADS
- SYRIA SASSANIDS
- THRACIA ALAMANNI
- THRACIA GOTHS
-*/
-
-
const P1 = "Red"
const P2 = "Blue"
const P3 = "Yellow"
@@ -75,6 +14,7 @@ const PLAYER_INDEX = {
[P2]: 1,
[P3]: 2,
[P4]: 3,
+ "Solo": 4,
"Observer": -1,
}
@@ -170,6 +110,36 @@ const BARBARIAN_HOMELAND = [
SASSANIDS_HOMELAND,
]
+const BARBARIAN_INVASION = [
+ // Alamanni
+ [
+ [ 1, 3, [ PANNONIA, ITALIA ] ],
+ [ 4, 6, [ THRACIA, MACEDONIA ] ],
+ ],
+ // Franks
+ [
+ [ 1, 2, [ BRITANNIA, GALLIA ] ],
+ [ 3, 4, [ GALLIA, HISPANIA ] ],
+ [ 5, 6, [ PANNONIA, ITALIA ] ],
+ ],
+ // Goths
+ [
+ [ 1, 2, [ THRACIA, MACEDONIA ] ],
+ [ 3, 4, [ ASIA, MACEDONIA ] ],
+ [ 5, 6, [ GALATIA, SYRIA ] ],
+ ],
+ // Nomads
+ [
+ [ 1, 3, [ AFRICA, HISPANIA ] ],
+ [ 4, 6, [ AEGYPTUS, SYRIA ] ],
+ ],
+ // Sassanids
+ [
+ [ 1, 3, [ GALATIA, ASIA ] ],
+ [ 4, 6, [ SYRIA, AEGYPTUS ] ],
+ ],
+]
+
// 12x
const CARD_M1 = [ 1, 12 ]
const CARD_S1 = [ 13, 24 ]
@@ -207,45 +177,45 @@ const EVENT_GOOD_AUGURIES = 14
const EVENT_DIOCLETIAN = 15
const CRISIS_TABLE_4P = [ 0, 0,
- "Ira Deorum",
+ 0,
SASSANIDS,
FRANKS,
SASSANIDS,
GOTHS,
- "Event",
+ 0,
ALAMANNI,
NOMADS,
FRANKS,
NOMADS,
- "Pax Deorum"
+ 0,
]
const CRISIS_TABLE_3P = [ 0, 0,
- "Ira Deorum",
+ 0,
FRANKS,
SASSANIDS,
SASSANIDS,
FRANKS,
- "Event",
+ 0,
ALAMANNI,
GOTHS,
GOTHS,
ALAMANNI,
- "Pax Deorum"
+ 0,
]
const CRISIS_TABLE_2P = [ 0, 0,
- "Ira Deorum",
+ 0,
FRANKS,
ALAMANNI,
FRANKS,
GOTHS,
- "Event",
+ 0,
GOTHS,
FRANKS,
ALAMANNI,
ALAMANNI,
- "Pax Deorum"
+ 0,
]
// ===
@@ -258,6 +228,8 @@ const states = {}
exports.scenarios = [ "Standard" ]
exports.roles = function (scenario, options) {
+ if (options.players == 1)
+ return [ "Solo" ]
if (options.players == 2)
return [ P1, P2 ]
if (options.players == 3)
@@ -312,7 +284,8 @@ exports.setup = function (seed, scenario, options) {
log: [],
undo: [],
active: 0,
- state: "setup",
+ current: 0,
+ state: "setup_province",
first: 0,
events: null,
active_events: [],
@@ -353,6 +326,11 @@ exports.setup = function (seed, scenario, options) {
setup_barbarians(NOMADS)
setup_barbarians(SASSANIDS)
+ if (player_count === 1) {
+ game.solo = 1
+ player_count = 4
+ }
+
for (let pi = 0; pi < player_count; ++pi) {
game.players[pi] = {
legacy: 0,
@@ -391,7 +369,7 @@ exports.setup = function (seed, scenario, options) {
remove_barbarians(SASSANIDS)
}
- game.first = game.active = random(player_count)
+ game.first = game.current = random(player_count)
log(PLAYER_NAMES[game.first] + " is First Player!")
return save_game()
@@ -399,11 +377,13 @@ exports.setup = function (seed, scenario, options) {
function load_game(state) {
game = state
- game.active = PLAYER_INDEX[game.active]
}
function save_game() {
- game.active = PLAYER_NAMES[game.active]
+ if (game.solo)
+ game.active = "Solo"
+ else
+ game.active = PLAYER_NAMES[game.current]
return game
}
@@ -421,6 +401,12 @@ exports.action = function (state, player, action, arg) {
return save_game()
}
+function is_current_player(player) {
+ if (player === 4)
+ return true
+ return game.current === player
+}
+
exports.view = function (state, player_name) {
let player = PLAYER_INDEX[player_name]
@@ -428,7 +414,7 @@ exports.view = function (state, player_name) {
view = {
log: game.log,
- active: PLAYER_NAMES[game.active],
+ current: game.current,
prompt: null,
militia: game.militia,
@@ -462,23 +448,25 @@ exports.view = function (state, player_name) {
if (game.state === "game_over") {
view.prompt = game.victory
- } else if (game.active !== player) {
+ } else if (player !== game.current && player_name !== "Solo") {
let inactive = states[game.state].inactive || game.state
- view.prompt = `Waiting for ${PLAYER_NAMES[game.active]} \u2014 ${inactive}...`
+ view.prompt = `Waiting for ${PLAYER_NAMES[game.current]} \u2014 ${inactive}...`
} else {
- view.hand = game.players[player].hand
- view.draw = game.players[player].draw
- view.discard = game.players[player].discard
-
view.actions = {}
states[game.state].prompt()
- view.prompt = PLAYER_NAMES[game.active] + ": " + view.prompt
+ view.prompt = PLAYER_NAMES[game.current] + ": " + view.prompt
if (game.undo && game.undo.length > 0)
view.actions.undo = 1
else
view.actions.undo = 0
}
+ if (player >= 0 && player <= game.players.length) {
+ view.hand = game.players[player].hand
+ view.draw = game.players[player].draw
+ view.discard = game.players[player].discard
+ }
+
save_game()
return view
}
@@ -489,8 +477,37 @@ function log(msg) {
game.log.push(msg)
}
+function log_br() {
+ if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
+ game.log.push("")
+}
+
+function log_h1(msg) {
+ log_br()
+ log(".h1 " + msg)
+ log_br()
+}
+
+function log_h2(msg) {
+ log_br()
+ log(".h2 " + msg)
+ log_br()
+}
+
+function logi(msg) {
+ game.log.push(">" + msg)
+}
+
+function logii(msg) {
+ game.log.push(">>" + msg)
+}
+
// === STATES ===
+function next_player() {
+ return (game.current + 1) % game.players.length
+}
+
function get_governor(r) {
for (let p = 0; p < game.players.length; ++p) {
for (let i = 0; i < 6; ++i)
@@ -511,6 +528,14 @@ function find_legion() {
return -1
}
+function current_hand() {
+ return game.players[game.current].hand
+}
+
+function current_draw() {
+ return game.players[game.current].draw
+}
+
function add_militia(r) {
game.militia |= (1 << r)
}
@@ -527,7 +552,7 @@ function set_support(r, level) {
game.support[r] = level
}
-states.setup = {
+states.setup_province = {
prompt() {
view.prompt = "Select a starting Province."
for (let r = 1; r < 12; ++r)
@@ -535,21 +560,236 @@ states.setup = {
gen_action("capital", r)
},
capital(r) {
- let p = game.active
+ push_undo()
+ let p = game.current
game.players[p].generals[0] = r
game.players[p].governors[0] = r
game.players[p].capital = 1
game.legions[find_legion()] = ARMY[p][0]
add_militia(r)
+ game.state = "setup_hand"
+ },
+}
- game.active = (game.active + 1) % game.players.length
- if (game.active === game.first)
+states.setup_hand = {
+ prompt() {
+ view.prompt = "Draw your initial hand."
+ let hand = current_hand()
+ if (hand.length < 5) {
+ for (let c of current_draw())
+ gen_action("card", c)
+ } else {
+ view.actions.done = 1
+ }
+ },
+ card(c) {
+ push_undo()
+ set_delete(current_draw(), c)
+ set_add(current_hand(), c)
+ },
+ done() {
+ clear_undo()
+ game.state = "setup_province"
+ game.current = next_player()
+ if (game.current === game.first)
goto_start_turn()
},
}
+function goto_start_turn() {
+ log_h2(PLAYER_NAMES[game.current])
+ goto_upkeep()
+}
+
+function goto_upkeep() {
+ goto_crisis()
+}
+
+// === CRISIS ===
+
+function goto_crisis() {
+ game.dice[0] = roll_die()
+ game.dice[1] = roll_die()
+
+ log(`Crisis B${game.dice[0]} W${game.dice[1]}`)
+
+ let sum = game.dice[0] + game.dice[1]
+
+ if (sum === 2)
+ return goto_ira_deorum()
+ if (sum === 12)
+ return goto_pax_deorum()
+ if (sum === 7)
+ return goto_event()
+
+ if (game.players.length === 2)
+ return goto_barbarian_crisis(CRISIS_TABLE_2P[sum])
+ if (game.players.length === 3)
+ return goto_barbarian_crisis(CRISIS_TABLE_3P[sum])
+ return goto_barbarian_crisis(CRISIS_TABLE_4P[sum])
+}
+
+function goto_ira_deorum() {
+ logi("Ira Deorum")
+ activate_one_barbarian(ALAMANNI)
+ activate_one_barbarian(FRANKS)
+ activate_one_barbarian(GOTHS)
+ activate_one_barbarian(NOMADS)
+ activate_one_barbarian(SASSANIDS)
+ goto_take_actions()
+}
+
+function goto_pax_deorum() {
+ logi("Pax Deorum")
+ logi("TODO")
+ goto_take_actions()
+}
+
+function goto_event() {
+ logi("Event")
+ logi("TODO")
+ goto_take_actions()
+}
+
+function is_barbarian_active(i) {
+ return !game.is_barbarian_inactive[i]
+}
+
+function is_barbarian_inactive(i) {
+ return game.is_barbarian_inactive[i]
+}
+
+function set_barbarian_active(i) {
+ game.is_barbarian_inactive[i] = 0
+}
+
+function set_barbarian_inactive(i) {
+ game.is_barbarian_inactive[i] = 1
+}
+
+function find_active_barbarian(tribe) {
+ let home = BARBARIAN_HOMELAND[tribe]
+ for (let i = tribe * 10; i < tribe * 10 + 10; ++i)
+ if (game.barbarians[i] === home && is_barbarian_active(i))
+ return i
+ return -1
+}
+
+function find_inactive_barbarian(tribe) {
+ let home = BARBARIAN_HOMELAND[tribe]
+ for (let i = tribe * 10; i < tribe * 10 + 10; ++i)
+ if (game.barbarians[i] === home && is_barbarian_inactive(i))
+ return i
+ return -1
+}
+
+function count_active_barbarians_at_home(tribe) {
+ let home = BARBARIAN_HOMELAND[tribe]
+ let n = 0
+ for (let i = tribe * 10; i < tribe * 10 + 10; ++i)
+ if (game.barbarians[i] === home && is_barbarian_active(i))
+ n += 1
+ return n
+}
+
+function count_barbarians(tribe, region) {
+ // TODO: count leaders
+ let n = 0
+ for (let i = tribe * 10; i < tribe * 10 + 10; ++i)
+ if (game.barbarians[i] === region)
+ n += 1
+ return n
+}
+
+function activate_one_barbarian(tribe) {
+ let i = find_inactive_barbarian(tribe)
+ if (i >= 0)
+ set_barbarian_active(i)
+}
+
+function goto_barbarian_crisis(tribe) {
+ logi(BARBARIAN_NAME[tribe])
+ activate_one_barbarian(tribe)
+
+ let black = game.dice[2] = roll_die()
+ let white = game.dice[3] = roll_die()
+
+ logi(`B${black} W${white}`)
+
+ if (black <= count_active_barbarians_at_home(tribe))
+ goto_barbarian_invasion(tribe, black, white)
+ else
+ goto_take_actions()
+}
+
+function invade_with_active_barbarian(tribe, region) {
+ // TODO: move leaders first
+ let i = find_active_barbarian(tribe)
+ if (i >= 0)
+ game.barbarians[i] = region
+}
+
+function goto_barbarian_invasion(tribe, black, white) {
+ logi("Invasion!")
+
+ let path = null
+ for (let list of BARBARIAN_INVASION[tribe])
+ if (white >= list[0] && white <= list[1])
+ path = list[2]
+
+ let k = 0
+ for (let i = 0; i < black;) {
+ let n = count_barbarians(tribe, path[k])
+ if (n < 3) {
+ invade_with_active_barbarian(tribe, path[k])
+ ++i
+ } else {
+ if (++k > path.length)
+ break
+ }
+ }
+
+ goto_take_actions()
+}
+
+// === TAKE ACTIONS ===
+
+function goto_take_actions() {
+ game.state = "take_actions"
+}
+
+states.take_actions = {
+ prompt() {
+ view.actions.end_actions = 1
+ },
+ end_actions() {
+ goto_expand_pretender_empire()
+ },
+}
+
+function goto_expand_pretender_empire() {
+ goto_gain_legacy()
+}
+
+function goto_gain_legacy() {
+ goto_buy_trash_cards()
+}
+
+function goto_buy_trash_cards() {
+ goto_end_of_turn()
+}
+
+function goto_end_of_turn() {
+ game.current = next_player()
+ goto_start_turn()
+}
+
// === COMMON LIBRARY ===
+function roll_die() {
+ return random(6) + 1
+}
+
function gen_action(action, argument) {
if (!(action in view.actions))
view.actions[action] = []