"use strict" // TODO: barbarian leaders -> barbarian list /* TODO ---- game.battle -> game.battle / game.count + game.where -- castra/quaestor - directly or extra select target step? [x] crisis ira deorum [x] crisis barbarians [ ] crisis pax deorum [x] tribute [x] foederati [x] mobs [x] disperse mob [ ] combat victory [ ] combat remove general if army is eliminated [ ] combat battle screen - splay stack / dialog for watching contents and taking hits [ ] combat retreat out of capital [ ] combat retreat barbarians to homeland [ ] combat advance into capital [ ] combat flanking maneuver [ ] combat - select both inactive/active barbarian for hits [x] quaestor [x] place [x] remove at start [x] remove when replaced [ ] castra [x] place [x] remove at start [x] remove when move/attack [ ] move militia to general at end of turn [ ] remove when retreat [x] support check [ ] gain legacy [ ] emperor [ ] provinces [ ] improvements [x] end turn [x] grow mobs [x] flip barbarians [x] praetorian guard [ ] damnatio memoriae [ ] pretender [ ] place [ ] expand [ ] scoring effects [ ] occupation effects [ ] game end [ ] rival emperors [ ] emperor turns [ ] combat [ ] support check [ ] combat bonus [ ] buy/trash bonus [ ] barbarian leaders [ ] invasion [ ] combat bonus [ ] combat effect [ ] buy/trash bonus [ ] other events */ 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 = { [P1]: 0, [P2]: 1, [P3]: 2, [P4]: 3, "Solo": 4, "Observer": -1, } const MILITARY = 0 const SENATE = 1 const POPULACE = 2 const LEGION_COUNT = 33 // REGIONS const ITALIA = 0 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 const NOMADS_HOMELAND = 15 const SASSANIDS_HOMELAND = 16 const MARE_OCCIDENTALE = 17 const MARE_ORIENTALE = 18 const OCEANUS_ATLANTICUS = 19 const PONTUS_EUXINUS = 20 const AVAILABLE = 21 const UNAVAILABLE = 22 const ARMY = 23 const REGION_NAME = [ "Italia", "Asia", "Gallia", "Macedonia", "Pannonia", "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 BARBARIAN_COUNT = [ 0, 50, 30, 40, 50 ] const first_barbarian = [ 0, 10, 20, 30, 40 ] const last_barbarian = [ 9, 19, 29, 39, 49 ] const GENERAL_NAME = [ "Red 0", "Red 1", "Red 2", "Red 3", "Red 4", "Red 5", "Blue 0", "Blue 1", "Blue 2", "Blue 3", "Blue 4", "Blue 5", "Yellow 0", "Yellow 1", "Yellow 2", "Yellow 3", "Yellow 4", "Yellow 5", "Green 0", "Green 1", "Green 2", "Green 3", "Green 4", "Green 5", ] const BARBARIAN_NAME = [ "Alamanni", "Franks", "Goths", "Nomads", "Sassanids", ] const BARBARIAN_HOMELAND = [ ALAMANNI_HOMELAND, FRANKS_HOMELAND, GOTHS_HOMELAND, NOMADS_HOMELAND, SASSANIDS_HOMELAND, ] const BARBARIAN_INVASION = [ // Alamanni [ [ 1, 3, [ PANNONIA, ITALIA ] ], [ 4, 6, [ THRACIA, MACEDONIA ] ], ], // Franks [ [ 1, 2, [ BRITANNIA, GALLIA ] ], [ 3, 4, [ GALLIA, HISPANIA ] ], [ 5, 6, [ PANNONIA, ITALIA ] ], ], // Goths [ [ 1, 2, [ THRACIA, MACEDONIA ] ], [ 3, 4, [ ASIA, MACEDONIA ] ], [ 5, 6, [ GALATIA, SYRIA ] ], ], // Nomads [ [ 1, 3, [ AFRICA, HISPANIA ] ], [ 4, 6, [ AEGYPTUS, SYRIA ] ], ], // Sassanids [ [ 1, 3, [ GALATIA, ASIA ] ], [ 4, 6, [ SYRIA, AEGYPTUS ] ], ], ] const CRISIS_TABLE_4P = [ 0, SASSANIDS, FRANKS, SASSANIDS, GOTHS, 0, ALAMANNI, NOMADS, FRANKS, NOMADS, 0, ] const CRISIS_TABLE_3P = [ 0, FRANKS, SASSANIDS, SASSANIDS, FRANKS, 0, ALAMANNI, GOTHS, GOTHS, ALAMANNI, 0, ] const CRISIS_TABLE_2P = [ 0, FRANKS, ALAMANNI, FRANKS, GOTHS, 0, GOTHS, FRANKS, ALAMANNI, ALAMANNI, 0, ] // CARDS 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 // 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 ] 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_cost(c) { if (c >= CARD_M1[0] && c <= CARD_M1[1]) return 1 if (c >= CARD_M2[0] && c <= CARD_M2[1]) return 2 if (c >= CARD_M3[0] && c <= CARD_M3[1]) return 3 if (c >= CARD_M4[0] && c <= CARD_M4[1]) return 4 if (c >= CARD_S1[0] && c <= CARD_S1[1]) return 1 if (c >= CARD_S2[0] && c <= CARD_S2[1]) return 2 if (c >= CARD_S3[0] && c <= CARD_S3[1]) return 3 if (c >= CARD_S4[0] && c <= CARD_S4[1]) return 4 if (c >= CARD_P1[0] && c <= CARD_P1[1]) return 1 if (c >= CARD_P2[0] && c <= CARD_P2[1]) return 2 if (c >= CARD_P3[0] && c <= CARD_P3[1]) return 3 if (c >= CARD_P4[0] && c <= CARD_P4[1]) return 4 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 can_play_card_event(c) { if (c >= CARD_M1[0] && c <= CARD_M1[1]) return false if (c >= CARD_M2[0] && c <= CARD_M2[1]) return can_play_castra() if (c >= CARD_M3[0] && c <= CARD_M3[1]) return false // "Flanking Maneuver" if (c >= CARD_M4[0] && c <= CARD_M4[1]) return can_play_praetorian_guard() if (c >= CARD_S1[0] && c <= CARD_S1[1]) return false if (c >= CARD_S2[0] && c <= CARD_S2[1]) return can_play_tribute() if (c >= CARD_S3[0] && c <= CARD_S3[1]) return can_play_foederati() if (c >= CARD_S4[0] && c <= CARD_S4[1]) return false // "Damnatio Memoriae" if (c >= CARD_P1[0] && c <= CARD_P1[1]) return false if (c >= CARD_P2[0] && c <= CARD_P2[1]) return can_play_quaestor() if (c >= CARD_P3[0] && c <= CARD_P3[1]) return can_play_mob() if (c >= CARD_P4[0] && c <= CARD_P4[1]) return false // "Pretender" return false } function play_card_event(c) { if (c >= CARD_M2[0] && c <= CARD_M2[1]) play_castra() if (c >= CARD_M3[0] && c <= CARD_M3[1]) play_flanking_maneuver() if (c >= CARD_M4[0] && c <= CARD_M4[1]) play_praetorian_guard() if (c >= CARD_S2[0] && c <= CARD_S2[1]) play_tribute() if (c >= CARD_S3[0] && c <= CARD_S3[1]) play_foederati() if (c >= CARD_S4[0] && c <= CARD_S4[1]) play_damnatio_memoriae() if (c >= CARD_P2[0] && c <= CARD_P2[1]) play_quaestor() if (c >= CARD_P3[0] && c <= CARD_P3[1]) play_mob() if (c >= CARD_P4[0] && c <= CARD_P4[1]) play_pretender() } 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 is_sea(where) { return where >= MARE_OCCIDENTALE && where <= PONTUS_EUXINUS } function current_hand() { return game.hand[game.current] } function current_draw() { return game.draw[game.current] } function current_discard() { return game.discard[game.current] } function get_barbarian_tribe(id) { for (let tribe = 0; tribe < 5; ++tribe) if (id >= first_barbarian[tribe] && id <= last_barbarian[tribe]) return tribe return -1 } // === BOARD STATE === function is_no_place_governor(where) { return where >= game.support.length } 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 } 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 } 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 } 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) } 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) } 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) } 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) } function has_mob(province) { return game.mobs[province] > 0 } function count_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 } 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) } 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) } 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) } // === 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 clear_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) } function clear_militia_battled(province) { game.mbattled &= ~(1 << province) } // === COMPOUND STATE === function get_selected_region() { if (game.selected_governor >= 0) return get_governor_location(game.selected_governor) if (game.selected_general >= 0) return get_general_location(game.selected_general) return UNAVAILABLE } 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 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 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)) } 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 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 } 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 } function for_each_barbarian(f) { let n = game.barbarians.length for (let id = 0; id < n; ++id) f(id, get_barbarian_location(id), is_barbarian_active(id)) } function find_barbarian(f) { let n = game.barbarians.length for (let id = 0; id < n; ++id) if (f(id, get_barbarian_location(id), is_barbarian_active(id))) return id return -1 } function some_governor(f) { return find_governor(f) >= 0 } function some_general(f) { return find_general(f) >= 0 } function some_barbarian(f) { return find_barbarian(f) >= 0 } function next_player() { return (game.current + 1) % game.legacy.length } function find_unused_legion() { for (let ix = 0; ix < LEGION_COUNT; ++ix) if (get_legion_location(ix) === AVAILABLE) return ix return -1 } 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_general_in_capital(province)) return false return true } function update_neutral_italia() { 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 } function get_tribe_count() { return game.legacy.length + 1 } 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_own_province(where)) return false } return true } function is_own_general(id) { let a = game.current * 6 return id >= a && id < a + 6 } function is_enemy_general(id) { return id >= 0 && !is_own_general(id) } function is_own_governor(id) { let a = game.current * 6 return id >= a && id < a + 6 } function is_enemy_governor(id) { return id >= 0 && !is_own_governor(id) } function is_capital_free_of_enemy(where) { return !is_enemy_general(get_capital_general(where)) } function get_province_governor(where) { return find_governor((id, loc) => loc === where) } function get_capital_general(where) { return find_general((id, loc, cap) => loc === where && cap) } function is_neutral_province(where) { if (is_no_place_governor(where)) return false return get_province_governor(where) < 0 } function has_active_barbarians(where) { if (has_barbarian_leader(where)) return true return some_barbarian((id, loc, active) => loc === where && active) } function has_inactive_barbarians(where) { return some_barbarian((id, loc, active) => loc === where && !active) } function find_barbarian_of_tribe(where, tribe) { for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === where) return id return -1 } function find_active_barbarian_of_tribe(where, tribe) { for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === where && is_barbarian_active(id)) return id return -1 } function find_inactive_barbarian_of_tribe(where, tribe) { for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === where && is_barbarian_inactive(id)) return id return -1 } function has_barbarian_leader(where) { for (let i = 0; i < 3; ++i) if (game.barbarian_leaders[i] === where) return true return false } function has_rival_emperor(where) { for (let i = 0; i < 3; ++i) if (game.rival_emperors[i] === where) return true return false } function is_enemy_province(where) { return is_enemy_governor(get_province_governor(where)) } function is_own_province(where) { return is_own_governor(get_province_governor(where)) } function is_emperor(governor) { let emperor = get_province_governor(ITALIA) if (emperor >= 0 && governor >= 0) return (emperor / 6 | 0) === (governor / 6 | 0) return false } function has_enemy_general_in_capital(where) { return is_enemy_general(get_capital_general(where)) } function has_enemy_army_in_province(where) { if (some_general((id, loc) => loc === where && is_enemy_general(id))) return true if (is_province(where) && some_barbarian((id, loc) => loc === where)) return true if (!is_province(where) && some_barbarian((id, loc, active) => loc === where && active)) return true return false } function spend_ip(type, n) { game.ip[type] -= n } function can_place_governor(where) { if (is_no_place_governor(where)) return false if (is_own_province(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 find_active_barbarian_at_home(tribe) { let home = BARBARIAN_HOMELAND[tribe] for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === home && is_barbarian_active(id)) return id return -1 } function count_active_barbarians_at_home(tribe) { let home = BARBARIAN_HOMELAND[tribe] let n = 0 for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === home && is_barbarian_active(id)) n += 1 return n } function find_inactive_barbarian_at_home(tribe) { let home = BARBARIAN_HOMELAND[tribe] for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === home && is_barbarian_inactive(id)) return id return -1 } function count_barbarians_in_province(tribe, where) { let n = 0 for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === where) n += 1 return n } function count_legions_in_army(army_id) { let n = 0 for (let id = 0; id < LEGION_COUNT; ++id) if (get_legion_location(id) === ARMY + army_id) ++n return n } function count_barbarians_in_army(army_id) { let n = 0 for (let id = 0; id < game.barbarians.length; ++id) if (get_barbarian_location(id) === ARMY + army_id) ++n return n } 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 has_reduced_legions_in_army(id) { return find_reduced_legion_in_army(id) >= 0 } function has_lone_militia(where) { return has_militia(where) && get_capital_general(where) < 0 } 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 count_own_provinces() { let n = 0 for (let where = 0; where < 12; ++where) if (is_own_province(where)) ++n return n } function count_own_basilicas() { let n = 0 for (let where = 0; where < 12; ++where) if (is_own_province(where) && has_basilica(where)) ++n return n } function roll_dice(count, target) { let hits = 0 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 } function eliminate_barbarian(id) { let tribe = get_barbarian_tribe(id) set_barbarian_location(id, BARBARIAN_HOMELAND[tribe]) set_barbarian_inactive(id) } function flip_discard_to_available() { game.draw[game.current] = game.discard[game.current] game.discard[game.current] = [] } function assign_hit_to_legion(id) { let army = get_legion_location(id) - ARMY if (is_legion_reduced(id)) { set_legion_location(id, AVAILABLE) if (count_legions_in_army(army) === 0) { set_general_location(army, AVAILABLE) clear_general_battled(army) } } else { set_legion_reduced(id) } } // === SETUP === states.setup_province = { prompt() { view.prompt = "Select a starting Province." for (let where = 2; where <= 12; ++where) if (is_neutral_province(where) && !is_no_place_governor(where)) gen_action("capital", where) }, capital(where) { push_undo() 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" }, } states.setup_hand = { prompt() { view.prompt = "Draw your initial hand." let hand = current_hand() if (hand.length < 5) { for (let c of current_draw()) gen_action_card(c) } else { view.actions.done = 1 } }, card(c) { push_undo() set_delete(current_draw(), c) set_add(current_hand(), c) }, done() { clear_undo() game.state = "setup_province" game.current = next_player() if (game.current === game.first) goto_start_turn() }, } // === UPKEEP === function goto_start_turn() { log_h2(PLAYER_NAMES[game.current]) game.battled = 0 game.mbattled = 0 game.placed = 0 goto_upkeep() } function goto_upkeep() { // TODO: manually remove Quaestor and Castra? for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i let where = get_governor_location(id) if (is_province(where) && has_quaestor(where)) { log("Removed Quaestor from S" + where) remove_quaestor(where) } if (has_militia_castra(where)) { log("Removed Castra from S" + where) remove_militia_castra(where) } if (has_general_castra(id)) { log("Removed Castra from S" + get_general_location(id)) remove_general_castra(id) } } goto_crisis() } // === CRISIS === // TODO: manual barbarian invasions! function goto_crisis() { game.dice[0] = roll_die() game.dice[1] = roll_die() log(`Crisis B${game.dice[0]} W${game.dice[1]}`) let sum = game.dice[0] + game.dice[1] if (sum === 2) return goto_ira_deorum() if (sum === 12) return goto_pax_deorum() if (sum === 7) return goto_event() if (game.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() { logi("Ira Deorum") game.count = 0 let tribe_count = get_tribe_count() for (let tribe = 0; tribe < tribe_count; ++tribe) if (find_inactive_barbarian_at_home(tribe) >= 0) game.count |= (1 << tribe) game.state = "ira_deorum" if (game.count === 0) goto_take_actions() } states.ira_deorum = { prompt() { prompt("Ira Deorum: Activate one Barbarian in each tribe's homeland.") let tribe_count = get_tribe_count() for (let tribe = 0; tribe < tribe_count; ++tribe) if (game.count & (1 << tribe)) gen_action_barbarian(find_inactive_barbarian_at_home(tribe)) }, barbarian(id) { let tribe = get_barbarian_tribe(id) game.count &= ~(1 << tribe) log("Activated " + BARBARIAN_NAME[tribe]) set_barbarian_active(id) if (game.count === 0) goto_take_actions() }, } function goto_pax_deorum() { logi("Pax Deorum") logi("TODO") goto_take_actions() } function goto_event() { logi("Event") logi("TODO") goto_take_actions() } function goto_barbarian_crisis(tribe) { logi(BARBARIAN_NAME[tribe]) game.crisis = tribe if (find_inactive_barbarian_at_home(tribe) >= 0) game.state = "barbarian_crisis" else roll_barbarian_crisis() } states.barbarian_crisis = { prompt() { let tribe = game.crisis prompt(BARBARIAN_NAME[tribe] + " Crisis!") gen_action_barbarian(find_inactive_barbarian_at_home(tribe)) }, barbarian(id) { let tribe = game.crisis log("Activated " + BARBARIAN_NAME[tribe]) set_barbarian_active(id) roll_barbarian_crisis() }, } function roll_barbarian_crisis() { let tribe = game.crisis let black = game.dice[2] = roll_die() let white = game.dice[3] = roll_die() logi(`B${black} W${white}`) if (black <= count_active_barbarians_at_home(tribe)) goto_barbarian_invasion() else goto_take_actions() } function goto_barbarian_invasion(tribe, black, white) { logi("Invasion!") game.count = game.dice[2] game.state = "barbarian_invasion" } states.barbarian_invasion = { prompt() { let tribe = game.crisis prompt(BARBARIAN_NAME[tribe] + " Invasion!") gen_action_barbarian(find_active_barbarian_at_home(tribe)) }, barbarian(id) { if (invade_with_barbarian(id) || --game.count === 0) goto_take_actions() }, } function invade_with_barbarian(id) { let tribe = game.crisis let white = game.dice[3] let path = null for (let list of BARBARIAN_INVASION[tribe]) if (white >= list[0] && white <= list[1]) path = list[2] for (let i = 0; i < path.length; ++i) { let n = count_barbarians_in_province(tribe, path[i]) if (n < 3) { invade_with_barbarian_counter(id, path, path[i]) return false } } return true } function invade_with_barbarian_counter(id, path, where) { set_barbarian_location(id, where) for (let loc of path) { if (has_limes(loc)) set_barbarian_inactive(id) if (loc === where) break } } // === TAKE ACTIONS === 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) } states.take_actions = { prompt() { let [ mip, sip, pip ] = game.ip prompt(`Take Actions: ${mip} Military, ${sip} Senate, ${pip} Populace.`) let where = UNAVAILABLE view.actions.end_actions = 1 // Play cards for IP for (let c of current_hand()) gen_action_card(c) // Use events on played cards for (let c of game.played) if (!set_has(game.used, c) && can_play_card_event(c)) gen_action_card(c) if (game.selected_governor >= 0) { view.color = SENATE view.selected_governor = game.selected_governor where = get_governor_location(game.selected_governor) } if (game.selected_general >= 0) { view.color = MILITARY view.selected_general = game.selected_general where = get_general_location(game.selected_general) } // Select Governor for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i if (id !== game.selected_governor) { switch (get_governor_location(id)) { case UNAVAILABLE: if (sip >= i) gen_action_governor(id) break case AVAILABLE: if (sip >= 1 || has_unused_praetorian_guard()) gen_action_governor(id) break default: gen_action_governor(id) break } } } // Select General for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i if (id !== game.selected_general) { switch (get_general_location(id)) { case UNAVAILABLE: if (mip >= i) gen_action_general(id) break case AVAILABLE: if (mip >= 1) gen_action_general(id) break default: gen_action_general(id) break } } } // Recruit Governor if (game.selected_governor >= 0 && where === UNAVAILABLE) { view.actions.recruit_governor = 0 if (sip >= game.selected_governor % 6) view.actions.recruit_governor = 1 } // Recruit General if (game.selected_general >= 0 && where === UNAVAILABLE) { view.actions.recruit_general = 0 if (mip >= game.selected_general % 6) view.actions.recruit_general = 1 } // Place Governor if (game.selected_governor >= 0 && where === AVAILABLE) { gen_place_governor() } // Create Army if (game.selected_general >= 0 && where === AVAILABLE) { if (mip >= 1 && find_unused_legion() >= 0) gen_create_army() } // Governor Actions if (game.selected_governor >= 0 && is_province(where)) { view.actions.place_militia = 0 view.actions.amphitheater = 0 view.actions.basilica = 0 view.actions.limes = 0 // Recall Governor if (sip >= 2) gen_action_recall(where) // Increase Support Level let support = game.support[where] if (where !== ITALIA && support < 4) { if (pip > support) gen_action_support(where, support + 1) } // Place Militia if (!has_militia(where) && is_capital_free_of_enemy(where)) { if (pip >= 2) view.actions.place_militia = 1 } // Hold Games if (has_mob(where)) { view.actions.disperse_mob = 0 view.actions.hold_games = 0 if (has_lone_militia(where) && mip >= 1) view.actions.disperse_mob = 1 if (pip >= 2) view.actions.hold_games = 1 } // Build an Improvement if (can_build_improvement(where)) { if (pip >= 3) { if (!has_amphitheater(where)) view.actions.amphitheater = 1 if (!has_basilica(where)) view.actions.basilica = 1 if (!has_limes(where)) view.actions.limes = 1 } } // Initiate Battle with Militia not stacked with General if (!has_militia_battled(where)) { if (has_lone_militia(where)) gen_initiate_battle(where) } } // General Actions if (game.selected_general >= 0 && is_region(where)) { view.actions.disperse_mob = 0 view.actions.train_legions = 0 view.actions.add_legion_to_army = 0 // Disperse Mob if (has_mob(where) && is_own_province(where)) { if (mip >= 1) view.actions.disperse_mob = 1 } // Train Legions if (has_reduced_legions_in_army(game.selected_general)) { if (mip >= 1) view.actions.train_legions = 1 } // Add Legion to Army if (is_own_province(where)) { let cost = count_legions_in_army(game.selected_general) + 1 if (mip >= cost) view.actions.add_legion_to_army = 1 } if (!has_general_battled(game.selected_general)) { // Move Army gen_move_army() // Initiate Battle gen_initiate_battle(where) // Free Action: Enter/Leave Capital if (is_province(where)) { if (is_general_inside_capital(game.selected_general)) { view.actions.leave = 1 } else if (can_enter_capital(where)) { view.actions.enter = 1 gen_action_capital(where) } } } } }, end_actions() { push_undo() // TODO: move militia castra to castra with stacked general game.selected_governor = -1 game.selected_general = -1 goto_support_check() }, 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)) { set_add(game.used, c) play_card_event(c) } }, governor(id) { game.selected_governor = id game.selected_general = -1 }, general(id) { if (is_own_general(id)) { game.selected_governor = -1 game.selected_general = id } else { goto_battle_vs_general(get_general_location(game.selected_general), game.selected_general, id) } }, recruit_governor() { push_undo() log("Recruited Governor " + (game.selected_governor % 6) + ".") spend_ip(SENATE, game.selected_governor % 6) set_governor_location(game.selected_governor, AVAILABLE) }, recruit_general() { push_undo() log("Recruited General " + (game.selected_general % 6) + ".") spend_ip(MILITARY, game.selected_general % 6) set_general_location(game.selected_general, AVAILABLE) }, recall() { push_undo() recall_governor() }, support() { push_undo() improve_support() }, place_militia() { push_undo() let where = get_governor_location(game.selected_governor) spend_ip(POPULACE, 2) add_militia(where) }, hold_games() { push_undo() let where = get_governor_location(game.selected_governor) log("Held Games in S" + where + ".") spend_ip(POPULACE, 3) remove_one_mob(where) }, amphitheater() { push_undo() spend_ip(POPULACE, 3) let where = get_governor_location(game.selected_governor) add_amphitheater(where) log("Built Amphitheater in S" + where + ".") }, basilica() { push_undo() spend_ip(POPULACE, 3) let where = get_governor_location(game.selected_governor) add_basilica(where) log("Built Basilica in S" + where + ".") }, limes() { push_undo() spend_ip(POPULACE, 3) let where = get_governor_location(game.selected_governor) add_limes(where) log("Built Limes in S" + where + ".") }, add_legion_to_army() { push_undo() log("Added Legion to Army.") let cost = count_legions_in_army(game.selected_general) + 1 spend_ip(MILITARY, cost) set_legion_location(find_unused_legion(), ARMY + game.selected_general) }, train_legions() { push_undo() log("Trained Legions.") spend_ip(MILITARY, 1) set_legion_full_strength(find_reduced_legion_in_army(game.selected_general)) }, enter() { push_undo() enter_capital() }, leave() { push_undo() set_general_outside_capital(game.selected_general) }, disperse_mob() { push_undo() let where = get_selected_region() let n = 0 if (has_militia(where)) n += 1 if (game.selected_general >= 0) n += count_units_in_army(game.selected_general) n = Math.min(game.mobs[where], n) log("Disperse " + n + " Mobs in S" + where) game.mobs[where] -= n reduce_support(where) }, region(where) { push_undo() if (game.selected_governor >= 0) { spend_ip(SENATE, 1) game.count = 1 game.where = where game.state = "place_governor" } if (game.selected_general >= 0) { if (get_general_location(game.selected_general) === AVAILABLE) create_army(where) else move_army_to(game.selected_general, where) } }, capital(where) { push_undo() if (get_general_location(game.selected_general) !== where) move_army_to(game.selected_general, where) enter_capital() }, barbarian(id) { push_undo() goto_battle_vs_barbarian(get_general_location(game.selected_general), game.selected_general, id) }, rival_emperor(id) { push_undo() goto_battle_vs_rival_emperor(get_general_location(game.selected_general), game.selected_general, id) }, militia(where) { push_undo() goto_battle_vs_militia(get_general_location(game.selected_general), game.selected_general) }, } // FREE ACTION: ENTER PROVINCIAL CAPITAL function enter_capital() { set_general_inside_capital(game.selected_general) // TODO: seat of power / breakaway } // ACTION: IMPROVE SUPPORT function improve_support() { let where = get_governor_location(game.selected_governor) let support = game.support[where] log("Built Support in S" + where + ".") spend_ip(POPULACE, support + 1) game.support[where] = support + 1 } // ACTION: RECALL GOVERNOR function recall_governor() { let where = get_governor_location(game.selected_governor) log("Recalled Governor from S" + where + ".") spend_ip(SENATE, 2) set_placed_governor(where) remove_governor(where) update_neutral_italia() } // ACTION: PLACE GOVERNOR function gen_place_governor() { let sip = game.ip[SENATE] if (sip >= 1) { for (let where = 0; where < 12; ++where) if (can_place_governor(where)) gen_action_region(where) } } function reduce_support(where) { if (game.support[where] === 1) remove_governor(where) else game.support[where] -= 1 } function increase_support(where) { game.support[where] += 1 } function remove_governor(where) { log("Removed Governor from S" + where) remove_all_mobs(where) remove_militia(where) clear_militia_battled(where) remove_militia_castra(where) remove_quaestor(where) let old_governor = get_province_governor(where) if (old_governor >= 0) { set_governor_location(old_governor, AVAILABLE) if (where !== ITALIA && is_emperor(old_governor)) reduce_support(ITALIA) } if (where !== ITALIA) game.support[where] = 1 update_neutral_italia() } function place_governor(where, new_governor) { remove_all_mobs(where) remove_militia(where) clear_militia_battled(where) remove_militia_castra(where) remove_quaestor(where) let old_governor = get_province_governor(where) if (old_governor >= 0) { set_governor_location(old_governor, AVAILABLE) if (where !== ITALIA && is_emperor(old_governor)) reduce_support(ITALIA) } set_governor_location(new_governor, where) if (where === ITALIA) game.support[where] = count_own_provinces() else game.support[where] = Math.max(1, game.support[where] - 1) if (where !== ITALIA && is_emperor(new_governor)) increase_support(ITALIA) update_neutral_italia() } function calc_needed_votes(where, pg) { let n = get_support(where) * 2 // base number of votes let old_governor = get_province_governor(where) let old_player = (old_governor < 0) ? -1 : (old_governor / 6 | 0) // Praetorian Guard ignores units in capital if (!pg) { 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 } return Math.max(1, n) } states.place_governor = { prompt() { let [ mip, sip, pip ] = game.ip let need = calc_needed_votes(game.where, false) let votes = game.count if (game.where === ITALIA) votes += count_own_basilicas() view.color = SENATE view.selected_region = game.where prompt(`Place Governor: ${sip} Senate. Rolling ${votes} dice. ${need} votes needed.`) if (sip >= 1) view.actions.spend_senate = 1 else view.actions.spend_senate = 0 view.actions.roll = 1 }, spend_senate() { push_undo() spend_ip(SENATE, 1) game.count += 1 }, roll() { roll_to_place_governor() }, } states.praetorian_guard = { prompt() { let [ mip, sip, pip ] = game.ip let need = calc_needed_votes(game.where, true) let votes = game.count if (game.where === ITALIA) votes += count_own_basilicas() view.color = MILITARY view.selected_region = game.where prompt(`Praetorian Guard: ${mip} Military. Rolling ${votes} dice. ${need} votes needed.`) if (mip >= 1) view.actions.spend_military = 1 else view.actions.spend_military = 0 view.actions.roll = 1 }, spend_military() { push_undo() spend_ip(MILITARY, 1) game.count += 1 }, roll() { roll_to_place_governor() }, } function roll_to_place_governor(pg) { let need = calc_needed_votes(game.where, pg) let have = 0 set_placed_governor(game.where) if (game.where === ITALIA) game.count += count_own_basilicas() if (pg) log("Praetorian Guard in S" + game.where) else log("Place Governor in S" + game.where) if (is_neutral_province(game.where)) have = roll_dice(game.count, 1) else if (!pg && has_quaestor(game.where)) have = roll_dice(game.count, 3) else have = roll_dice(game.count, 2) if (have >= need) { logi("Success!") place_governor(game.where, game.selected_governor) } else { logi("Failed!") } game.state = "take_actions" } // ACTION: PRAETORIAN GUARD function has_unused_praetorian_guard() { return game.ip[MILITARY] >= 1 && !is_own_province(ITALIA) && !has_placed_governor(ITALIA) && has_praetorian_guard_card() } function has_praetorian_guard_card() { let played = false let used = false for (let c of game.played) if (c >= CARD_M4[0] && c <= CARD_M4[1]) played = true for (let c of game.used) if (c >= CARD_M4[0] && c <= CARD_M4[1]) used = true return played && !used } function can_play_praetorian_guard() { if (game.selected_governor >= 0 && get_governor_location(game.selected_governor) === AVAILABLE) return game.ip[MILITARY] >= 1 && !is_own_province(ITALIA) && !has_placed_governor(ITALIA) return false } function play_praetorian_guard() { spend_ip(MILITARY, 1) game.count = 0 game.where = ITALIA game.state = "praetorian_guard" } // ACTION: CREATE ARMY function gen_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) } } function create_army(where) { spend_ip(MILITARY, 1) log("Created Army in S" + where + ".") set_general_location(game.selected_general, where) if (can_enter_capital(where)) set_general_inside_capital(game.selected_general) set_legion_location(find_unused_legion(), ARMY + game.selected_general) game.state = "take_actions" } // ACTION: MOVE ARMY function gen_move_army() { let mip = game.ip[MILITARY] let from = get_general_location(game.selected_general) if (mip >= 1) { for (let to of ADJACENT[from]) { if (!is_sea(to)) { gen_action_region(to) if (can_enter_capital(to)) gen_action_capital(to) } else if (mip >= 2) gen_action_region(to) } } } states.move_army_at_sea = { prompt() { let [ mip, sip, pip ] = game.ip prompt("Move Army.") view.selected_general = game.selected_general gen_move_army() }, region(to) { push_undo() move_army_to(game.selected_general, to) }, capital(to) { push_undo() move_army_to(game.selected_general, to) enter_capital() }, } function move_army_to(who, to) { log("Moved Army to S" + to + ".") remove_general_castra(who) spend_ip(MILITARY, 1) set_general_location(who, to) if (is_sea(to)) game.state = "move_army_at_sea" else game.state = "take_actions" } // CARD: CASTRA function can_play_castra() { let where = get_selected_region() if (game.selected_governor >= 0 && is_province(where)) return has_lone_militia(where) && !has_militia_castra(where) if (game.selected_general >= 0 && is_province(where)) return !has_general_castra(where) return false } function play_castra() { let where = get_selected_region() if (game.selected_governor >= 0 && is_province(where)) { log("Castra in S" + where) add_militia_castra(where) } if (game.selected_general >= 0) { log("Castra on " + GENERAL_NAME[game.selected_general]) add_general_castra(game.selected_general) } } // CARD: QUAESTOR function can_play_quaestor() { let where = get_selected_region() if (game.selected_governor >= 0 && is_province(where)) return !has_quaestor(where) return false } function play_quaestor() { let where = get_selected_region() log("Quaestor in S" + where) add_quaestor(where) } // CARD: TRIBUTE function can_play_tribute() { for (let where = 0; where < 12; ++where) if (has_active_barbarians(where)) return true return false } function play_tribute() { game.state = "tribute" } states.tribute = { prompt() { prompt("Tribute: Flip a single tribe to their inactive side in any province.") let tribe_count = get_tribe_count() for (let where = 0; where < 12; ++where) { for (let tribe = 0; tribe < tribe_count; ++tribe) { let id = find_active_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) } } }, barbarian(target) { let where = get_barbarian_location(target) let tribe = get_barbarian_tribe(target) log("Tribute " + BARBARIAN_NAME[tribe] + " in S" + where) for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === where && is_barbarian_active(id)) set_barbarian_inactive(id) game.state = "take_actions" }, } // CARD: FOEDERATI function can_foederati_from_region(where) { let tribe_count = get_tribe_count() for (let tribe = 0; tribe < tribe_count; ++tribe) { if (find_barbarian_of_tribe(where, tribe) >= 0) return true } return false } function gen_foederati(where, recruit) { let tribe_count = get_tribe_count() for (let tribe = 0; tribe < tribe_count; ++tribe) { let id = find_active_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) if (recruit || is_province(where)) { id = find_inactive_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) } } } function can_foederati_from_region_and_adjacent(from) { if (can_foederati_from_region(from)) return true for (let to of ADJACENT[from]) if (can_foederati_from_region(to)) return true return false } function can_play_foederati() { let where = get_selected_region() if (game.selected_general >= 0) if (can_foederati_from_region_and_adjacent(where)) return true if (game.selected_governor >= 0) if (has_lone_militia(where) && can_foederati_from_region_and_adjacent(where)) return true return false } function play_foederati() { game.state = "foederati" } states.foederati = { prompt() { prompt("Foederati: Remove or recruit a Barbarian.") view.selected_general = game.selected_general let from = get_general_location(game.selected_general) let recruit = (game.selected_general >= 0 && count_legions_in_army(game.selected_general) > count_barbarians_in_army(game.selected_general)) gen_foederati(from, recruit) for (let to of ADJACENT[from]) gen_foederati(to, recruit) }, barbarian(id) { let tribe = get_barbarian_tribe(id) let from = get_barbarian_location(id) log("Foederati " + BARBARIAN_NAME[tribe] + " from S" + from) if (game.selected_general >= 0 && count_legions_in_army(game.selected_general) > count_barbarians_in_army(game.selected_general)) set_barbarian_location(id, ARMY + game.selected_general) else eliminate_barbarian(id) game.state = "take_actions" }, } // CARD: MOB function can_play_mob() { for (let where = 0; where < 12; ++where) if (!has_mob(where) && is_enemy_province(where)) return true return false } function play_mob() { game.state = "mob" } states.mob = { prompt() { prompt("Mob: Place a Mob in a province.") view.color = POPULACE for (let where = 0; where < 12; ++where) if (!has_mob(where) && is_enemy_province(where)) gen_action_region(where) }, region(where) { log("Mob in S" + where) add_one_mob(where) game.state = "take_actions" }, } // === COMBAT === function goto_battle_vs_general(where, attacker, target) { log("Initiate Battle vs " + GENERAL_NAME[target] + " in S" + where) goto_battle("general", where, attacker, target) } function goto_battle_vs_barbarian(where, attacker, target) { log("Initiate Battle vs " + BARBARIAN_NAME[target] + " in S" + where) goto_battle("barbarians", where, attacker, get_barbarian_tribe(target)) } function goto_battle_vs_rival_emperor(where, attacker, target) { log("Initiate Battle vs Rival Emperor in S" + where) goto_battle("rival_emperor", where, attacker, target) } function goto_battle_vs_militia(where, attacker) { log("Initiate Battle vs Militia in S" + where) goto_battle("militia", where, attacker, -1) } function goto_battle(type, where, attacker, target) { spend_ip(MILITARY, 1) game.where = where game.battle = { type, attacker, target } game.state = "battle" if (attacker >= 0) { remove_general_castra(attacker) set_general_battled(attacker) } else { set_militia_battled(where) } } function gen_initiate_battle(where) { if (game.ip[MILITARY] >= 1) { for_each_general((id, loc) => { if (loc === where && is_enemy_general(id)) gen_action_general(id) }) let tribe_count = get_tribe_count() for (let tribe = 0; tribe < tribe_count; ++tribe) { let id = find_active_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) else if (is_province(where)) { id = find_inactive_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) } } if (is_enemy_province(where)) { if (has_lone_militia(where)) gen_action_militia(where) } } } states.battle = { prompt() { prompt("Battle!") // TODO: play "Flanking Maneuver" view.actions.roll = 1 }, roll() { // clear_undo() game.battle.dhits = roll_attacker_dice() game.battle.dtaken = 0 if (game.battle.type === "militia" && has_militia_castra(game.where)) { log("Castra reduces 1 hit") if (game.battle.dhits > 0) game.battle.dhits -= 1 } if (game.battle.type === "general" && has_general_castra(game.battle.target)) { log("Castra reduces 1 hit") if (game.battle.dhits > 0) game.battle.dhits -= 1 } game.battle.ahits = roll_defender_dice() game.battle.ataken = 0 goto_assign_hits() }, } function roll_general_dice(general) { let army = ARMY + general let n = 0 log(GENERAL_NAME[general]) if (is_general_inside_capital(general) && has_militia(game.where)) { log("Militia") n += roll_dice(1, 5) } let full_strength = 0 let reduced = 0 for (let id = 0; id < 33; ++id) { if (get_legion_location(id) === army) { if (is_legion_reduced(id)) reduced += 1 else full_strength += 1 } } if (full_strength > 0) { log("Legions") n += roll_dice(full_strength, 3) } if (reduced > 0) { log("Reduced Legions") n += roll_dice(reduced, 5) } let barbarians = 0 for (let id = 0; id < game.barbarians.length; ++id) if (get_barbarian_location(id) === army) barbarians += 1 if (barbarians > 0) { log("Barbarians") n += roll_dice(barbarians, 4) } return n } function roll_militia_dice() { log("Militia") return roll_dice(1, 5) } function roll_rival_emperor_dice(rival_emperor) { log("Rival Emperor") return roll_dice(3, 4) } function roll_barbarian_dice(tribe) { log(BARBARIAN_NAME[tribe]) let prov = is_province(game.where) let n = 0 for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === game.where) if (prov || is_barbarian_active(id)) n += 1 return roll_dice(n, 4) } function roll_attacker_dice() { log_h3("ATTACKER") if (game.battle.attacker < 0) return roll_militia_dice() else return roll_general_dice(game.battle.attacker) } function roll_defender_dice() { log_h3("DEFENDER") switch (game.battle.type) { case "militia": return roll_militia_dice() case "rival_emperor": return roll_rival_emperor_dice(game.battle.target) case "barbarians": return roll_barbarian_dice(game.battle.target) case "general": return roll_general_dice(game.battle.target) } return 0 } // COMBAT: ASSIGN HITS function has_hits_militia() { return has_militia(game.where) } function gen_hits_militia() { if (has_militia(game.where)) gen_action_militia(game.where) } function has_hits_barbarians(tribe) { let prov = is_province(game.where) for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === game.where) if (prov || is_barbarian_active(id)) return true return false } function gen_hits_barbarians(tribe) { let prov = is_province(game.where) for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === game.where) if (prov || is_barbarian_active(id)) gen_action_barbarian(id) } function has_hits_general(general) { let army = ARMY + general if (is_general_inside_capital(general) && has_militia(game.where)) return true for (let id = 0; id < game.barbarians.length; ++id) if (get_barbarian_location(id) === army) return true for (let id = 0; id < 33; ++id) if (get_legion_location(id) === army) return true return false } function gen_hits_general(general) { let army = ARMY + general let done = false if (is_general_inside_capital(general) && has_militia(game.where)) { gen_action_militia(game.where) done = true } if (!done) { for (let id = 0; id < game.barbarians.length; ++id) { if (get_barbarian_location(id) === army) { gen_action_barbarian(id) done = true } } } // NOTE: reduce all legions before eliminating any if (!done) { for (let id = 0; id < 33; ++id) { if (get_legion_location(id) === army && !is_legion_reduced(id)) { gen_action_legion(id) done = true } } } if (!done) { for (let id = 0; id < 33; ++id) { if (get_legion_location(id) === army && is_legion_reduced(id)) { gen_action_legion(id) done = true } } } } function has_hits_on_attacker() { if (game.battle.ataken < game.battle.ahits) { if (game.battle.attacker < 0) return has_hits_militia() else return has_hits_general(game.battle.attacker) } return false } function has_hits_on_defender() { if (game.battle.dtaken < game.battle.dhits) { switch (game.battle.type) { case "militia": return has_hits_militia() break case "rival_emperor": return has_hits_rival_emperor(game.battle.target) break case "barbarians": return has_hits_barbarians(game.battle.target) break case "general": return has_hits_general(game.battle.target) break } } return false } function goto_assign_hits() { // TODO: flanking maneuver goto_assign_hits_on_attacker() } function goto_assign_hits_on_attacker() { if (has_hits_on_attacker()) game.state = "assign_hits_on_attacker" else goto_assign_hits_on_defender() } function goto_assign_hits_on_defender() { if (has_hits_on_defender()) game.state = "assign_hits_on_defender" else goto_combat_victory() } states.assign_hits_on_attacker = { prompt() { prompt("Assign " + (game.battle.ahits - game.battle.ataken) + " hits to attacker!") if (game.battle.attacker < 0) gen_hits_militia() else gen_hits_general(game.battle.attacker) }, militia(where) { game.battle.ataken += 1 remove_militia(where) clear_militia_battled(where) goto_assign_hits_on_attacker() }, legion(id) { game.battle.ataken += 1 assign_hit_to_legion(id) goto_assign_hits_on_attacker() }, barbarian(id) { game.battle.ataken += 1 eliminate_barbarian(id) goto_assign_hits_on_attacker() }, } states.assign_hits_on_defender = { prompt() { prompt("Assign " + (game.battle.dhits - game.battle.dtaken) + " hits to defender!") switch (game.battle.type) { case "militia": gen_hits_militia() break case "rival_emperor": gen_hits_rival_emperor(game.battle.target) break case "barbarians": gen_hits_barbarians(game.battle.target) break case "general": gen_hits_general(game.battle.target) break } }, militia(where) { game.battle.dtaken += 1 remove_militia(where) clear_militia_battled(where) goto_assign_hits_on_defender() }, legion(id) { game.battle.dtaken += 1 assign_hit_to_legion(id) goto_assign_hits_on_defender() }, barbarian(id) { game.battle.dtaken += 1 eliminate_barbarian(id) goto_assign_hits_on_defender() }, } function is_attacker_eliminated() { if (game.selected_general < 0) return !has_militia(game.where) else return get_general_location(game.selected_general) === AVAILABLE } function is_defender_eliminated() { switch (game.battle.type) { case "militia": return !has_militia(game.where) case "rival_emperor": return get_rival_emperor_location(game.battle.target) === AVAILABLE case "barbarians": if (is_province(game.where)) return find_barbarian_of_tribe(game.where, game.battle.target) < 0 else return find_active_barbarian_of_tribe(game.where, game.battle.target) < 0 case "general": return get_general_location(game.battle.target) === AVAILABLE } return false } function goto_combat_victory() { let de = is_defender_eliminated() let ae = is_attacker_eliminated() console.log("goto_combat_victory", de, ae) if (de && ae) end_battle() else if (de || game.battle.dtaken > game.battle.ataken) goto_combat_victory_attacker() else goto_combat_victory_defender() } function award_legacy(p, n) { log(PLAYER_NAMES[p] + " gained " + n + " VP.") game.legacy[p] += n } function goto_combat_victory_defender() { if (game.battle.type === "general") award_legacy(game.battle.target / 6 | 0, 2) if (game.battle.type === "militia") award_legacy(get_province_governor(game.where) / 6 | 0, 2) end_battle() } function goto_combat_victory_attacker() { award_legacy(game.current, 2) if (game.battle.type === "barbarians") { award_legacy(game.current, game.battle.dtaken) // Surviving Barbarians go home (to active) let tribe = get_barbarian_tribe(game.battle.target) for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === game.battle) set_barbarian_location(id, BARBARIAN_HOMELAND[tribe]) } // Defending Romans must retreat into province if (game.battle.type === "general") { set_general_outside_capital(game.battle.target) remove_general_castra(game.battle.target) } // TODO: barbarian leader / rival emperor bonus if (can_enter_capital(game.where)) game.state = "advance_after_combat" else end_battle() } states.advance_after_combat = { prompt() { prompt("Battle: You may advance into provincial capital.") gen_action_capital(game.where) view.actions.pass = 1 }, capital(where) { push_undo() end_battle() enter_capital() }, pass() { push_undo() end_battle() }, } function end_battle() { game.battle = null game.state = "take_actions" } // === SUPPORT CHECK === function goto_support_check() { game.count = 0 resume_support_check() } function is_any_rival_emperor_or_pretender() { for (let i = 0; i < 3; ++i) if (game.rival_emperors[i] !== UNAVAILABLE) return true for (let where = 0; where < 12; ++where) if (is_seat_of_power(where) && is_enemy_province(where)) return true return false } function resume_support_check() { for (let where = 0; where < 12; ++where) { if ((game.count & (1 << where)) === 0) { if (is_own_province(where)) { if (has_active_barbarians(where) || has_rival_emperor(where) || has_enemy_general_in_capital(where)) { game.state = "support_check" return } } } } goto_support_check_emperor() } states.support_check = { prompt() { prompt("Support Check: Reduce support where active Barbarians, Rival Emperors or opponent armies in capital.") view.color = POPULACE for (let where = 0; where < 12; ++where) if ((game.count & (1 << where)) === 0) if (is_own_province(where)) if (has_active_barbarians(where) || has_rival_emperor(where) || has_enemy_general_in_capital(where)) gen_action_region(where) }, region(where) { push_undo() game.count |= (1 << where) if (game.support[where] > 1) game.support[where] -= 1 else remove_governor(where) resume_support_check() }, } function goto_support_check_emperor() { if (is_own_province(ITALIA) && is_any_rival_emperor_or_pretender()) { game.state = "support_check_emperor" return } goto_support_check_mobs() } states.support_check_emperor = { prompt() { prompt("Support Check: Reduce support in Italia for Rival Emperor and/or Pretender.") view.color = POPULACE gen_action_region(where) }, region(where) { push_undo() game.count |= (1 << where) if (game.support[where] > 1) game.support[where] -= 1 else remove_governor(where) goto_support_check_mobs() }, } function goto_support_check_mobs() { for (let where = 0; where < 12; ++where) { if (is_own_province(where) && game.mobs[where] >= game.support[where]) { game.state = "support_check_mobs" return } } goto_expand_pretender_empire() } states.support_check_mobs = { prompt() { prompt("Support Check: Remove Governors where number of Mobs exceed support.") view.color = POPULACE for (let where = 0; where < 12; ++where) if (is_own_province(where) && game.mobs[where] >= game.support[where]) gen_action_region(where) }, region(where) { push_undo() remove_governor(where) goto_support_check_mobs() }, } // === 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 count_political_points() { let pp = 0 for (let where = 0; where < 12; ++where) { if (is_own_province(where)) { pp += game.support[where] pp -= game.mobs[where] } } return pp } function goto_buy_trash_cards() { let discard = current_discard() for (let c of game.played) set_add(discard, c) game.played.length = 0 game.battle = { count: 0, pp: count_political_points(), } if (current_hand().length > 0) game.state = "buy_trash_discard" else game.state = "buy_trash" } states.buy_trash_discard = { prompt() { prompt("You may discard any number of cards.") for (let c of current_hand()) gen_action_card(c) view.actions.done = 1 }, card(c) { push_undo() set_delete(current_hand(), c) set_add(current_discard(), c) }, done() { push_undo() game.state = "buy_trash" }, } function find_market_with_card(c) { for (let m of game.market) if (set_has(m, c)) return m return null } states.buy_trash = { prompt() { prompt("Buy/Trash cards: " + game.battle.pp + "PP left.") let nprov = count_own_provinces() if (game.battle.pp >= 3) { for (let c of current_discard()) gen_action_card(c) } for (let m of game.market) { if (m.length > 0) { let c = m[0] let cost = card_cost(c) if (cost > nprov) cost *= 2 cost += game.battle.count if (game.battle.pp >= cost) gen_action_card(c) } } view.actions.done = 1 }, card(c) { push_undo() if (set_has(current_discard(), c)) { log("Trashed " + card_name(c)) set_delete(current_discard(), c) game.battle.pp -= 3 } else { log("Bought " + card_name(c)) set_add(current_discard(), c) set_delete(find_market_with_card(c), c) let cost = card_cost(c) if (cost > count_own_provinces()) cost *= 2 cost += game.battle.count game.battle.pp -= cost game.battle.count += 1 } }, done() { push_undo() goto_end_of_turn() }, } // === END OF TURN === function goto_end_of_turn() { game.count = 0 goto_grow_mobs() } function goto_grow_mobs() { for (let where = 0; where < 12; ++where) { if ((game.count & (1 << where)) === 0) { if (is_own_province(where) && has_mob(where) && !has_amphitheater(where)) { game.state = "grow_mobs" return } } } goto_flip_inactive_barbarians() } states.grow_mobs = { prompt() { prompt("Grow Mobs in each province you govern.") view.color = POPULACE for (let where = 0; where < 12; ++where) if ((game.count & (1 << where)) === 0) if (is_own_province(where) && has_mob(where) && !has_amphitheater(where)) gen_action_region(where) }, region(where) { push_undo() game.count |= (1 << where) add_one_mob(where) goto_grow_mobs() }, } function goto_flip_inactive_barbarians() { for (let where = 0; where < 12; ++where) { if (is_own_province(where) && has_inactive_barbarians(where)) { game.state = "flip_inactive_barbarians" return } } goto_refill_hand() } states.flip_inactive_barbarians = { prompt() { prompt("Flip all inactive barbarians in your provinces to their active side.") view.color = POPULACE let tribe_count = get_tribe_count() for (let where = 0; where < 12; ++where) { if (is_own_province(where)) { for (let tribe = 0; tribe < tribe_count; ++tribe) { let id = find_inactive_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) } } } }, barbarian(target) { let where = get_barbarian_location(target) let tribe = get_barbarian_tribe(target) for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (get_barbarian_location(id) === where && is_barbarian_inactive(id)) set_barbarian_active(id) goto_flip_inactive_barbarians() }, } function goto_refill_hand() { if (current_draw().length === 0) flip_discard_to_available() game.state = "refill_hand" } states.refill_hand = { prompt() { view.prompt = "Refill your hand." let hand = current_hand() let draw = current_draw() if (hand.length < 5 && draw.length > 0) { for (let c of draw) gen_action_card(c) } else { view.actions.done = 1 } }, card(c) { push_undo() let hand = current_hand() let draw = current_draw() set_delete(draw, c) set_add(hand, c) if (draw.length === 0) flip_discard_to_available() }, done() { clear_undo() end_refill_hand() } } function end_refill_hand() { 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 id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) { set_barbarian_location(id, home) set_barbarian_inactive(id) } } 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 game = { seed: seed, log: [], undo: [], active: 0, current: 0, state: "setup_province", first: 0, events: null, active_events: [], ip: [], selected_governor: -1, selected_general: -1, played: [], used: [], placed: 0, battled: 0, count: 0, where: 0, battle: null, support: new Array(player_count * 3).fill(1), mobs: new Array(player_count * 3).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(BARBARIAN_COUNT[player_count]).fill(AVAILABLE), rival_emperors: [ 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), ] */ game.market = [ setup_market_pile(CARD_M2), setup_market_pile(CARD_M3), setup_market_pile(CARD_M4), setup_market_pile(CARD_S2), setup_market_pile(CARD_S3), setup_market_pile(CARD_S4), setup_market_pile(CARD_P2), setup_market_pile(CARD_P3), 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] = [] game.draw[player].push(game.market[2].pop()) } update_neutral_italia() 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, mcastra: game.mcastra, amphitheater: game.amphitheater, basilica: game.basilica, limes: game.limes, governors: game.governors, generals: game.generals, legions: game.legions, barbarians: game.barbarians, rival_emperors: game.rival_emperors, barbarian_leaders: game.barbarian_leaders, dice: game.dice, events: game.active_events, played: game.played, used: game.used, market: game.market.map(m => m[0] | 0), 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 prompt(s) { view.prompt = s } 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 log_h3(msg) { log_br() log(".h3 " + msg) } function logi(msg) { game.log.push(">" + msg) } function logii(msg) { game.log.push(">>" + msg) } // === COMMON LIBRARY === function roll_die() { return random(6) + 1 } function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_general(id) { gen_action("general", id) } function gen_action_governor(id) { gen_action("governor", id) } function gen_action_legion(id) { gen_action("legion", id) } function gen_action_barbarian(id) { gen_action("barbarian", id) } function gen_action_militia(where) { gen_action("militia", where) } function gen_action_region(where) { gen_action("region", where) } function gen_action_capital(where) { gen_action("capital", where) } function gen_action_recall(where) { gen_action("recall", where) } function gen_action_support(where, level) { gen_action("support", where << 3 | level) } function gen_action_card(c) { gen_action("card", c) } function clear_undo() { if (game.undo.length > 0) game.undo = [] } function push_undo() { let copy = {} for (let k in game) { let v = game[k] if (k === "undo") continue else if (k === "log") v = v.length else if (typeof v === "object" && v !== null) v = object_copy(v) copy[k] = v } game.undo.push(copy) } function pop_undo() { let save_log = game.log let save_undo = game.undo game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo } function random(range) { // An MLCG using integer arithmetic with doubles. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**35 − 31 return (game.seed = game.seed * 200105 % 34359738337) % range } function random_bigint(range) { // Largest MLCG that will fit its state in a double. // Uses BigInt for arithmetic, so is an order of magnitude slower. // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf // m = 2**53 - 111 return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range } function shuffle(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } function shuffle_bigint(list) { // Fisher-Yates shuffle for (let i = list.length - 1; i > 0; --i) { let j = random_bigint(i + 1) let tmp = list[j] list[j] = list[i] list[i] = tmp } } // Fast deep copy for objects without cycles function object_copy(original) { if (Array.isArray(original)) { let n = original.length let copy = new Array(n) for (let i = 0; i < n; ++i) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } else { let copy = {} for (let i in original) { let v = original[i] if (typeof v === "object" && v !== null) copy[i] = object_copy(v) else copy[i] = v } return copy } } // Array remove and insert (faster than splice) function array_remove(array, index) { let n = array.length for (let i = index + 1; i < n; ++i) array[i - 1] = array[i] array.length = n - 1 } function array_insert(array, index, item) { for (let i = array.length; i > index; --i) array[i] = array[i - 1] array[index] = item } function array_remove_pair(array, index) { let n = array.length for (let i = index + 2; i < n; ++i) array[i - 2] = array[i] array.length = n - 2 } function array_insert_pair(array, index, key, value) { for (let i = array.length; i > index; i -= 2) { array[i] = array[i-2] array[i+1] = array[i-1] } array[index] = key array[index+1] = value } // Set as plain sorted array function set_clear(set) { set.length = 0 } function set_has(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return true } return false } function set_add(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else return } array_insert(set, a, item) } function set_delete(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } } function set_toggle(set, item) { let a = 0 let b = set.length - 1 while (a <= b) { let m = (a + b) >> 1 let x = set[m] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove(set, m) return } } array_insert(set, a, item) } // Map as plain sorted array of key/value pairs function map_clear(map) { map.length = 0 } function map_has(map, key) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return true } return false } function map_get(map, key, missing) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else return map[(m<<1)+1] } return missing } function map_set(map, key, value) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (key < x) b = m - 1 else if (key > x) a = m + 1 else { map[(m<<1)+1] = value return } } array_insert_pair(map, a<<1, key, value) } function map_delete(map, item) { let a = 0 let b = (map.length >> 1) - 1 while (a <= b) { let m = (a + b) >> 1 let x = map[m<<1] if (item < x) b = m - 1 else if (item > x) a = m + 1 else { array_remove_pair(map, m<<1) return } } }