summaryrefslogtreecommitdiff
path: root/rules.js
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2023-05-27 19:28:16 +0200
committerTor Andersson <tor@ccxvii.net>2023-07-07 18:39:23 +0200
commit495090873cf62a1ca698cee18a2d292fc30aa292 (patch)
tree7b179325adf10a13ab3827ddafe320249a2d3e06 /rules.js
parent069a9310cea47805d5b8d55d04961b9e5b3d9527 (diff)
downloadtime-of-crisis-495090873cf62a1ca698cee18a2d292fc30aa292.tar.gz
Stuff.
Diffstat (limited to 'rules.js')
-rw-r--r--rules.js1722
1 files changed, 1304 insertions, 418 deletions
diff --git a/rules.js b/rules.js
index f1b960e..da23666 100644
--- a/rules.js
+++ b/rules.js
@@ -1,12 +1,28 @@
"use strict"
-// === CONSTANTS AND DATA ===
+var game
+var view
+const states = {}
const P1 = "Red"
const P2 = "Blue"
const P3 = "Yellow"
const P4 = "Green"
+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)
+ return [ P1, P2, P3 ]
+ return [ P1, P2, P3, P4 ]
+}
+
+// === CONSTANTS ===
+
const PLAYER_NAMES = [ P1, P2, P3, P4 ]
const PLAYER_INDEX = {
@@ -18,23 +34,26 @@ const PLAYER_INDEX = {
"Observer": -1,
}
-const NO_PLACE_GOVERNOR = -1
-const OFF_MAP = -1
-const AVAILABLE = -1
-const UNAVAILABLE = -2
+const MILITARY = 0
+const SENATE = 1
+const POPULACE = 2
+
+const LEGION_COUNT = 33
+
+// REGIONS
const ITALIA = 0
-const AEGYPTUS = 1
-const AFRICA = 2
-const ASIA = 3
-const BRITANNIA = 4
-const GALATIA = 5
-const GALLIA = 6
-const HISPANIA = 7
-const MACEDONIA = 8
-const PANNONIA = 9
-const SYRIA = 10
-const THRACIA = 11
+const ASIA = 1
+const GALLIA = 2
+const MACEDONIA = 3
+const PANNONIA = 4
+const THRACIA = 5
+const AEGYPTUS = 6
+const AFRICA = 7
+const HISPANIA = 8
+const BRITANNIA = 9
+const GALATIA = 10
+const SYRIA = 11
const ALAMANNI_HOMELAND = 12
const FRANKS_HOMELAND = 13
const GOTHS_HOMELAND = 14
@@ -44,40 +63,70 @@ const MARE_OCCIDENTALE = 17
const MARE_ORIENTALE = 18
const OCEANUS_ATLANTICUS = 19
const PONTUS_EUXINUS = 20
-
-const ARMY = [
- [ 100, 101, 102, 103, 104, 105 ],
- [ 200, 201, 202, 203, 204, 205 ],
- [ 300, 301, 302, 303, 304, 305 ],
- [ 400, 401, 402, 403, 404, 405 ]
-]
+const AVAILABLE = 21
+const UNAVAILABLE = 22
+const ARMY = 23
const REGION_NAME = [
"Italia",
- "Aegyptus",
- "Africa",
"Asia",
- "Britannia",
- "Galatia",
"Gallia",
- "Hispania",
"Macedonia",
"Pannonia",
- "Syria",
"Thracia",
+ "Aegyptus",
+ "Africa",
+ "Hispania",
+ "Britannia",
+ "Galatia",
+ "Syria",
"Alamanni Homeland",
"Franks Homeland",
"Goths Homeland",
"Nomads Homeland",
"Sassanids Homeland",
+ "Mare Occidentale",
+ "Mare Orientale",
+ "Oceanus Atlanticus",
+ "Pontus Euxinus",
+ "Available",
+ "Unavailable",
+]
+
+const ADJACENT = [
+ /* ITALIA */ [ GALLIA, PANNONIA, MARE_OCCIDENTALE ],
+ /* ASIA */ [ THRACIA, GALATIA, MARE_ORIENTALE, PONTUS_EUXINUS ],
+ /* GALLIA */ [ ITALIA, PANNONIA, HISPANIA, FRANKS_HOMELAND, MARE_OCCIDENTALE, OCEANUS_ATLANTICUS ],
+ /* MACEDONIA */ [ PANNONIA, THRACIA, MARE_OCCIDENTALE, MARE_ORIENTALE ],
+ /* PANNONIA */ [ ITALIA, GALLIA, MACEDONIA, THRACIA, ALAMANNI_HOMELAND, FRANKS_HOMELAND, MARE_OCCIDENTALE ],
+ /* THRACIA */ [ ASIA, MACEDONIA, PANNONIA, ALAMANNI_HOMELAND, GOTHS_HOMELAND, MARE_ORIENTALE, PONTUS_EUXINUS ],
+ /* AEGYPTUS */ [ AFRICA, SYRIA, NOMADS_HOMELAND, MARE_ORIENTALE ],
+ /* AFRICA */ [ AEGYPTUS, HISPANIA, NOMADS_HOMELAND, MARE_OCCIDENTALE, MARE_ORIENTALE, OCEANUS_ATLANTICUS ],
+ /* HISPANIA */ [ GALLIA, AFRICA, MARE_OCCIDENTALE, OCEANUS_ATLANTICUS ],
+ /* BRITANNIA */ [ OCEANUS_ATLANTICUS ],
+ /* GALATIA */ [ ASIA, SYRIA, SASSANIDS_HOMELAND, MARE_ORIENTALE, PONTUS_EUXINUS ],
+ /* SYRIA */ [ AEGYPTUS, GALATIA, SASSANIDS_HOMELAND, MARE_ORIENTALE ],
+ /* ALAMANNI_HOMELAND */ [ PANNONIA, THRACIA, FRANKS_HOMELAND, GOTHS_HOMELAND ],
+ /* FRANKS_HOMELAND */ [ GALLIA, PANNONIA, ALAMANNI_HOMELAND ],
+ /* GOTHS_HOMELAND */ [ THRACIA, ALAMANNI_HOMELAND, PONTUS_EUXINUS ],
+ /* NOMADS_HOMELAND */ [ AEGYPTUS, AFRICA, OCEANUS_ATLANTICUS ],
+ /* SASSANIDS_HOMELAND */ [ GALATIA, SYRIA, PONTUS_EUXINUS ],
+ /* MARE_OCCIDENTALE */ [ ITALIA, GALLIA, MACEDONIA, PANNONIA, AFRICA, HISPANIA, MARE_ORIENTALE, OCEANUS_ATLANTICUS ],
+ /* MARE_ORIENTALE */ [ ASIA, MACEDONIA, THRACIA, AEGYPTUS, AFRICA, GALATIA, SYRIA, MARE_OCCIDENTALE ],
+ /* OCEANUS_ATLANTICUS */ [ GALLIA, AFRICA, HISPANIA, BRITANNIA, NOMADS_HOMELAND, MARE_OCCIDENTALE ],
+ /* PONTUS_EUXINUS */ [ ASIA, THRACIA, GALATIA, GOTHS_HOMELAND, SASSANIDS_HOMELAND ],
]
+// BARBARIANS
+
const ALAMANNI = 0
const FRANKS = 1
const GOTHS = 2
const NOMADS = 3
const SASSANIDS = 4
+const TRIBE_COUNT = [ 0, 5, 3, 4, 5 ]
+
const BARBARIAN_NAME = [
"Alamanni",
"Franks",
@@ -86,22 +135,6 @@ const BARBARIAN_NAME = [
"Sassanids",
]
-const ALAMANNI_UNITS = [ 0, 9 ]
-const FRANKS_UNITS = [ 10, 19 ]
-const GOTHS_UNITS = [ 20, 29 ]
-const NOMADS_UNITS = [ 30, 39 ]
-const SASSANIDS_UNITS = [ 40, 49 ]
-
-const LEGION_UNITS = [ 50, 82 ]
-
-const BARBARIAN_UNITS = [
- ALAMANNI_UNITS,
- FRANKS_UNITS,
- GOTHS_UNITS,
- NOMADS_UNITS,
- SASSANIDS_UNITS,
-]
-
const BARBARIAN_HOMELAND = [
ALAMANNI_HOMELAND,
FRANKS_HOMELAND,
@@ -140,43 +173,7 @@ const BARBARIAN_INVASION = [
],
]
-// 12x
-const CARD_M1 = [ 1, 12 ]
-const CARD_S1 = [ 13, 24 ]
-const CARD_P1 = [ 25, 36 ]
-
-// 9x
-const CARD_M2 = [ 37, 45 ]
-const CARD_S2 = [ 46, 54 ]
-const CARD_P2 = [ 55, 63 ]
-
-// 8x
-const CARD_M3 = [ 64, 71 ]
-const CARD_S3 = [ 72, 79 ]
-const CARD_P3 = [ 80, 87 ]
-
-// 6x
-const CARD_M4 = [ 88, 93 ]
-const CARD_S4 = [ 94, 99 ]
-const CARD_P4 = [ 100, 105 ]
-
-const EVENT_PLAGUE_OF_CYPRIAN = 1
-const EVENT_ARDASHIR = 2
-const EVENT_PRIEST_KING = 3
-const EVENT_PALMYRA_ALLIES = 4
-const EVENT_SHAPUR_I = 5
-const EVENT_POSTUMUS = 6
-const EVENT_LUDI_SAECULARES = 7
-const EVENT_CNIVA = 8
-const EVENT_ZENOBIA = 9
-const EVENT_BAD_AUGURIES = 10
-const EVENT_RAIDING_PARTIES = 11
-const EVENT_PREPARING_FOR_WAR = 12
-const EVENT_INFLATION = 13
-const EVENT_GOOD_AUGURIES = 14
-const EVENT_DIOCLETIAN = 15
-
-const CRISIS_TABLE_4P = [ 0, 0,
+const CRISIS_TABLE_4P = [
0,
SASSANIDS,
FRANKS,
@@ -190,7 +187,7 @@ const CRISIS_TABLE_4P = [ 0, 0,
0,
]
-const CRISIS_TABLE_3P = [ 0, 0,
+const CRISIS_TABLE_3P = [
0,
FRANKS,
SASSANIDS,
@@ -204,7 +201,7 @@ const CRISIS_TABLE_3P = [ 0, 0,
0,
]
-const CRISIS_TABLE_2P = [ 0, 0,
+const CRISIS_TABLE_2P = [
0,
FRANKS,
ALAMANNI,
@@ -218,355 +215,471 @@ const CRISIS_TABLE_2P = [ 0, 0,
0,
]
-// ===
+// CARDS
-var game
-var view
+const EVENT_PLAGUE_OF_CYPRIAN = 1
+const EVENT_ARDASHIR = 2
+const EVENT_PRIEST_KING = 3
+const EVENT_PALMYRA_ALLIES = 4
+const EVENT_SHAPUR_I = 5
+const EVENT_POSTUMUS = 6
+const EVENT_LUDI_SAECULARES = 7
+const EVENT_CNIVA = 8
+const EVENT_ZENOBIA = 9
+const EVENT_BAD_AUGURIES = 10
+const EVENT_RAIDING_PARTIES = 11
+const EVENT_PREPARING_FOR_WAR = 12
+const EVENT_INFLATION = 13
+const EVENT_GOOD_AUGURIES = 14
+const EVENT_DIOCLETIAN = 15
-const states = {}
+// 12x
+const CARD_M1 = [ 1, 12 ]
+const CARD_S1 = [ 13, 24 ]
+const CARD_P1 = [ 25, 36 ]
-exports.scenarios = [ "Standard" ]
+// 9x
+const CARD_M2 = [ 37, 45 ]
+const CARD_S2 = [ 46, 54 ]
+const CARD_P2 = [ 55, 63 ]
-exports.roles = function (scenario, options) {
- if (options.players == 1)
- return [ "Solo" ]
- if (options.players == 2)
- return [ P1, P2 ]
- if (options.players == 3)
- return [ P1, P2, P3 ]
- return [ P1, P2, P3, P4 ]
-}
+// 8x
+const CARD_M3 = [ 64, 71 ]
+const CARD_S3 = [ 72, 79 ]
+const CARD_P3 = [ 80, 87 ]
-function setup_player_deck(pi) {
- return [
- CARD_M1[0] + (pi * 3) + 0,
- CARD_M1[0] + (pi * 3) + 1,
- CARD_M1[0] + (pi * 3) + 2,
- CARD_S1[0] + (pi * 3) + 0,
- CARD_S1[0] + (pi * 3) + 1,
- CARD_S1[0] + (pi * 3) + 2,
- CARD_P1[0] + (pi * 3) + 0,
- CARD_P1[0] + (pi * 3) + 1,
- CARD_P1[0] + (pi * 3) + 2,
- ]
-}
+// 6x
+const CARD_M4 = [ 88, 93 ]
+const CARD_S4 = [ 94, 99 ]
+const CARD_P4 = [ 100, 105 ]
-function setup_events() {
- let deck = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ]
- shuffle(deck)
- // Shuffle Diocletian with last 3 cards
- array_insert(deck, 11 + random(4), 15)
- return deck
+function card_name(c) {
+ if (c >= CARD_M1[0] && c <= CARD_M1[1]) return "M1"
+ if (c >= CARD_M2[0] && c <= CARD_M2[1]) return "M2"
+ if (c >= CARD_M3[0] && c <= CARD_M3[1]) return "M3"
+ if (c >= CARD_M4[0] && c <= CARD_M4[1]) return "M4"
+ if (c >= CARD_S1[0] && c <= CARD_S1[1]) return "S1"
+ if (c >= CARD_S2[0] && c <= CARD_S2[1]) return "S2"
+ if (c >= CARD_S3[0] && c <= CARD_S3[1]) return "S3"
+ if (c >= CARD_S4[0] && c <= CARD_S4[1]) return "S4"
+ if (c >= CARD_P1[0] && c <= CARD_P1[1]) return "P1"
+ if (c >= CARD_P2[0] && c <= CARD_P2[1]) return "P2"
+ if (c >= CARD_P3[0] && c <= CARD_P3[1]) return "P3"
+ if (c >= CARD_P4[0] && c <= CARD_P4[1]) return "P4"
+ return "??"
+}
+
+function card_event_name(c) {
+ if (c >= CARD_M1[0] && c <= CARD_M1[1]) return "None"
+ if (c >= CARD_M2[0] && c <= CARD_M2[1]) return "Castra"
+ if (c >= CARD_M3[0] && c <= CARD_M3[1]) return "Flanking Maneuver"
+ if (c >= CARD_M4[0] && c <= CARD_M4[1]) return "Praetorian Guard"
+ if (c >= CARD_S1[0] && c <= CARD_S1[1]) return "None"
+ if (c >= CARD_S2[0] && c <= CARD_S2[1]) return "Tribute"
+ if (c >= CARD_S3[0] && c <= CARD_S3[1]) return "Foederati"
+ if (c >= CARD_S4[0] && c <= CARD_S4[1]) return "Damnatio Memoriae"
+ if (c >= CARD_P1[0] && c <= CARD_P1[1]) return "None"
+ if (c >= CARD_P2[0] && c <= CARD_P2[1]) return "Quaestor"
+ if (c >= CARD_P3[0] && c <= CARD_P3[1]) return "Mob"
+ if (c >= CARD_P4[0] && c <= CARD_P4[1]) return "Pretender"
+ return "None"
+}
+
+function add_card_ip(c) {
+ if (c >= CARD_M1[0] && c <= CARD_M1[1]) return game.ip[MILITARY] += 1
+ if (c >= CARD_M2[0] && c <= CARD_M2[1]) return game.ip[MILITARY] += 2
+ if (c >= CARD_M3[0] && c <= CARD_M3[1]) return game.ip[MILITARY] += 3
+ if (c >= CARD_M4[0] && c <= CARD_M4[1]) return game.ip[MILITARY] += 4
+ if (c >= CARD_S1[0] && c <= CARD_S1[1]) return game.ip[SENATE] += 1
+ if (c >= CARD_S2[0] && c <= CARD_S2[1]) return game.ip[SENATE] += 2
+ if (c >= CARD_S3[0] && c <= CARD_S3[1]) return game.ip[SENATE] += 3
+ if (c >= CARD_S4[0] && c <= CARD_S4[1]) return game.ip[SENATE] += 4
+ if (c >= CARD_P1[0] && c <= CARD_P1[1]) return game.ip[POPULACE] += 1
+ if (c >= CARD_P2[0] && c <= CARD_P2[1]) return game.ip[POPULACE] += 2
+ if (c >= CARD_P3[0] && c <= CARD_P3[1]) return game.ip[POPULACE] += 3
+ if (c >= CARD_P4[0] && c <= CARD_P4[1]) return game.ip[POPULACE] += 4
+}
+
+function is_region(where) {
+ return where < AVAILABLE
+}
+
+function is_province(where) {
+ return where < 12
}
-function setup_market_pile(cards) {
- let pile = []
- for (let c = cards[0]; c <= cards[1]; ++c)
- pile.push(c)
- return pile
+function is_sea(where) {
+ return where >= MARE_OCCIDENTALE && where <= PONTUS_EUXINUS
}
-function setup_barbarians(tribe) {
- for (let i = BARBARIAN_UNITS[tribe][0]; i <= BARBARIAN_UNITS[tribe][1]; ++i)
- game.barbarians[i] = BARBARIAN_HOMELAND[tribe]
+function current_hand() { return game.hand[game.current] }
+function current_draw() { return game.draw[game.current] }
+function current_discard() { return game.discard[game.current] }
+
+// === BOARD STATE ===
+
+function is_no_place_governor(province) {
+ return province >= game.support.length
}
-function remove_barbarians(tribe) {
- for (let i = BARBARIAN_UNITS[tribe][0]; i <= BARBARIAN_UNITS[tribe][1]; ++i)
- game.barbarians[i] = OFF_MAP
+function get_support(province) { return game.support[province] }
+function set_support(province, level) { game.support[province] = level }
+
+function get_barbarian_location(id) { return game.barbarians[id] & 63 }
+function set_barbarian_location(id, loc) { game.barbarians[id] = loc }
+function is_barbarian_inactive(id) { return game.barbarians[id] & 64 }
+function is_barbarian_active(id) { return !is_barbarian_inactive(id) }
+function set_barbarian_inactive(id) { game.barbarians[id] |= 64 }
+function set_barbarian_active(id) { game.barbarians[id] &= 63 }
+
+function get_legion_location(ix) { return game.legions[ix] & 63 }
+function set_legion_location(ix, loc) { game.legions[ix] = loc }
+function is_legion_reduced(ix) { return game.legions[ix] & 64 }
+function set_legion_reduced(ix) { game.legions[ix] |= 64 }
+function set_legion_full_strength(ix) { game.legions[ix] &= 63 }
+
+function is_legion_unused(ix) {
+ return game.legions[ix] === AVAILABLE
}
-exports.setup = function (seed, scenario, options) {
- let player_count = options.players || 4
+function get_governor_location(id, loc) { return game.governors[id] & 63 }
+function set_governor_location(id, loc) { game.governors[id] = loc }
+function get_general_location(id) { return game.generals[id] & 63 }
- game = {
- seed: seed,
- log: [],
- undo: [],
- active: 0,
- current: 0,
- state: "setup_province",
- first: 0,
- events: null,
- active_events: [],
- support: new Array(12).fill(1),
- mobs: 0,
- militia: 0,
- quaestor: 0,
- legions: new Array(33).fill(OFF_MAP),
- is_legion_reduced: new Array(33).fill(0),
- barbarians: new Array(50).fill(OFF_MAP),
- is_barbarian_inactive: new Array(50).fill(1),
- barbarian_leaders: [ OFF_MAP, OFF_MAP, OFF_MAP ],
- rival_emperors: [ OFF_MAP, OFF_MAP, OFF_MAP ],
- amphitheater: 0,
- basilica: 0,
- limes: 0,
- dice: [ 0, 0, 0, 0 ], // first two are crisis table dice, second two are barbarian homeland dice
- market: null,
- players: [],
- }
+function set_general_location(id, loc) { game.generals[id] = loc }
+function is_general_inside_capital(id) { return game.generals[id] & 64 }
+function set_general_inside_capital(id) { game.generals[id] |= 64 }
+function set_general_outside_capital(id) { game.generals[id] &= 63 }
- game.events = setup_events()
- game.market = [
- setup_market_pile(CARD_M2),
- setup_market_pile(CARD_S2),
- setup_market_pile(CARD_P2),
- setup_market_pile(CARD_M3),
- setup_market_pile(CARD_S3),
- setup_market_pile(CARD_P3),
- setup_market_pile(CARD_M4),
- setup_market_pile(CARD_S4),
- setup_market_pile(CARD_P4),
- ]
+function has_general_castra(id) { return game.castra & (1 << id) }
+function add_general_castra(id) { game.castra |= (1 << id) }
+function remove_general_castra(id) { game.castra &= ~(1 << id) }
- setup_barbarians(ALAMANNI)
- setup_barbarians(FRANKS)
- setup_barbarians(GOTHS)
- setup_barbarians(NOMADS)
- setup_barbarians(SASSANIDS)
+function has_militia_castra(province) { return game.mcastra & (1 << province) }
+function add_militia_castra(province) { game.mcastra |= (1 << province) }
+function remove_militia_castra(province) { game.mcastra &= ~(1 << province) }
- if (player_count === 1) {
- game.solo = 1
- player_count = 4
- }
+function has_quaestor(province) { return game.quaestor & (1 << province) }
+function add_quaestor(province) { game.quaestor |= (1 << province) }
+function remove_quaestor(province) { game.quaestor &= ~(1 << province) }
- for (let pi = 0; pi < player_count; ++pi) {
- game.players[pi] = {
- legacy: 0,
- emperor_turns: 0,
- hand: [],
- draw: setup_player_deck(pi),
- discard: [],
- generals: [ AVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ],
- governors: [ AVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ],
- capital: 0,
- castra: 0,
- }
- }
+function has_militia(province) { return game.militia & (1 << province) }
+function add_militia(province) { game.militia |= (1 << province) }
+function remove_militia(province) { game.militia &= ~(1 << province) }
- if (player_count === 4) {
- game.support[ITALIA] = 8
- }
+function has_mob(province) { return game.mobs[province] > 0 }
+function get_mobs(province) { return game.mobs[province] }
+function add_one_mob(province) { game.mobs[province] ++ }
+function remove_one_mob(province) { game.mobs[province] -- }
+function remove_all_mobs(province) { game.mobs[province] = 0 }
- if (player_count === 3) {
- game.support[ITALIA] = 6
- game.support[HISPANIA] = NO_PLACE_GOVERNOR
- game.support[AFRICA] = NO_PLACE_GOVERNOR
- game.support[AEGYPTUS] = NO_PLACE_GOVERNOR
- remove_barbarians(NOMADS)
- }
+function has_amphitheater(province) { return game.amphitheater & (1 << province) }
+function has_basilica(province) { return game.basilica & (1 << province) }
+function has_limes(province) { return game.limes & (1 << province) }
+function add_amphitheater(province) { game.amphitheater |= (1 << province) }
+function add_basilica(province) { game.basilica |= (1 << province) }
+function add_limes(province) { game.limes |= (1 << province) }
- if (player_count === 2) {
- game.support[ITALIA] = 4
- game.support[BRITANNIA] = NO_PLACE_GOVERNOR
- game.support[HISPANIA] = NO_PLACE_GOVERNOR
- game.support[AFRICA] = NO_PLACE_GOVERNOR
- game.support[AEGYPTUS] = NO_PLACE_GOVERNOR
- game.support[SYRIA] = NO_PLACE_GOVERNOR
- game.support[GALATIA] = NO_PLACE_GOVERNOR
- remove_barbarians(NOMADS)
- remove_barbarians(SASSANIDS)
- }
+function is_breakaway(province) { return game.breakaway & (1 << province) }
+function add_breakaway(province) { game.breakaway |= (1 << province) }
+function remove_breakaway(province) { game.breakaway &= ~(1 << province) }
- game.first = game.current = random(player_count)
- log(PLAYER_NAMES[game.first] + " is First Player!")
+function is_seat_of_power(province) { return game.seat_of_power & (1 << province) }
+function add_seat_of_power(province) { game.seat_of_power |= (1 << province) }
+function remove_seat_of_power(province) { game.seat_of_power &= ~(1 << province) }
- return save_game()
+// === TRANSIENT STATE ===
+
+function has_placed_governor(province) { return game.placed & (1 << province) }
+function set_placed_governor(province) { game.placed |= (1 << province) }
+
+function has_general_battled(id) { return game.battled & (1 << id) }
+function set_general_battled(id) { game.battled |= (1 << id) }
+
+function has_militia_battled(province) { return game.mbattled & (1 << province) }
+function set_militia_battled(province) { game.mbattled |= (1 << province) }
+
+// === COMPOUND STATE ===
+
+function for_each_current_general(f) {
+ let a = game.current * 6
+ for (let id = a; id < a + 6; ++id)
+ f(id, get_general_location(id), is_general_inside_capital(id))
}
-function load_game(state) {
- game = state
+function for_each_current_governor(f) {
+ let a = game.current * 6
+ for (let id = a; id < a + 6; ++id)
+ f(id, get_governor_location(id))
}
-function save_game() {
- if (game.solo)
- game.active = "Solo"
- else
- game.active = PLAYER_NAMES[game.current]
- return game
+function for_each_general(f) {
+ let n = game.legacy.length * 6
+ for (let id = 0; id < n; ++id)
+ f(id, get_general_location(id), is_general_inside_capital(id))
}
-exports.action = function (state, player, action, arg) {
- load_game(state)
- let S = states[game.state]
- if (action in S) {
- S[action](arg)
- } else {
- if (action === "undo" && game.undo && game.undo.length > 0)
- pop_undo()
- else
- throw new Error("Invalid action: " + action)
- }
- return save_game()
+function for_each_governor(f) {
+ let n = game.legacy.length * 6
+ for (let id = 0; id < n; ++id)
+ f(id, get_governor_location(id))
}
-function is_current_player(player) {
- if (player === 4)
- return true
- return game.current === player
+function find_general(f) {
+ let n = game.legacy.length * 6
+ for (let id = 0; id < n; ++id)
+ if (f(id, get_general_location(id), is_general_inside_capital(id)))
+ return id
+ return -1
}
-exports.view = function (state, player_name) {
- let player = PLAYER_INDEX[player_name]
+function find_governor(f) {
+ let n = game.legacy.length * 6
+ for (let id = 0; id < n; ++id)
+ if (f(id, get_governor_location(id)))
+ return id
+ return -1
+}
- load_game(state)
+function some_general(f) {
+ let n = game.legacy.length * 6
+ for (let id = 0; id < n; ++id)
+ if (f(id, get_general_location(id), is_general_inside_capital(id)))
+ return true
+ return false
+}
- view = {
- log: game.log,
- current: game.current,
- prompt: null,
+function some_governor(f) {
+ let n = game.legacy.length * 6
+ for (let id = 0; id < n; ++id)
+ if (f(id, get_governor_location(id)))
+ return true
+ return false
+}
- militia: game.militia,
- support: game.support,
- quaestor: game.quaestor,
- mobs: game.mobs,
- legions: game.legions,
- is_legion_reduced: game.is_legion_reduced,
- barbarians: game.barbarians,
- is_barbarian_inactive: game.is_barbarian_inactive,
- barbarian_leaders: game.barbarian_leaders,
- rival_emperors: game.rival_emperors,
- amphitheater: game.amphitheater,
- basilica: game.basilica,
- limes: game.limes,
- dice: game.dice,
- market: game.market,
- players: [],
- }
+function next_player() {
+ return (game.current + 1) % game.legacy.length
+}
- for (let i = 0; i < game.players.length; ++i) {
- view.players[i] = {
- legacy: game.players[i].legacy,
- emperor_turns: game.players[i].emperor_turns,
- generals: game.players[i].generals,
- governors: game.players[i].governors,
- capital: game.players[i].capital,
- castra: game.players[i].castra,
- }
- }
+function find_unused_legion() {
+ for (let ix = 0; ix < LEGION_COUNT; ++ix)
+ if (get_legion_location(ix) === AVAILABLE)
+ return ix
+ return -1
+}
- if (game.state === "game_over") {
- view.prompt = game.victory
- } else if (player !== game.current && player_name !== "Solo") {
- let inactive = states[game.state].inactive || game.state
- view.prompt = `Waiting for ${PLAYER_NAMES[game.current]} \u2014 ${inactive}...`
- } else {
- view.actions = {}
- states[game.state].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
+function can_build_improvement(province) {
+ if (has_mob(province))
+ return false
+ if (has_active_barbarians(province))
+ return false
+ if (has_rival_emperor(province))
+ return false
+ if (has_enemy_army_in_capital(province))
+ return false
+ return true
+}
+
+function update_italia_support() {
+ if (is_neutral_province(ITALIA)) {
+ let n = 1
+ for (let s = 1; s < 12; ++s)
+ if (is_neutral_province(s))
+ ++n
+ if (n > 8)
+ n = 8
+ set_support(ITALIA, n)
}
+}
+
+function get_player_count() {
+ return game.legacy.length
+}
- 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
+function can_enter_capital(where) {
+ // No Capital
+ if (is_no_place_governor(where))
+ return false
+
+ // Occupied by General
+ if (some_general((id, loc, cap) => loc === where && cap))
+ return false
+
+ // Occupied by opponent Militia
+ if (has_militia(where)) {
+ if (!is_province_you_govern(where))
+ return false
}
- save_game()
- return view
+ return true
}
-// === MISC ===
+function get_province_governor(where) {
+ return find_governor((id, loc) => loc === where)
+}
-function log(msg) {
- game.log.push(msg)
+function get_capital_general(where) {
+ return find_general((id, loc, cap) => loc === where && cap)
}
-function log_br() {
- if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
- game.log.push("")
+function is_neutral_province(where) {
+ if (is_no_place_governor(where))
+ return false
+ return get_province_governor(where) < 0
}
-function log_h1(msg) {
- log_br()
- log(".h1 " + msg)
- log_br()
+function is_province_you_govern(where) {
+ let a = game.current * 6
+ let id = get_province_governor(where)
+ if (id >= a && id < a + 6)
+ return true
+ return false
}
-function log_h2(msg) {
- log_br()
- log(".h2 " + msg)
- log_br()
+function has_active_barbarians(where) {
+ return false // TODO
}
-function logi(msg) {
- game.log.push(">" + msg)
+function has_rival_emperor(where) {
+ return false // TODO
}
-function logii(msg) {
- game.log.push(">>" + msg)
+function has_enemy_army_in_capital(where) {
+ return false // TODO
}
-// === STATES ===
+function spend_ip(type, n) {
+ game.ip[type] -= n
+}
-function next_player() {
- return (game.current + 1) % game.players.length
+function can_place_governor(where) {
+ if (is_province_you_govern(where))
+ return false
+ // Recalled or already attempted to place.
+ if (has_placed_governor(where))
+ return false
+ // Cannot Place in breakaway provinces
+ if (is_breakaway(where) || is_seat_of_power(where))
+ return false
+ return true
}
-function get_governor(r) {
- for (let p = 0; p < game.players.length; ++p) {
- for (let i = 0; i < 6; ++i)
- if (game.players[p].governors[i] === r)
- return p
- }
+function find_active_barbarian(tribe) {
+ let home = BARBARIAN_HOMELAND[tribe]
+ for (let i = 0; i < 10; ++i)
+ if (get_barbarian_location(tribe * 10 + i) === home)
+ if (is_barbarian_active(tribe * 10 + i))
+ return tribe * 10 + i
return -1
}
-function is_neutral_province(r) {
- return (game.support[r] !== NO_PLACE_GOVERNOR) && (get_governor(r) < 0)
+function count_active_barbarians_at_home(tribe) {
+ let home = BARBARIAN_HOMELAND[tribe]
+ let n = 0
+ for (let i = 0; i < 10; ++i)
+ if (get_barbarian_location(tribe * 10 + i) === home)
+ if (is_barbarian_active(tribe * 10 + i))
+ n += 1
+ return n
}
-function find_legion() {
- for (let i = 0; i < 33; ++i)
- if (game.legions[i] < 0)
- return i
+function find_inactive_barbarian_at_home(tribe) {
+ let home = BARBARIAN_HOMELAND[tribe]
+ for (let i = 0; i < 10; ++i)
+ if (get_barbarian_location(tribe * 10 + i) === home)
+ if (is_barbarian_inactive(tribe * 10 + i))
+ return tribe * 10 + i
return -1
}
-function current_hand() {
- return game.players[game.current].hand
+function activate_one_barbarian(tribe) {
+ let i = find_inactive_barbarian_at_home(tribe)
+ if (i >= 0)
+ set_barbarian_active(i)
+}
+
+function count_barbarians_in_province(tribe, where) {
+ let n = 0
+ for (let i = 0; i < 10; ++i)
+ if (get_barbarian_location(tribe * 10 + i) === where)
+ n += 1
+ return n
}
-function current_draw() {
- return game.players[game.current].draw
+
+function count_legions_in_army(id) {
+ let n = 0
+ for (let i = 0; i < LEGION_COUNT; ++i)
+ if (get_legion_location(i) === ARMY + id)
+ ++n
+ return n
}
-function add_militia(r) {
- game.militia |= (1 << r)
+function find_reduced_legion_in_army(id) {
+ for (let i = 0; i < LEGION_COUNT; ++i)
+ if (get_legion_location(i) === ARMY + id && is_legion_reduced(i))
+ return i
+ return -1
}
-function remove_militia(r) {
- game.militia &= ~(1 << r)
+function has_reduced_legions_in_army(id) {
+ return find_reduced_legion_in_army(id) >= 0
}
-function get_support(r) {
- return game.support[r]
+function count_units_in_army(id) {
+ let n = 0
+ for (let i = 0; i < LEGION_COUNT; ++i)
+ if (get_legion_location(i) === ARMY + id)
+ ++n
+ for (let loc of game.barbarians)
+ if (loc === ARMY + id)
+ ++n
+ return n
}
-function set_support(r, level) {
- game.support[r] = level
+function roll_dice(count, target) {
+ let hits = 0
+ console.log("roll_dice", count, target)
+ while (count > 0) {
+ let summary = []
+ let sixes = 0
+ for (let i = 0; i < count; ++i) {
+ let die = roll_die()
+ if (die === 6)
+ sixes += 1
+ if (die >= target) {
+ summary.push("B" + die)
+ hits += 1
+ } else {
+ summary.push("W" + die)
+ }
+ }
+ log("Rolled " + summary.join(" "))
+ count = sixes
+ }
+ return hits
}
+// === SETUP ===
+
states.setup_province = {
prompt() {
view.prompt = "Select a starting Province."
- for (let r = 1; r < 12; ++r)
- if (is_neutral_province(r))
- gen_action("capital", r)
+ for (let where = 2; where <= 12; ++where)
+ if (is_neutral_province(where))
+ gen_action("capital", where)
},
- capital(r) {
+ capital(where) {
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)
+
+ set_governor_location(game.current * 6 + 0, where)
+
+ add_militia(where)
+
+ set_general_location(game.current * 6 + 0, where)
+ set_general_inside_capital(game.current * 6 + 0)
+ set_legion_location(find_unused_legion(), ARMY + game.current * 6 + 0)
+
game.state = "setup_hand"
},
}
@@ -577,7 +690,7 @@ states.setup_hand = {
let hand = current_hand()
if (hand.length < 5) {
for (let c of current_draw())
- gen_action("card", c)
+ gen_action_card(c)
} else {
view.actions.done = 1
}
@@ -596,6 +709,8 @@ states.setup_hand = {
},
}
+// === UPKEEP ===
+
function goto_start_turn() {
log_h2(PLAYER_NAMES[game.current])
goto_upkeep()
@@ -607,6 +722,8 @@ function goto_upkeep() {
// === CRISIS ===
+// TODO: manual barbarian invasions!
+
function goto_crisis() {
game.dice[0] = roll_die()
game.dice[1] = roll_die()
@@ -622,11 +739,11 @@ function goto_crisis() {
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])
+ if (game.legacy.length === 2)
+ return goto_barbarian_crisis(CRISIS_TABLE_2P[sum - 2])
+ if (game.legacy.length === 3)
+ return goto_barbarian_crisis(CRISIS_TABLE_3P[sum - 2])
+ return goto_barbarian_crisis(CRISIS_TABLE_4P[sum - 2])
}
function goto_ira_deorum() {
@@ -634,8 +751,10 @@ function goto_ira_deorum() {
activate_one_barbarian(ALAMANNI)
activate_one_barbarian(FRANKS)
activate_one_barbarian(GOTHS)
- activate_one_barbarian(NOMADS)
- activate_one_barbarian(SASSANIDS)
+ if (game.legacy.length > 3)
+ activate_one_barbarian(NOMADS)
+ if (game.legacy.length > 2)
+ activate_one_barbarian(SASSANIDS)
goto_take_actions()
}
@@ -651,62 +770,6 @@ function goto_event() {
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)
@@ -722,11 +785,11 @@ function goto_barbarian_crisis(tribe) {
goto_take_actions()
}
-function invade_with_active_barbarian(tribe, region) {
+function invade_with_active_barbarian(tribe, where) {
// TODO: move leaders first
- let i = find_active_barbarian(tribe)
- if (i >= 0)
- game.barbarians[i] = region
+ let b = find_active_barbarian(tribe)
+ if (b >= 0)
+ set_barbarian_location(tribe * 10 + b, where)
}
function goto_barbarian_invasion(tribe, black, white) {
@@ -739,7 +802,7 @@ function goto_barbarian_invasion(tribe, black, white) {
let k = 0
for (let i = 0; i < black;) {
- let n = count_barbarians(tribe, path[k])
+ let n = count_barbarians_in_province(tribe, path[k])
if (n < 3) {
invade_with_active_barbarian(tribe, path[k])
++i
@@ -756,34 +819,833 @@ function goto_barbarian_invasion(tribe, black, white) {
function goto_take_actions() {
game.state = "take_actions"
+ game.ip = [ 0, 0, 0 ]
+ game.played = []
+ game.used = []
+ game.placed = 0 // only place governor once (and no place if recalled)
+}
+
+function prompt(s) {
+ view.prompt = s
+}
+
+function prompt_take_actions(label) {
+ let [ mip, sip, pip ] = game.ip
+ prompt(`${label}: ${mip} Military, ${sip} Senate, ${pip} Populace.`)
+}
+
+function action_take_actions_card(c) {
+ push_undo()
+ let hand = current_hand()
+ if (set_has(hand, c)) {
+ set_delete(hand, c)
+ set_add(game.played, c)
+ add_card_ip(c)
+ } else if (set_has(game.played, c)) {
+ log("TODO - use event")
+ set_remove(game.played, c)
+ set_add(current_discard(), c)
+ }
+}
+
+function action_take_actions_end_actions() {
+ push_undo()
+ goto_support_check()
}
states.take_actions = {
prompt() {
+ let player = game.current
+ let [ mip, sip, pip ] = game.ip
+
+ prompt_take_actions("Take Actions")
+ for (let c of current_hand())
+ gen_action_card(c)
+
+ for (let i = 0; i < 6; ++i) {
+ let id = game.current * 6 + i
+ let where = get_governor_location(id)
+ if ((where === UNAVAILABLE) && (sip >= i))
+ gen_action_governor(id)
+ if ((where === AVAILABLE) && (sip >= 1))
+ gen_action_governor(id)
+ if (is_region(where) && (sip >= 1 || pip >= 1))
+ gen_action_governor(id)
+ }
+
+ for (let i = 0; i < 6; ++i) {
+ let id = game.current * 6 + i
+ let where = get_general_location(id)
+ if ((where === UNAVAILABLE) && (mip >= i))
+ gen_action_general(id)
+ if ((where === AVAILABLE) && (mip >= 1))
+ gen_action_general(id)
+ // if (is_region(where) && (mip >= 1))
+ if (is_region(where) && (mip >= 1 || where <= 12))
+ gen_action_general(id)
+ }
+
view.actions.end_actions = 1
},
- end_actions() {
- goto_expand_pretender_empire()
+ card: action_take_actions_card,
+ end_actions: action_take_actions_end_actions,
+ governor(id) {
+ push_undo()
+ let where = get_governor_location(id)
+ if (where === UNAVAILABLE) {
+ spend_ip(SENATE, id % 6)
+ set_governor_location(id, AVAILABLE)
+ }
+ else if (where === AVAILABLE) {
+ game.who = id
+ spend_ip(SENATE, 1)
+ game.state = "place_governor_where"
+ game.misc = { spend: 1, where: -1 }
+ }
+ else {
+ game.who = id
+ game.state = "take_actions_governor"
+ }
+ },
+ general(id) {
+ push_undo()
+ let where = get_general_location(id)
+ if (where === UNAVAILABLE) {
+ spend_ip(MILITARY, id % 6)
+ set_general_location(id, AVAILABLE)
+ }
+ else if (where === AVAILABLE) {
+ spend_ip(MILITARY, 1)
+ game.who = id
+ game.state = "create_army"
+ }
+ else {
+ game.who = id
+ game.state = "take_actions_general"
+ }
+ },
+}
+
+states.take_actions_governor = {
+ prompt() {
+ let [ mip, sip, pip ] = game.ip
+ let governor = game.who
+ let where = get_governor_location(game.who)
+
+ prompt_take_actions("Take Governor Actions")
+ for (let c of current_hand())
+ gen_action_card(c)
+ view.actions.done = 1
+
+ // Recruit Governor
+ if (where === UNAVAILABLE) {
+ if (sip >= game.who % 6)
+ view.actions.recruit = 1
+ }
+
+ // Place Governor
+ if (where === AVAILABLE) {
+ if (sip >= 1)
+ view.actions.place = 1
+ }
+
+ if (where >= 0) {
+ // Recall Governor
+ if (sip >= 2)
+ view.actions.recall = 1
+
+ // Increase Support Level
+ let support = game.support[where]
+ if (support < 4 && where !== ITALIA) {
+ if (pip > support)
+ view.actions.support = 1
+
+ }
+
+ // Place Militia
+ if (!has_militia(where)) {
+ if (pip >= 2)
+ view.actions.militia = 1
+ }
+
+ // Hold Games
+ if (has_mob(where)) {
+ if (pip >= 2)
+ view.actions.hold_games = 1
+ }
+
+ // Build an Improvement
+ if (!has_amphitheater(where) || !has_basilica(where) || !has_limes(where)) {
+ if (can_build_improvement(where))
+ if (pip >= 3)
+ view.actions.build_improvement = 1
+ }
+
+ // TODO: Initiate Battle with Militia not stacked with General
+ }
+ },
+ card: action_take_actions_card,
+ end_actions: action_take_actions_end_actions,
+ governor(id) {
+ push_undo()
+ game.state = "take_actions"
+ },
+ recruit() {
+ push_undo()
+ log("Recruited Governor " + (game.who % 6) + ".")
+ spend_ip(SENATE, game.who % 6)
+ set_governor_location(game.who, AVAILABLE)
+ },
+ place() {
+ push_undo()
+ spend_ip(SENATE, 1)
+ game.state = "place_governor"
+ game.misc = { spend: 1 }
+ },
+ recall() {
+ push_undo()
+ let where = get_governor_location(game.who)
+ log("Recalled Governor from S" + where + ".")
+ spend_ip(SENATE, 2)
+ set_governor_location(game.who, AVAILABLE)
+ set_support(where, 1)
+ set_placed_governor(where)
+ update_italia_support()
+ },
+ support() {
+ push_undo()
+ let where = get_governor_location(game.who)
+ let support = game.support[where]
+ log("Built Support in S" + where + ".")
+ spend_ip(POPULACE, support + 1)
+ game.support[where] = support + 1
+ },
+ hold_games() {
+ push_undo()
+ let where = get_governor_location(game.who)
+ log("Held Games in S" + where + ".")
+ spend_ip(POPULACE, 3)
+ remove_one_mob(where)
+ },
+ build_improvement() {
+ push_undo()
+ game.state = "build_improvement"
+ spend_ip(POPULACE, 3)
+ },
+ done() {
+ push_undo()
+ game.state = "take_actions"
+ },
+}
+
+states.take_actions_general = {
+ prompt() {
+ let [ mip, sip, pip ] = game.ip
+ let where = get_general_location(game.who)
+
+ prompt_take_actions("Take General Actions")
+ for (let c of current_hand())
+ gen_action_card(c)
+ view.actions.done = 1
+
+ gen_action_general(game.who)
+
+ // Recruit General
+ if (where === UNAVAILABLE) {
+ if (mip >= game.who % 6)
+ view.actions.recruit = 1
+ }
+
+ // Create Army
+ if (where === AVAILABLE) {
+ if (mip >= 1 && find_unused_legion() >= 0)
+ view.actions.create_army = 1
+ }
+
+ if (where >= 0) {
+ // Add Legion to Army
+ if (is_province_you_govern(where)) {
+ let cost = count_legions_in_army(game.who) + 1
+ if (mip >= cost)
+ view.actions.add_legion_to_army = 1
+ }
+
+ // Train Legions
+ if (has_reduced_legions_in_army(game.who)) {
+ if (mip >= 1)
+ view.actions.train_legions = 1
+ }
+
+ // Disperse Mob
+ if (has_mob(where)) {
+ if (mip >= 1)
+ view.actions.disperse_mob = 1
+ }
+
+ if (!has_general_battled(game.who)) {
+ // Move Army
+ if (mip >= 1) {
+ for (let to of ADJACENT[where]) {
+ if (!is_sea(to))
+ gen_action_region(to)
+ else if (mip >= 2)
+ gen_action_region(to)
+ }
+ }
+
+ // Initiate Battle
+
+ // Free Action: Enter/Leave capital
+ if (where < 12) {
+ if (is_general_inside_capital(game.who))
+ view.actions.leave = 1
+ else if (can_enter_capital(where))
+ view.actions.enter = 1
+ }
+ }
+ }
+
+ },
+ card: action_take_actions_card,
+ end_actions: action_take_actions_end_actions,
+ general(id) {
+ push_undo()
+ game.state = "take_actions"
+ },
+ recruit() {
+ push_undo()
+ let general = game.who
+ log("Recruited General " + general + ".")
+ spend_ip(MILITARY, general)
+ set_piece_location(game.who, AVAILABLE)
+ },
+ add_legion_to_army() {
+ push_undo()
+ let general = game.who
+ let cost = count_legions_in_army(game.who) + 1
+ log("Added Legion to Army.")
+ spend_ip(MILITARY, cost)
+ set_legion_location(find_unused_legion(), ARMY + game.who)
+ },
+ train_legions() {
+ push_undo()
+ let general = game.who
+ let i = find_reduced_legion_in_army(game.who)
+ log("Trained Legions.")
+ spend_ip(MILITARY, 1)
+ set_legion_full_strength(i)
+ },
+ create_army() {
+ push_undo()
+ spend_ip(MILITARY, 1)
+ game.state = "create_army"
+ },
+ region(to) {
+ push_undo()
+ move_army_to(game.who, to)
+ },
+ enter() {
+ push_undo()
+ set_general_inside_capital(game.who)
+ },
+ leave() {
+ push_undo()
+ set_general_outside_capital(game.who)
+ },
+ capital() {
+ this.enter()
+ },
+ done() {
+ push_undo()
+ game.state = "take_actions"
+ },
+}
+
+states.place_governor_where = {
+ prompt() {
+ prompt("Place Governor.")
+ for (let where = 0; where < 12; ++where) {
+ if (can_place_governor(where))
+ gen_action_region(where)
+ }
+ },
+ region(where) {
+ push_undo()
+ game.misc.where = where
+ game.state = "place_governor_spend"
+ },
+}
+
+// ACTION: PLACE GOVERNOR
+
+function place_governor(new_governor, where) {
+ let support = get_support(where)
+ let new_support = Math.max(1, support - 1)
+
+ // TODO: Italia support
+
+ let old_governor = get_province_governor(where)
+ if (old_governor >= 0)
+ set_governor_location(old_governor, AVAILABLE)
+
+ set_governor_location(new_governor, where)
+ set_support(where, new_support)
+}
+
+function calc_needed_votes(where) {
+ let n = get_support(where) * 2 // base number of votes
+ let old_governor = get_province_governor(where)
+ if (old_governor >= 0) {
+ let old_player = old_governor / 6 | 0
+ let army_general = get_capital_general(where)
+ if (army_general >= 0) {
+ let army_player = army_general / 6 | 0
+ let army_size = count_units_in_army(army_general)
+ if (army_player === old_player)
+ n += army_size
+ else if (army_player === game.current)
+ n -= army_size
+ }
+ if (has_militia(where))
+ n += 1
+ }
+ console.log("votes needed", where, n)
+ return Math.max(1, n)
+}
+
+states.place_governor_spend = {
+ prompt() {
+ let [ mip, sip, pip ] = game.ip
+ let need = calc_needed_votes(game.misc.where)
+ prompt("Place Governor: " + game.misc.spend + " IP spent; " + need + " votes needed.")
+ if (sip >= 1)
+ view.actions.spend = 1
+ else
+ view.actions.spend = 0
+ view.actions.roll = 1
+ },
+ spend() {
+ push_undo()
+ spend_ip(SENATE, 1)
+ game.misc.spend += 1
+ },
+ roll() {
+ let need = calc_needed_votes(game.misc.where)
+ let have = 0
+
+ set_placed_governor(game.misc.where)
+
+ log("Place Governor in S" + game.misc.where)
+ if (is_neutral_province(game.misc.where))
+ have = roll_dice(game.misc.spend, 1)
+ else if (has_quaestor(game.misc.where))
+ have = roll_dice(game.misc.spend, 3)
+ else
+ have = roll_dice(game.misc.spend, 2)
+
+ if (have >= need)
+ place_governor(game.who, game.misc.where)
+
+ game.misc = null
+ game.state = "take_actions"
+ },
+}
+
+// ACTION: BUILD IMPROVEMENT
+
+states.build_improvement = {
+ prompt() {
+ let where = get_governor_location(game.who)
+ prompt("Build Improvement.")
+ if (!has_amphitheater(where))
+ view.actions.amphitheater = 1
+ if (!has_basilica(where))
+ view.actions.basilica = 1
+ if (!has_limes(where))
+ view.actions.limes = 1
+ },
+ amphitheater() {
+ let where = get_governor_location(game.who)
+ add_amphitheater(where)
+ log("Built Amphitheater in S" + where + ".")
+ game.state = "take_actions_governor"
+ },
+ basilica() {
+ let where = get_governor_location(game.who)
+ add_basilica(where)
+ log("Built Basilica in S" + where + ".")
+ game.state = "take_actions_governor"
+ },
+ limes() {
+ let where = get_governor_location(game.who)
+ add_limes(where)
+ log("Built Limes in S" + where + ".")
+ game.state = "take_actions_governor"
+ },
+}
+
+// ACTION: CREATE ARMY
+
+states.create_army = {
+ prompt() {
+ prompt("Create Army.")
+ for (let i = 0; i < 6; ++i) {
+ let where = get_governor_location(game.current * 6 + i)
+ if (is_province(where))
+ gen_action_region(where)
+ }
+ },
+ region(where) {
+ push_undo()
+ log("Created Army in S" + where + ".")
+ set_general_location(game.who, where)
+ if (can_enter_capital(where))
+ set_general_inside_capital(game.who)
+ set_legion_location(find_unused_legion(), ARMY + game.who)
+ game.state = "take_actions_general"
},
}
+// ACTION: MOVE ARMY
+
+states.move_army_at_sea = {
+ prompt() {
+ prompt("Move Army.")
+ let [ mip, sip, pip ] = game.ip
+ let who = view.who = game.who
+ let from = get_piece_location(who)
+ for (let to of ADJACENT[from]) {
+ if (is_sea(to)) {
+ if (mip >= 2)
+ gen_action_region(to)
+ } else {
+ gen_action_region(to)
+ }
+ }
+ },
+ region(to) {
+ push_undo()
+ move_army_to(game.who, to)
+ },
+}
+
+function move_army_to(who, to) {
+ log("Moved Army to S" + to + ".")
+
+ spend_ip(MILITARY, 1)
+ set_general_location(who, to)
+ if (can_enter_capital(to))
+ set_general_inside_capital(who)
+
+ if (is_sea(to))
+ game.state = "move_army_at_sea"
+ else if (game.ip[MILITARY] > 0)
+ game.state = "take_actions_general"
+ else
+ game.state = "take_actions"
+}
+
+// === SUPPORT CHECK ===
+
+function goto_support_check() {
+ goto_expand_pretender_empire()
+}
+
+// === EXPAND PRETENDER EMPIRE ===
+
function goto_expand_pretender_empire() {
goto_gain_legacy()
}
+// === GAIN LEGACY ===
+
function goto_gain_legacy() {
goto_buy_trash_cards()
}
+// === BUY / TRASH CARDS ===
+
function goto_buy_trash_cards() {
+ let discard = current_discard()
+ for (let c of game.played) {
+ set_add(discard, c)
+ }
+ game.played.length = 0
+ game.state = "buy_trash_discard"
+ game.misc = {
+ count: 0,
+ pp: 0,
+ }
goto_end_of_turn()
}
+// === END OF TURN ===
+
function goto_end_of_turn() {
game.current = next_player()
goto_start_turn()
}
+// === SETUP ===
+
+function setup_player_deck(player) {
+ return [
+ CARD_M1[0] + (player * 3) + 0,
+ CARD_M1[0] + (player * 3) + 1,
+ CARD_M1[0] + (player * 3) + 2,
+ CARD_S1[0] + (player * 3) + 0,
+ CARD_S1[0] + (player * 3) + 1,
+ CARD_S1[0] + (player * 3) + 2,
+ CARD_P1[0] + (player * 3) + 0,
+ CARD_P1[0] + (player * 3) + 1,
+ CARD_P1[0] + (player * 3) + 2,
+ ]
+}
+
+function setup_events() {
+ let deck = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ]
+ shuffle(deck)
+ // Shuffle Diocletian with last 3 cards
+ array_insert(deck, 11 + random(4), 15)
+ return deck
+}
+
+function setup_market_pile(cards) {
+ let pile = []
+ for (let c = cards[0]; c <= cards[1]; ++c)
+ pile.push(c)
+ return pile
+}
+
+function setup_barbarians(tribe, home) {
+ for (let i = 0; i < 10; ++i) {
+ set_barbarian_location(tribe * 10 + i, home)
+ set_barbarian_inactive(tribe * 10 + i)
+ }
+}
+
+exports.setup = function (seed, scenario, options) {
+ let real_player_count = options.players || 4
+ let player_count = real_player_count
+ if (player_count === 1)
+ player_count = 4
+ let tribe_count = TRIBE_COUNT[player_count]
+
+ game = {
+ seed: seed,
+ log: [],
+ undo: [],
+ active: 0,
+ current: 0,
+ state: "setup_province",
+ first: 0,
+ events: null,
+ active_events: [],
+
+ ip: [],
+ who: -1,
+ played: [],
+ used: [],
+ placed: 0,
+ battled: 0,
+
+ // grab bag of temporary data for the current procedure
+ misc: null,
+
+ support: new Array(12).fill(1),
+ mobs: new Array(12).fill(0),
+ militia: 0,
+ quaestor: 0,
+ castra: 0,
+ mcastra: 0,
+ amphitheater: 0,
+ basilica: 0,
+ limes: 0,
+ breakaway: 0,
+ seat_of_power: 0,
+
+ governors: new Array(6 * player_count).fill(UNAVAILABLE),
+ generals: new Array(6 * player_count).fill(UNAVAILABLE),
+ legions: new Array(LEGION_COUNT).fill(AVAILABLE),
+ barbarians: new Array(10 * tribe_count).fill(0),
+ rivals: [ UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ],
+ barbarian_leaders: [ UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ],
+
+ dice: [ 0, 0, 0, 0 ], // first two are crisis table dice, second two are barbarian homeland dice
+ market: null,
+
+ // per-player data
+ legacy: new Array(player_count).fill(0),
+ emperor_turns: new Array(player_count).fill(0),
+ hand: [],
+ draw: [],
+ discard: [],
+ }
+
+ if (real_player_count === 1)
+ game.solo = 1
+
+ game.events = setup_events()
+
+ game.market = [
+ setup_market_pile(CARD_M2),
+ setup_market_pile(CARD_S2),
+ setup_market_pile(CARD_P2),
+ setup_market_pile(CARD_M3),
+ setup_market_pile(CARD_S3),
+ setup_market_pile(CARD_P3),
+ setup_market_pile(CARD_M4),
+ setup_market_pile(CARD_S4),
+ setup_market_pile(CARD_P4),
+ ]
+
+ setup_barbarians(ALAMANNI, ALAMANNI_HOMELAND)
+ setup_barbarians(FRANKS, FRANKS_HOMELAND)
+ setup_barbarians(GOTHS, GOTHS_HOMELAND)
+ if (player_count >= 3)
+ setup_barbarians(NOMADS, NOMADS_HOMELAND)
+ if (player_count >= 4)
+ setup_barbarians(SASSANIDS, SASSANIDS_HOMELAND)
+
+ for (let player = 0; player < player_count; ++player) {
+ game.hand[player] = []
+ game.draw[player] = setup_player_deck(player)
+ game.discard[player] = []
+ }
+
+ update_italia_support()
+
+ game.first = game.current = random(player_count)
+ log("First Player is " + PLAYER_NAMES[game.first] + "!")
+
+ return save_game()
+}
+
+function load_game(state) {
+ game = state
+}
+
+function save_game() {
+ if (game.solo)
+ game.active = "Solo"
+ else
+ game.active = PLAYER_NAMES[game.current]
+ return game
+}
+
+exports.action = function (state, player, action, arg) {
+ load_game(state)
+ let S = states[game.state]
+ if (action in S) {
+ S[action](arg)
+ } else {
+ if (action === "undo" && game.undo && game.undo.length > 0)
+ pop_undo()
+ else
+ throw new Error("Invalid action: " + action)
+ }
+ return save_game()
+}
+
+function is_current_player(player) {
+ if (player === 4)
+ return true
+ return game.current === player
+}
+
+exports.view = function (state, player_name) {
+ load_game(state)
+
+ let player = PLAYER_INDEX[player_name]
+ if (game.solo)
+ player = game.current
+ let player_count = game.legacy.length
+
+ view = {
+ log: game.log,
+ current: game.current,
+ prompt: null,
+
+ support: game.support,
+ mobs: game.mobs,
+ militia: game.militia,
+ quaestor: game.quaestor,
+ castra: game.castra,
+ amphitheater: game.amphitheater,
+ basilica: game.basilica,
+ limes: game.limes,
+
+ governors: game.governors,
+ generals: game.generals,
+ legions: game.legions,
+ barbarians: game.barbarians,
+ rivals: game.rivals,
+ barbarian_leaders: game.barbarian_leaders,
+
+ dice: game.dice,
+ events: game.active_events,
+ played: game.played,
+ market: game.market,
+
+ legacy: game.legacy,
+ emperor_turns: game.emperor_turns,
+ }
+
+ if (game.state === "game_over") {
+ view.prompt = game.victory
+ } else if (player !== game.current && player_name !== "Solo") {
+ let inactive = states[game.state].inactive || game.state
+ view.prompt = `Waiting for ${PLAYER_NAMES[game.current]} \u2014 ${inactive}...`
+ } else {
+ view.actions = {}
+ states[game.state].prompt()
+ if (game.undo && game.undo.length > 0)
+ view.actions.undo = 1
+ else
+ view.actions.undo = 0
+ }
+
+ if (player >= 0 && player < player_count) {
+ view.hand = game.hand[player]
+ view.draw = game.draw[player]
+ view.discard = game.discard[player]
+ }
+
+ save_game()
+ return view
+}
+
+// === MISC ===
+
+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)
+}
+
// === COMMON LIBRARY ===
function roll_die() {
@@ -796,6 +1658,30 @@ function gen_action(action, argument) {
set_add(view.actions[action], argument)
}
+function gen_action_general(ix) {
+ gen_action("general", ix)
+}
+
+function gen_action_governor(ix) {
+ gen_action("governor", ix)
+}
+
+function gen_action_legion(ix) {
+ gen_action("legion", ix)
+}
+
+function gen_action_barbarian(ix) {
+ gen_action("barbarian", ix)
+}
+
+function gen_action_region(where) {
+ gen_action("region", where)
+}
+
+function gen_action_card(c) {
+ gen_action("card", c)
+}
+
function clear_undo() {
if (game.undo.length > 0)
game.undo = []