diff options
Diffstat (limited to 'rules.js')
-rw-r--r-- | rules.js | 1722 |
1 files changed, 1304 insertions, 418 deletions
@@ -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 = [] |