"use strict" /* TODO ---- [ ] cut holes for capital in region svg [ ] combat battle screen - splay stack / dialog for watching contents and taking hits [ ] killed leader stash for buy/trash phase */ 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 == 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, "Observer": -1, } const MILITARY = 0 const SENATE = 1 const POPULACE = 2 const LEGION_COUNT = 33 const BARBARIAN_COUNT = 53 // REGIONS const ITALIA = 0 const ASIA = 1 const GALLIA = 2 const MACEDONIA = 3 const PANNONIA = 4 const THRACIA = 5 const BRITANNIA = 6 const GALATIA = 7 const SYRIA = 8 const AEGYPTUS = 9 const AFRICA = 10 const HISPANIA = 11 const ALAMANNI_HOMELAND = 12 const FRANKS_HOMELAND = 13 const GOTHS_HOMELAND = 14 const SASSANIDS_HOMELAND = 15 const NOMADS_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", "Britannia", "Galatia", "Syria", "Aegyptus", "Africa", "Hispania", "Alamanni Homeland", "Franks Homeland", "Goths Homeland", "Sassanids Homeland", "Nomads 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 ], /* BRITANNIA */ [ OCEANUS_ATLANTICUS ], /* GALATIA */ [ ASIA, SYRIA, SASSANIDS_HOMELAND, MARE_ORIENTALE, PONTUS_EUXINUS ], /* SYRIA */ [ AEGYPTUS, GALATIA, SASSANIDS_HOMELAND, MARE_ORIENTALE ], /* 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 ], /* ALAMANNI_HOMELAND */ [ PANNONIA, THRACIA, FRANKS_HOMELAND, GOTHS_HOMELAND ], /* FRANKS_HOMELAND */ [ GALLIA, PANNONIA, ALAMANNI_HOMELAND ], /* GOTHS_HOMELAND */ [ THRACIA, ALAMANNI_HOMELAND, PONTUS_EUXINUS ], /* SASSANIDS_HOMELAND */ [ GALATIA, SYRIA, PONTUS_EUXINUS ], /* NOMADS_HOMELAND */ [ AEGYPTUS, AFRICA, OCEANUS_ATLANTICUS ], /* 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 ], ] const PRETENDER_ADJACENT = [ /* ITALIA */ [ GALLIA, PANNONIA, MARE_OCCIDENTALE ], /* ASIA */ [ THRACIA, GALATIA, MARE_ORIENTALE, PONTUS_EUXINUS ], /* GALLIA */ [ ITALIA, PANNONIA, HISPANIA, BRITANNIA, 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 ], /* BRITANNIA */ [ GALLIA, OCEANUS_ATLANTICUS ], /* GALATIA */ [ ASIA, SYRIA, SASSANIDS_HOMELAND, MARE_ORIENTALE, PONTUS_EUXINUS ], /* SYRIA */ [ AEGYPTUS, GALATIA, SASSANIDS_HOMELAND, MARE_ORIENTALE ], /* 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 ], /* ALAMANNI_HOMELAND */ [ PANNONIA, THRACIA, FRANKS_HOMELAND, GOTHS_HOMELAND ], /* FRANKS_HOMELAND */ [ GALLIA, PANNONIA, ALAMANNI_HOMELAND ], /* GOTHS_HOMELAND */ [ THRACIA, ALAMANNI_HOMELAND, PONTUS_EUXINUS ], /* SASSANIDS_HOMELAND */ [ GALATIA, SYRIA, PONTUS_EUXINUS ], /* NOMADS_HOMELAND */ [ AEGYPTUS, AFRICA, OCEANUS_ATLANTICUS ], /* 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 SASSANIDS = 3 const NOMADS = 4 const first_barbarian = [ 0, 10, 20, 31, 43 ] const last_barbarian = [ 9, 19, 30, 42, 52 ] const CNIVA = first_barbarian[GOTHS] + 0 const ARDASHIR = first_barbarian[SASSANIDS] + 0 const SHAPUR = first_barbarian[SASSANIDS] + 1 const POSTUMUS = 0 const PRIEST_KING = 1 const ZENOBIA = 2 const CNIVA_BONUS = 1 << 3 const ARDASHIR_BONUS = 1 << 4 const SHAPUR_BONUS = 1 << 5 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", "Sassanids", "Nomads", ] const BARBARIAN_HOMELAND = [ ALAMANNI_HOMELAND, FRANKS_HOMELAND, GOTHS_HOMELAND, SASSANIDS_HOMELAND, NOMADS_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 ] ], ], // Sassanids [ [ 1, 3, [ GALATIA, ASIA ] ], [ 4, 6, [ SYRIA, AEGYPTUS ] ], ], // Nomads [ [ 1, 3, [ AFRICA, HISPANIA ] ], [ 4, 6, [ AEGYPTUS, SYRIA ] ], ], ] 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 const EVENT_NAME = [ "None", "Plague of Cyprian", "Ardashir", "Priest King", "Palmyra Allies", "Shapur I", "Postumus", "Ludi Saeculares", "Cniva", "Zenobia", "Bad Auguries", "Raiding Parties", "Preparing for War", "Inflation", "Good_auguries", "Diocletian", ] const CARD_M1 = [ 0, 11 ] const CARD_S1 = [ 12, 23 ] const CARD_P1 = [ 24, 35 ] const CARD_M2 = [ 36, 44 ] const CARD_S2 = [ 45, 53 ] const CARD_P2 = [ 54, 62 ] const CARD_M2X = [ 63, 71 ] const CARD_S2X = [ 72, 80 ] const CARD_P2X = [ 81, 89 ] const CARD_M3 = [ 90, 97 ] const CARD_S3 = [ 98, 105 ] const CARD_P3 = [ 106, 113 ] const CARD_M3X = [ 114, 121 ] const CARD_S3X = [ 122, 129 ] const CARD_P3X = [ 130, 137 ] const CARD_M4 = [ 138, 143 ] const CARD_S4 = [ 144, 149 ] const CARD_S4B = [ 150, 155 ] const CARD_P4 = [ 156, 161 ] const CARD_M4X = [ 162, 167 ] const CARD_S4X = [ 168, 173 ] const CARD_P4X = [ 174, 179 ] const CARD_INDEX = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, ] const CARD_INFO = [ { name: "M1", type: 0, value: 1, event: "None" }, { name: "S1", type: 1, value: 1, event: "None" }, { name: "P1", type: 2, value: 1, event: "None" }, { name: "M2", type: 0, value: 2, event: "Castra" }, { name: "S2", type: 1, value: 2, event: "Tribute" }, { name: "P2", type: 2, value: 2, event: "Quaestor" }, { name: "M2X", type: 0, value: 2, event: "Cavalry" }, { name: "S2X", type: 1, value: 2, event: "Princeps Senatus" }, { name: "P2X", type: 2, value: 2, event: "Ambitus" }, { name: "M3", type: 0, value: 3, event: "Flanking Maneuver" }, { name: "S3", type: 1, value: 3, event: "Foederati" }, { name: "P3", type: 2, value: 3, event: "Mob" }, { name: "M3X", type: 0, value: 3, event: "Force March" }, { name: "S3X", type: 1, value: 3, event: "Frumentarii" }, { name: "P3X", type: 2, value: 3, event: "Mobile Vulgus" }, { name: "M4", type: 0, value: 4, event: "Praetorian Guard" }, { name: "S4", type: 1, value: 4, event: "Damnatio Memoriae" }, { name: "S4B", type: 1, value: 4, event: "Damnatio Memoriae (exp)" }, { name: "P4", type: 2, value: 4, event: "Pretender" }, { name: "M4X", type: 0, value: 4, event: "Spiculum" }, { name: "S4X", type: 1, value: 4, event: "Triumph" }, { name: "P4X", type: 2, value: 4, event: "Demagogue" }, ] function card_name(c) { return CARD_INFO[CARD_INDEX[c]].name } function card_value(c) { return CARD_INFO[CARD_INDEX[c]].value } function card_influence(c) { return CARD_INFO[CARD_INDEX[c]].type } function card_event_name(c) { return CARD_INFO[CARD_INDEX[c]].event } const PLAY_CARD_EVENT = { "Castra": play_castra, "Tribute": play_tribute, "Quaestor": play_quaestor, "Flanking Maneuver": play_flanking_maneuver, "Foederati": play_foederati, "Mob": play_mob, "Praetorian Guard": play_praetorian_guard, "Damnatio Memoriae": play_damnatio_memoriae, "Damnatio Memoriae (exp)": play_damnatio_memoriae_exp, "Pretender": play_pretender, } const CAN_PLAY_CARD_EVENT = { "Castra": can_play_castra, "Tribute": can_play_tribute, "Quaestor": can_play_quaestor, "Flanking Maneuver": null, "Foederati": can_play_foederati, "Mob": can_play_mob, "Praetorian Guard": can_play_praetorian_guard, "Damnatio Memoriae": null, "Damnatio Memoriae (exp)": null, "Pretender": can_play_pretender, } function can_play_card_event(c) { let f = CAN_PLAY_CARD_EVENT[card_event_name(c)] return f ? f() : false } function play_card_event(c) { PLAY_CARD_EVENT[card_event_name(c)]() } function add_card_ip(c) { let value = CARD_INFO[CARD_INDEX[c]].value let type = CARD_INFO[CARD_INDEX[c]].type game.ip[type] += value } 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 } function is_barbarian_leader(id) { return id === CNIVA || id === ARDASHIR || id === SHAPUR } // === 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_rival_emperor_location(id) { return game.rival_emperors[id] } function set_rival_emperor_location(id, loc) { game.rival_emperors[id] = loc } 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 prev_player() { return (game.current + game.legacy.length - 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_province_player(where) { let gov = find_governor((id, loc) => loc === where) if (gov >= 0) return gov / 6 | 0 return -1 } 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) { 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_active_non_leader_barbarian_of_tribe(where, tribe) { for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (!is_barbarian_leader(id) && 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 find_inactive_non_leader_barbarian_of_tribe(where, tribe) { for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) if (!is_barbarian_leader(id) && get_barbarian_location(id) === where && is_barbarian_inactive(id)) return id return -1 } function has_rival_emperor(where) { for (let i = 0; i < 3; ++i) if (get_rival_emperor_location(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(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 has_available_governor() { for (let i = 0; i < 6; ++i) if (get_governor_location(game.current * 6 + i) === AVAILABLE) 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_own_legions() { let n = 0 for (let i = 0; i < 6; ++i) n += count_legions_in_army(game.current * 6 + i) 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 is_emperor_player() { return is_own_province(ITALIA) } function is_pretender_player() { for (let where = 1; where < 12; ++where) if (is_seat_of_power(where) || is_breakaway(where)) if (is_own_province(where)) return true return false } function is_only_pretender_player() { let result = false for (let where = 1; where < 12; ++where) if (is_seat_of_power(where)) if (is_own_province(where)) result = true else return false return result } function count_pretender_provinces() { let n = 0 for (let where = 1; where < 12; ++where) if (is_seat_of_power(where) || is_breakaway(where)) ++n return n } function count_own_breakaway_provinces() { let n = 0 for (let where = 0; where < 12; ++where) if (is_own_province(where) && (is_seat_of_power(where) || is_breakaway(where))) ++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_improvements() { let n = 0 for (let where = 0; where < 12; ++where) { if (is_own_province(where)) { if (has_amphitheater(where)) ++n if (has_basilica(where)) ++n if (has_limes(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) } } logi(summary.join(" ")) count = sixes } return hits } function roll_dice_no_reroll(count, target) { let hits = 0 let summary = [] for (let i = 0; i < count; ++i) { let die = roll_die() if (die >= target) { summary.push("B" + die) hits += 1 } else { summary.push("W" + die) } } logi(summary.join(" ")) return hits } function eliminate_barbarian(id) { let tribe = get_barbarian_tribe(id) if (is_barbarian_leader(id) && game.battle) { if (id === CNIVA) { log("Cniva killed!") game.battle.killed |= CNIVA_BONUS } if (id === ARDASHIR) { log("Ardashir killed!") game.battle.killed |= ARDASHIR_BONUS } if (id === SHAPUR) { log("Shapur killed!") game.battle.killed |= SHAPUR_BONUS } set_barbarian_location(id, UNAVAILABLE) } else { set_barbarian_location(id, BARBARIAN_HOMELAND[tribe]) set_barbarian_inactive(id) } } function eliminate_rival_emperor(id) { if (game.battle) game.battle.killed |= (1 << id) set_rival_emperor_location(id, UNAVAILABLE) } function eliminate_militia(where) { remove_militia(where) clear_militia_battled(where) remove_militia_castra(where) } function flip_discard_to_available(p) { game.draw[p] = game.discard[p] game.discard[p] = [] } 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) { log(GENERAL_NAME[army] + " killed!") set_general_location(army, AVAILABLE) clear_general_battled(army) } } else { set_legion_reduced(id) } } function is_pretender_province(where) { return is_breakaway(where) || is_seat_of_power(where) } function find_seat_of_power() { for (let where = 1; where < 12; ++where) if (is_seat_of_power(where) && is_own_province(where)) return where return -1 } function is_possible_seat_of_power(from) { if (is_own_province(from) && get_support(from) >= 3) for (let to of PRETENDER_ADJACENT[from]) if (is_own_province(to) && get_support(to) >= 3) return true return false } function is_expand_pretender_province(from) { if (get_support(from) >= 3 && !is_pretender_province(from) && is_own_province(from)) for (let to of PRETENDER_ADJACENT[from]) if (is_pretender_province(to) && is_own_province(to)) return true return false } function gen_card_event(event) { for (let c = event[0]; c <= event[1]; ++c) if (set_has(game.played, c) && !set_has(game.used, c)) gen_action_card(c) } function has_card_event(event) { for (let c = event[0]; c <= event[1]; ++c) if (set_has(game.played, c) && !set_has(game.used, c)) return true return false } // === SETUP === states.setup_province = { prompt() { view.prompt = "Select a starting Province." view.selected_governor = game.current * 6 view.selected_general = game.current * 6 view.color = SENATE for (let where = 1; 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.killed = 0 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 === function goto_crisis() { game.crisis = -1 game.dice[0] = roll_die() game.dice[1] = roll_die() game.dice[2] = 0 game.dice[3] = 0 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_crisis_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]) } // CRISIS: IRA DEORUM 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() }, } // CRISIS: PAX DEORUM function goto_pax_deorum() { logi("Pax Deorum") game.count = game.current for (let p = 0; p < game.legacy.length; ++p) if (game.draw[p].length === 0) flip_discard_to_available(p) game.current = prev_player() game.state = "pax_deorum" } states.pax_deorum = { prompt(player) { prompt("Pax Deorum: Draw one card.") for (let c of game.draw[player]) gen_action_card(c) }, card(c, player) { set_add(game.hand[player], c) set_delete(game.draw[player], c) if (game.current === game.count) goto_take_actions() else game.current = prev_player() }, } // CRISIS: BARBARIAN INVASION 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() if (game.active_event === EVENT_RAIDING_PARTIES) black = Math.max(1, black - 2) if (game.active_event === EVENT_PREPARING_FOR_WAR) black = Math.min(6, black + 2) 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] // Ardashir: All active Sassanids invade! if (tribe === SASSANIDS) { if (get_barbarian_location(ARDASHIR) === SASSANIDS_HOMELAND) game.count = count_active_barbarians_at_home(tribe) } 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 } } // CRISIS: EVENT function goto_crisis_event() { game.active_event = game.events.pop() log_h3(EVENT_NAME[game.active_event]) switch (game.active_event) { case EVENT_ARDASHIR: return goto_crisis_barbarian_leader(ARDASHIR, SASSANIDS_HOMELAND) case EVENT_SHAPUR_I: return goto_crisis_barbarian_leader(SHAPUR, SASSANIDS_HOMELAND) case EVENT_CNIVA: return goto_crisis_barbarian_leader(CNIVA, GOTHS_HOMELAND) case EVENT_PRIEST_KING: return goto_crisis_rival_emperor(PRIEST_KING, SYRIA) case EVENT_POSTUMUS: return goto_crisis_rival_emperor(POSTUMUS, GALLIA) case EVENT_ZENOBIA: return goto_crisis_rival_emperor(ZENOBIA, AEGYPTUS) case EVENT_PALMYRA_ALLIES: return goto_palmyra_allies() case EVENT_LUDI_SAECULARES: return goto_ludi_saeculares() case EVENT_DIOCLETIAN: return goto_game_end() } goto_take_actions() } function get_improvement_cost() { if (game.active_event === EVENT_INFLATION) return 4 return 3 } function get_roman_drm() { if (game.active_event === EVENT_BAD_AUGURIES) return 1 if (game.active_event === EVENT_GOOD_AUGURIES) return -1 return 0 } function get_plague_hits() { if (game.active_event === EVENT_PLAGUE_OF_CYPRIAN) { log("Plague B0") return 1 } return 0 } // CRISIS: BARBARIAN LEADER function goto_crisis_barbarian_leader(id, where) { game.count = id game.where = where game.state = "crisis_barbarian_leader" } states.crisis_barbarian_leader = { prompt() { prompt("Crisis: " + EVENT_NAME[game.active_event] + ".") gen_action_region(game.where) }, region(where) { set_barbarian_location(game.count, game.where) goto_take_actions() }, } // CRISIS: RIVAL EMPEROR function goto_crisis_rival_emperor(id, where) { game.count = id game.where = where game.state = "crisis_rival_emperor" } states.crisis_rival_emperor = { prompt() { prompt("Crisis: " + EVENT_NAME[game.active_event] + ".") gen_action_region(game.where) }, region(where) { set_rival_emperor_location(game.count, game.where) goto_take_actions() }, } // CRISIS: PALMYRA ALLIES function goto_palmyra_allies() { game.count = roll_dice_no_reroll(4, 4) resume_palmyra_allies() } function resume_palmyra_allies() { // TODO: barbarian leaders if ( (find_active_barbarian_of_tribe(SASSANIDS, GALATIA) >= 0) || (find_active_barbarian_of_tribe(SASSANIDS, SYRIA) >= 0) || (find_active_barbarian_of_tribe(SASSANIDS, SASSANIDS_HOMELAND) >= 0) ) game.state = "palmyra_allies" else goto_take_actions() } states.palmyra_allies = { prompt() { prompt("Palmyra Allies: Remove " + game.count + " active Sassanids.") let id, where where = get_barbarian_location(SHAPUR) if (where === GALATIA || where === SYRIA || where === SASSANIDS_HOMELAND) gen_action_barbarian(SHAPUR) where = get_barbarian_location(ARDASHIR) if (where === GALATIA || where === SYRIA || where === SASSANIDS_HOMELAND) gen_action_barbarian(ARDASHIR) id = find_active_non_leader_barbarian_of_tribe(SASSANIDS, GALATIA) if (id >= 0) gen_action_barbarian(id) id = find_active_non_leader_barbarian_of_tribe(SASSANIDS, SYRIA) if (id >= 0) gen_action_barbarian(id) id = find_active_non_leader_barbarian_of_tribe(SASSANIDS, SASSANIDS_HOMELAND) if (id >= 0) gen_action_barbarian(id) }, barbarian(id) { push_undo() eliminate_barbarian(id) if (--game.count === 0) goto_take_actions() else resume_palmyra_allies() }, } // CRISIS: LUDI SAECULARES function goto_ludi_saeculares() { game.count = game.current // remember current player let emperor = get_province_player(ITALIA) if (emperor >= 0) { game.current = emperor game.state = "ludi_saeculares" } else { log("There is no Emperor.") goto_take_actions() } } states.ludi_saeculares = { prompt() { prompt("Ludi Saeculares: Discard one card from your hand.") for (let c of current_hand()) gen_action_card(c) }, card(c) { push_undo() set_delete(current_hand(), c) set_add(current_discard(), c) award_legacy(game.current, "Ludi Saeculares", 2 * card_value(c)) game.state = "ludi_saeculares_done" }, } states.ludi_saeculares_done = { prompt() { prompt("Ludi Saeculares: Discard one card from your hand.") view.actions.done = 1 }, done() { if (game.current !== game.count) clear_undo() game.current = game.count goto_take_actions() }, } // === 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 >= get_improvement_cost()) { 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() // If a castra is on lone militia, but general is now in the space, move it to the general. for (let where = 0; where < 12; ++where) { if (is_own_province(where) && has_militia_castra(where)) { let id = get_capital_general(where) if (is_own_general(id)) { remove_militia_castra(where) add_general_castra(id) } } } 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 { push_undo() 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, 2) remove_one_mob(where) }, amphitheater() { push_undo() spend_ip(POPULACE, get_improvement_cost()) let where = get_governor_location(game.selected_governor) add_amphitheater(where) log("Built Amphitheater in S" + where + ".") }, basilica() { push_undo() spend_ip(POPULACE, get_improvement_cost()) let where = get_governor_location(game.selected_governor) add_basilica(where) log("Built Basilica in S" + where + ".") }, limes() { push_undo() spend_ip(POPULACE, get_improvement_cost()) 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_selected_region(), game.selected_general, id) }, rival_emperor(id) { push_undo() goto_battle_vs_rival_emperor(get_selected_region(), game.selected_general, id) }, militia(where) { push_undo() goto_battle_vs_militia(get_selected_region(), game.selected_general) }, } // FREE ACTION: ENTER PROVINCIAL CAPITAL function enter_capital() { let where = get_general_location(game.selected_general) set_general_inside_capital(game.selected_general) if (is_pretender_province(where) && is_enemy_province(where)) { game.count = get_province_governor(where) / 6 | 0 game.where = where if (is_seat_of_power(where)) { game.state = "occupy_seat_of_power_1" return } else { game.state = "occupy_breakaway" return } } if (game.battle) end_battle() } function resume_occupy_seat_of_power() { for (let where = 1; where < 12; ++where) { if (is_breakaway(where) && (get_province_governor(where) / 6 | 0) === game.count) { game.state = "occupy_seat_of_power_2" return } } goto_replace_pretender() } states.occupy_seat_of_power_1 = { prompt() { prompt("Occupy Pretender Provincial Capital: Remove Seat of Power and pretender.") view.color = POPULACE gen_action_region(game.where) }, region(where) { push_undo() log("Removed Seat of Power in S" + where) remove_seat_of_power(where) remove_governor(where) resume_occupy_seat_of_power() }, } states.occupy_seat_of_power_2 = { prompt() { prompt("Occupy Pretender Provincial Capital: Remove Breakaway markers.") view.color = POPULACE for (let where = 1; where < 12; ++where) if (is_breakaway(where) && (get_province_governor(where) / 6 | 0) === game.count) gen_action_region(where) }, region(where) { push_undo() log("Removed Breakaway in S" + where) remove_breakaway(where) resume_occupy_seat_of_power() }, } states.occupy_breakaway = { prompt() { prompt("Occupy Pretender Provincial Capital: Remove Breakaway marker.") view.color = POPULACE gen_action_region(game.where) }, region(where) { push_undo() log("Removed Breakaway in S" + where) remove_breakaway(where) remove_governor(where) goto_replace_pretender() }, } function goto_replace_pretender() { if (has_available_governor()) game.state = "replace_pretender_governor" else game.state = "take_actions" } states.replace_pretender_governor = { prompt() { prompt("Occupy Pretender Provincial Capital: You may place an available Governor.") view.selected_region = game.where view.color = SENATE for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i if (get_governor_location(id) === AVAILABLE) gen_action_governor(id) } view.actions.pass = 1 }, governor(id) { push_undo() log("Placed Governor in S" + game.where) set_governor_location(id, game.where) game.state = "take_actions" }, pass() { push_undo() game.state = "take_actions" }, } // 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) } // 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) eliminate_militia(where) remove_all_mobs(where) remove_quaestor(where) // TODO: manual? if (is_seat_of_power(where)) auto_remove_pretender_empire(where) remove_breakaway(where) let old_governor = get_province_governor(where) if (old_governor >= 0) { set_governor_location(old_governor, AVAILABLE) if (where !== ITALIA && is_emperor_governor(old_governor)) reduce_support(ITALIA) } if (where !== ITALIA) game.support[where] = 1 update_neutral_italia() } function place_governor(where, new_governor) { eliminate_militia(where) remove_all_mobs(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_governor(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_governor(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!") if (game.where === ITALIA) { // Remember for Damnatio Memoriae let old_emperor = get_province_player(ITALIA) if (old_emperor >= 0) game.count = (old_emperor << 3) | get_support(ITALIA) else game.count = 0 } place_governor(game.where, game.selected_governor) } else { logi("Failed!") } if (has_card_event(CARD_S4) || has_card_event(CARD_S4B)) game.state = "damnatio_memoriae" else game.state = "take_actions" } // ACTION: DAMNATIO MEMORIAE states.damnatio_memoriae = { prompt() { prompt("Place Governor: You may play Damnatio Memoriae!") gen_card_event(CARD_S4) gen_card_event(CARD_S4B) }, card(c) { push_undo() log(card_name(c)) set_add(game.used, c) play_card_event(c) }, pass() { push_undo() game.state = "take_actions" }, } function play_damnatio_memoriae() { award_legacy(game.count >> 3, "Damnatio Memoriae", -get_support(ITALIA)) game.state = "damnatio_memoriae_mobs" game.count = game.count & 7 } function play_damnatio_memoriae_exp() { award_legacy(game.count >> 3, "Damnatio Memoriae", -get_support(ITALIA)) game.state = "take_actions" } states.damnatio_memoriae_mobs = { prompt() { prompt("Damnatio Memoriae: Place " + game.count + " Mobs provinces you govern.") view.color = SENATE for (let where = 0; where < 12; ++where) if (is_own_province(where) && game.mobs[where] < 6) gen_action_region(where) }, region(where) { push_undo() log("Added Mob to S" + where) add_one_mob(where) if (--game.count === 0) game.state = "take_actions" }, } // ACTION: PRAETORIAN GUARD function has_unused_praetorian_guard() { return game.ip[MILITARY] >= 1 && !is_emperor_player() && !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_emperor_player() && !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.color = MILITARY 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) && !is_breakaway(where) && !is_seat_of_power(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_active_non_leader_barbarian_of_tribe(where, tribe) >= 0) return true if (is_province(where)) if (find_inactive_non_leader_barbarian_of_tribe(where, tribe) >= 0) return true } return false } function gen_foederati(where) { let tribe_count = get_tribe_count() for (let tribe = 0; tribe < tribe_count; ++tribe) { let id = find_active_non_leader_barbarian_of_tribe(where, tribe) if (id >= 0) gen_action_barbarian(id) if (is_province(where)) { id = find_inactive_non_leader_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 (is_region(where)) { 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) gen_foederati(from) for (let to of ADJACENT[from]) gen_foederati(to) }, 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" }, } // CARD: PRETENDER function auto_remove_pretender_empire(seat) { let pretender = get_province_governor(seat) / 6 | 0 log("Removed Seat of Power in S" + seat) remove_seat_of_power(seat) for (let where = 1; where < 12; ++where) { if (is_breakaway(where) && (get_province_governor(where) / 6 | 0) === pretender) { log("Removed Breakaway in S" + where) remove_breakaway(where) } } } function can_play_pretender() { if (is_emperor_player()) return false if (is_pretender_player()) return false for (let where = 1; where < 12; ++where) if (is_possible_seat_of_power(where)) return true return false } function play_pretender() { game.state = "pretender_seat_of_power" } states.pretender_seat_of_power = { prompt() { prompt("Pretender: Place a Seat of Power.") view.color = POPULACE for (let where = 0; where < 12; ++where) for (let where = 1; where < 12; ++where) if (is_possible_seat_of_power(where)) gen_action_region(where) }, region(where) { push_undo() log("Seat of Power in S" + where) add_seat_of_power(where) remove_quaestor(where) // no effect anymore goto_pretender_breakaway() }, } function goto_pretender_breakaway() { let seat = find_seat_of_power() for (let where of PRETENDER_ADJACENT[seat]) { if (get_support(where) >= 3 && !is_breakaway(where) && is_own_province(where)) { game.state = "pretender_breakaway" return } } game.state = "take_actions" } states.pretender_breakaway = { prompt() { prompt("Pretender: Place Breakaway markers.") view.color = POPULACE let seat = find_seat_of_power() for (let where of PRETENDER_ADJACENT[seat]) if (get_support(where) >= 3 && !is_breakaway(where) && is_own_province(where)) gen_action_region(where) }, region(where) { push_undo() log("Breakaway in S" + where) add_breakaway(where) remove_quaestor(where) // no effect anymore goto_pretender_breakaway() }, } // === COMBAT === function play_flanking_maneuver() { game.battle.flanking = 1 } 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) { let tribe = get_barbarian_tribe(target) log("Initiate Battle vs " + BARBARIAN_NAME[tribe] + " in S" + where) goto_battle("barbarians", where, attacker, tribe) } 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, flanking: 0, killed: 0 } game.state = "battle" if (attacker >= 0) { if (is_general_inside_capital(attacker)) remove_militia_castra(where) remove_general_castra(attacker) set_general_battled(attacker) } else { remove_militia_castra(where) 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 = 5 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) } for (let id = 0; id < 3; ++id) if (get_rival_emperor_location(id) === where) gen_action_rival_emperor(id) } } states.battle = { prompt() { prompt("Battle!") if (!game.battle.flanking) gen_card_event(CARD_M3) view.actions.roll = 1 }, card(c) { log(card_name(c)) set_add(game.used, c) play_card_event(c) }, roll() { // clear_undo() roll_combat_dice() if (game.battle.flanking) game.state = "flanking_maneuver" else goto_assign_hits() }, } function format_hits() { return `${game.battle.ahits} hits to attacker vs ${game.battle.dhits} hits to defender` } states.flanking_maneuver = { prompt() { prompt("Flanking Maneuver: " + format_hits() + ".") view.actions.reroll = 1 view.actions.pass = 1 }, pass() { goto_assign_hits() }, reroll() { roll_flanking_maneuver_dice() goto_assign_hits() }, } function roll_combat_dice() { game.battle.dtaken = 0 game.battle.ataken = 0 game.battle.dhits = roll_attacker_dice() log("Total " + game.battle.dhits + " hits!") game.battle.ahits = roll_defender_dice() log("Total " + game.battle.ahits + " hits!") log_br() } function roll_flanking_maneuver_dice() { log("Flanking Maneuver Reroll") game.battle.dhits = roll_defender_dice() log("Total " + game.battle.dhits + " hits!") log_br() } function roll_general_dice(general) { let army = ARMY + general let n = 0 let drm = get_roman_drm() log(GENERAL_NAME[general]) if (is_general_inside_capital(general) && has_militia(game.where)) { log("Militia") n += roll_dice(1, 5 + drm) } let full_strength = 0 let reduced = 0 for (let id = 0; id < LEGION_COUNT; ++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 + drm) } if (reduced > 0) { log("Reduced Legions") n += roll_dice(reduced, 5 + drm) } 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 + drm) } return n } function roll_militia_dice() { log("Militia") return roll_dice(1, 5 + get_roman_drm()) } 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 += is_barbarian_leader(id) ? 2 : 1 return roll_dice(n, 4) } function roll_attacker_dice() { log_h3("ATTACKER") let n = get_plague_hits() if (game.battle.attacker < 0) n = roll_militia_dice() else n = roll_general_dice(game.battle.attacker) if (game.battle.type === "militia" && has_militia_castra(game.where)) { log("Castra reduces 1 hit") n -= 1 } if (game.battle.type === "general" && has_general_castra(game.battle.target)) { log("Castra reduces 1 hit") n -= 1 } if (game.battle.type === "barbarians" && get_barbarian_location(SHAPUR) === game.where) { log("Shapur I reduced 1 hit") n -= 1 } return Math.max(0, n) } function roll_defender_dice() { log_h3("DEFENDER") let n = get_plague_hits() switch (game.battle.type) { case "militia": n = roll_militia_dice() break case "rival_emperor": n = roll_rival_emperor_dice(game.battle.target) break case "barbarians": n = roll_barbarian_dice(game.battle.target) break case "general": n = roll_general_dice(game.battle.target) break } return Math.max(0, n) } // COMBAT: ASSIGN HITS function has_hits_rival_emperor(id) { return get_rival_emperor_location(id) !== UNAVAILABLE } function gen_hits_rival_emperor(id) { gen_action_rival_emperor(id) } 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 < LEGION_COUNT; ++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 < LEGION_COUNT; ++id) { if (get_legion_location(id) === army && !is_legion_reduced(id)) { gen_action_legion(id) done = true } } } if (!done) { for (let id = 0; id < LEGION_COUNT; ++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() { 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("Combat: " + format_hits() + " \u2013 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 eliminate_militia(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("Combat: " + format_hits() + " \u2013 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 eliminate_militia(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() }, rival_emperor(id) { game.battle.dtaken += 1 eliminate_rival_emperor(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() 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, reason, n) { if (n > 0) log(PLAYER_NAMES[p] + " gained " + n + " Legacy for " + reason + ".") if (n < 0) log(PLAYER_NAMES[p] + " lost " + (-n) + " Legacy for " + reason + ".") game.legacy[p] += n } function goto_combat_victory_defender() { game.battle.killed = 0 if (game.battle.type === "general") award_legacy(game.battle.target / 6 | 0, "Victory", 2) if (game.battle.type === "militia") award_legacy(get_province_player(game.where), "Victory", 2) end_battle() } function goto_combat_victory_attacker() { if (game.battle.type === "barbarians") { award_legacy(game.current, "Victory", 2 + 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]) } else { award_legacy(game.current, "Victory", 2) } // Defending Romans must retreat into province if (game.battle.type === "general") { set_general_outside_capital(game.battle.target) remove_general_castra(game.battle.target) } 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() enter_capital() }, pass() { push_undo() end_battle() }, } function end_battle() { // Deselect eliminated general... if (game.selected_general >= 0 && get_general_location(game.selected_general) === AVAILABLE) game.selected_general = -1 if (game.battle.killed && can_free_increase_support_level(game.where)) { game.state = "free_increase_support_level" return } game.battle = null game.state = "take_actions" } function can_free_increase_support_level(where) { return where !== ITALIA && get_support(where) < 4 && is_own_province(where) } states.free_increase_support_level = { prompt() { prompt("Combat: Increase Support Level or use Leader to buy cheaper card?") view.color = POPULACE gen_action_region(game.where) view.actions.save = 1 }, region(where) { push_undo() increase_support(game.where) game.battle = null game.state = "take_actions" }, save() { push_undo() log("Saved for cost reduction.") game.killed |= game.battle.killed 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 (get_rival_emperor_location(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_emperor_player() && 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(ITALIA) }, 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() { for (let where = 1; where < 12; ++where) { if (is_expand_pretender_province(where)) { game.state = "expand_pretender_empire" return } } goto_gain_legacy() } states.expand_pretender_empire = { prompt() { prompt("Expand Pretender Empire!") view.color = POPULACE for (let where = 1; where < 12; ++where) if (is_expand_pretender_province(where)) gen_action_region(where) }, region(where) { push_undo() add_breakaway(where) remove_quaestor(where) // no effect anymore goto_expand_pretender_empire() }, } // === GAIN LEGACY === function goto_gain_legacy() { log_h3("Gain Legacy") if (is_only_pretender_player()) award_legacy(game.current, "Pretender", count_own_breakaway_provinces()) if (is_emperor_player()) goto_legitimize_claim() else goto_gain_legacy_provinces() } function goto_legitimize_claim() { if (is_pretender_player()) game.state = "legitimize_claim" else goto_gain_legacy_emperor() } states.legitimize_claim = { prompt() { prompt("Gain Legacy: Remove Seat of Power and Breakaway markers in your provinces.") for (let where = 1; where < 12; ++where) if (is_own_province(where) && is_seat_of_power(where) || is_breakaway(where)) gen_action_region(where) }, region(where) { push_undo() remove_seat_of_power(where) remove_breakaway(where) goto_legitimize_claim() }, } function goto_gain_legacy_emperor() { award_legacy(game.current, "Emperor", Math.max(0, game.support[ITALIA] - count_pretender_provinces())) if (!is_any_rival_emperor_or_pretender()) { log(PLAYER_NAMES[game.current] + " gained Emperor Turn") game.emperor_turns[game.current] += 1 } goto_gain_legacy_provinces() } function goto_gain_legacy_provinces() { award_legacy(game.current, "Provinces", count_own_provinces()) award_legacy(game.current, "Improvements", count_own_improvements()) if (is_emperor_player() && game.legacy[game.current] >= 60) { log("Game will end after this round!") game.end = 1 } 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.count = 0 game.pp = count_political_points() if (game.end) goto_end_of_turn() else 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 } function has_military_card_bonus() { let military_bonus = 0 if (game.killed & CNIVA_BONUS) military_bonus += 2 if (game.killed & ARDASHIR_BONUS) military_bonus += 2 if (game.killed & SHAPUR_BONUS) military_bonus += 2 return military_bonus } function has_senate_card_bonus() { let senate_bonus = 0 if (game.killed & (1 << POSTUMUS)) senate_bonus += 2 if (game.killed & (1 << PRIEST_KING)) senate_bonus += 2 if (game.killed & (1 << ZENOBIA)) senate_bonus += 2 return senate_bonus } function spend_military_card_bonus() { game.killed &= ~(CNIVA_BONUS | ARDASHIR_BONUS | SHAPUR_BONUS) } function spend_senate_card_bonus() { game.killed &= ~((1 << POSTUMUS) | (1 << PRIEST_KING) | (1 << ZENOBIA)) } states.buy_trash = { prompt() { let military_bonus = has_military_card_bonus() let senate_bonus = has_senate_card_bonus() prompt("Buy/Trash cards: " + game.pp + "PP left.") if (military_bonus) view.prompt += " First Military card is 2 cheaper." if (senate_bonus) view.prompt += " First Senate card is 2 cheaper." let nprov = count_own_provinces() if (game.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_value(c) if (cost > nprov) cost *= 2 cost += game.count if (military_bonus && card_influence(c) === MILITARY) cost -= 2 if (senate_bonus && card_influence(c) === SENATE) cost -= 2 if (game.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.pp -= 3 } else { let military_bonus = has_military_card_bonus() let senate_bonus = has_senate_card_bonus() log("Bought " + card_name(c)) set_add(current_discard(), c) set_delete(find_market_with_card(c), c) let cost = card_value(c) if (cost > count_own_provinces()) cost *= 2 cost += game.count if (military_bonus && card_influence(c) === MILITARY) { spend_military_card_bonus() cost -= 2 } if (senate_bonus && card_influence(c) === SENATE) { spend_senate_card_bonus() cost -= 2 } game.pp -= cost game.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.current) 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(game.current) }, done() { clear_undo() end_refill_hand() } } function end_refill_hand() { game.current = next_player() if (game.current === game.first && game.end) goto_game_end() else goto_start_turn() } // === GAME END === function max_emperor_turns(cutoff) { let n = 0 for (let x of game.emperor_turns) if (x > n && x < cutoff) n = x return n } function award_emperor_turns(amount, cutoff) { let x = max_emperor_turns(cutoff) if (x > 0) { for (let p = 0; p < game.legacy.length; ++p) if (game.emperor_turns[p] === x) award_legacy(p, "Emperor Turns", amount) } return x } function vp_tie(p) { let vp = game.legacy[p] * 100000 game.current = p if (is_emperor_player()) vp += 10000 if (is_pretender_player()) vp += 1000 vp += count_own_provinces() * 100 vp += count_own_legions() * 10 vp += random(10) return vp } function goto_game_end() { let cutoff = 1000 log_h2("Game End") cutoff = award_emperor_turns(10, cutoff) cutoff = award_emperor_turns(6, cutoff) cutoff = award_emperor_turns(3, cutoff) cutoff = award_emperor_turns(0, cutoff) let victor = game.legacy.map((legacy,p) => [vp_tie(p),p]).sort((a,b) => b[0] - a[0])[0][1] goto_game_over(PLAYER_NAMES[victor], PLAYER_NAMES[victor] + " won!") } function goto_game_over(result, victory) { game.state = "game_over" game.active = "None" game.result = result game.victory = victory log_br() log(game.victory) return true } states.game_over = { get inactive() { return game.victory }, prompt() { view.prompt = game.victory }, } // === 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, 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) { if (is_barbarian_leader(id)) { set_barbarian_location(id, UNAVAILABLE) } else { set_barbarian_location(id, home) set_barbarian_inactive(id) } } } exports.setup = function (seed, scenario, options) { let player_count = options.players || 4 game = { seed: seed, log: [], undo: [], active: 0, current: 0, state: "setup_province", first: 0, events: null, active_event: 0, ip: [], pp: 0, selected_governor: -1, selected_general: -1, played: [], used: [], killed: 0, 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).fill(AVAILABLE), rival_emperors: [ 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: [], } 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(SASSANIDS, SASSANIDS_HOMELAND) else setup_barbarians(SASSANIDS, UNAVAILABLE) if (player_count >= 4) setup_barbarians(NOMADS, NOMADS_HOMELAND) else setup_barbarians(NOMADS, UNAVAILABLE) for (let player = 0; player < player_count; ++player) { game.hand[player] = [] game.draw[player] = setup_player_deck(player) game.discard[player] = [] // for (let i = 0; i < 9; ++i) set_add(game.draw[player], game.market[i].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() { 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] let player_count = game.legacy.length view = { log: game.log, current: game.current, prompt: null, crisis: game.crisis, 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, seat_of_power: game.seat_of_power, breakaway: game.breakaway, governors: game.governors, generals: game.generals, legions: game.legions, barbarians: game.barbarians, rival_emperors: game.rival_emperors, dice: game.dice, event: game.active_event, 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 (game.current !== player) { 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_rival_emperor(id) { gen_action("rival_emperor", 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 } } }