"use strict" // TODO: barbarian leaders -> barbarian list var game var view const states = {} const P1 = "Red" const P2 = "Blue" const P3 = "Yellow" const P4 = "Green" exports.scenarios = [ "Standard" ] exports.roles = function (scenario, options) { if (options.players == 1) return [ "Solo" ] if (options.players == 2) return [ P1, P2 ] if (options.players == 3) return [ P1, P2, P3 ] return [ P1, P2, P3, P4 ] } // === CONSTANTS === const PLAYER_NAMES = [ P1, P2, P3, P4 ] const PLAYER_INDEX = { [P1]: 0, [P2]: 1, [P3]: 2, [P4]: 3, "Solo": 4, "Observer": -1, } const MILITARY = 0 const SENATE = 1 const POPULACE = 2 const LEGION_COUNT = 33 // REGIONS const ITALIA = 0 const ASIA = 1 const GALLIA = 2 const MACEDONIA = 3 const PANNONIA = 4 const THRACIA = 5 const AEGYPTUS = 6 const AFRICA = 7 const HISPANIA = 8 const BRITANNIA = 9 const GALATIA = 10 const SYRIA = 11 const ALAMANNI_HOMELAND = 12 const FRANKS_HOMELAND = 13 const GOTHS_HOMELAND = 14 const NOMADS_HOMELAND = 15 const SASSANIDS_HOMELAND = 16 const MARE_OCCIDENTALE = 17 const MARE_ORIENTALE = 18 const OCEANUS_ATLANTICUS = 19 const PONTUS_EUXINUS = 20 const AVAILABLE = 21 const UNAVAILABLE = 22 const ARMY = 23 const REGION_NAME = [ "Italia", "Asia", "Gallia", "Macedonia", "Pannonia", "Thracia", "Aegyptus", "Africa", "Hispania", "Britannia", "Galatia", "Syria", "Alamanni Homeland", "Franks Homeland", "Goths Homeland", "Nomads Homeland", "Sassanids Homeland", "Mare Occidentale", "Mare Orientale", "Oceanus Atlanticus", "Pontus Euxinus", "Available", "Unavailable", ] const ADJACENT = [ /* ITALIA */ [ GALLIA, PANNONIA, MARE_OCCIDENTALE ], /* ASIA */ [ THRACIA, GALATIA, MARE_ORIENTALE, PONTUS_EUXINUS ], /* GALLIA */ [ ITALIA, PANNONIA, HISPANIA, FRANKS_HOMELAND, MARE_OCCIDENTALE, OCEANUS_ATLANTICUS ], /* MACEDONIA */ [ PANNONIA, THRACIA, MARE_OCCIDENTALE, MARE_ORIENTALE ], /* PANNONIA */ [ ITALIA, GALLIA, MACEDONIA, THRACIA, ALAMANNI_HOMELAND, FRANKS_HOMELAND, MARE_OCCIDENTALE ], /* THRACIA */ [ ASIA, MACEDONIA, PANNONIA, ALAMANNI_HOMELAND, GOTHS_HOMELAND, MARE_ORIENTALE, PONTUS_EUXINUS ], /* AEGYPTUS */ [ AFRICA, SYRIA, NOMADS_HOMELAND, MARE_ORIENTALE ], /* AFRICA */ [ AEGYPTUS, HISPANIA, NOMADS_HOMELAND, MARE_OCCIDENTALE, MARE_ORIENTALE, OCEANUS_ATLANTICUS ], /* HISPANIA */ [ GALLIA, AFRICA, MARE_OCCIDENTALE, OCEANUS_ATLANTICUS ], /* BRITANNIA */ [ OCEANUS_ATLANTICUS ], /* GALATIA */ [ ASIA, SYRIA, SASSANIDS_HOMELAND, MARE_ORIENTALE, PONTUS_EUXINUS ], /* SYRIA */ [ AEGYPTUS, GALATIA, SASSANIDS_HOMELAND, MARE_ORIENTALE ], /* ALAMANNI_HOMELAND */ [ PANNONIA, THRACIA, FRANKS_HOMELAND, GOTHS_HOMELAND ], /* FRANKS_HOMELAND */ [ GALLIA, PANNONIA, ALAMANNI_HOMELAND ], /* GOTHS_HOMELAND */ [ THRACIA, ALAMANNI_HOMELAND, PONTUS_EUXINUS ], /* NOMADS_HOMELAND */ [ AEGYPTUS, AFRICA, OCEANUS_ATLANTICUS ], /* SASSANIDS_HOMELAND */ [ GALATIA, SYRIA, PONTUS_EUXINUS ], /* MARE_OCCIDENTALE */ [ ITALIA, GALLIA, MACEDONIA, PANNONIA, AFRICA, HISPANIA, MARE_ORIENTALE, OCEANUS_ATLANTICUS ], /* MARE_ORIENTALE */ [ ASIA, MACEDONIA, THRACIA, AEGYPTUS, AFRICA, GALATIA, SYRIA, MARE_OCCIDENTALE ], /* OCEANUS_ATLANTICUS */ [ GALLIA, AFRICA, HISPANIA, BRITANNIA, NOMADS_HOMELAND, MARE_OCCIDENTALE ], /* PONTUS_EUXINUS */ [ ASIA, THRACIA, GALATIA, GOTHS_HOMELAND, SASSANIDS_HOMELAND ], ] // BARBARIANS const ALAMANNI = 0 const FRANKS = 1 const GOTHS = 2 const NOMADS = 3 const SASSANIDS = 4 const TRIBE_COUNT = [ 0, 5, 3, 4, 5 ] const BARBARIAN_NAME = [ "Alamanni", "Franks", "Goths", "Nomads", "Sassanids", ] const BARBARIAN_HOMELAND = [ ALAMANNI_HOMELAND, FRANKS_HOMELAND, GOTHS_HOMELAND, NOMADS_HOMELAND, SASSANIDS_HOMELAND, ] const BARBARIAN_INVASION = [ // Alamanni [ [ 1, 3, [ PANNONIA, ITALIA ] ], [ 4, 6, [ THRACIA, MACEDONIA ] ], ], // Franks [ [ 1, 2, [ BRITANNIA, GALLIA ] ], [ 3, 4, [ GALLIA, HISPANIA ] ], [ 5, 6, [ PANNONIA, ITALIA ] ], ], // Goths [ [ 1, 2, [ THRACIA, MACEDONIA ] ], [ 3, 4, [ ASIA, MACEDONIA ] ], [ 5, 6, [ GALATIA, SYRIA ] ], ], // Nomads [ [ 1, 3, [ AFRICA, HISPANIA ] ], [ 4, 6, [ AEGYPTUS, SYRIA ] ], ], // Sassanids [ [ 1, 3, [ GALATIA, ASIA ] ], [ 4, 6, [ SYRIA, AEGYPTUS ] ], ], ] const CRISIS_TABLE_4P = [ 0, SASSANIDS, FRANKS, SASSANIDS, GOTHS, 0, ALAMANNI, NOMADS, FRANKS, NOMADS, 0, ] const CRISIS_TABLE_3P = [ 0, FRANKS, SASSANIDS, SASSANIDS, FRANKS, 0, ALAMANNI, GOTHS, GOTHS, ALAMANNI, 0, ] const CRISIS_TABLE_2P = [ 0, FRANKS, ALAMANNI, FRANKS, GOTHS, 0, GOTHS, FRANKS, ALAMANNI, ALAMANNI, 0, ] // CARDS const EVENT_PLAGUE_OF_CYPRIAN = 1 const EVENT_ARDASHIR = 2 const EVENT_PRIEST_KING = 3 const EVENT_PALMYRA_ALLIES = 4 const EVENT_SHAPUR_I = 5 const EVENT_POSTUMUS = 6 const EVENT_LUDI_SAECULARES = 7 const EVENT_CNIVA = 8 const EVENT_ZENOBIA = 9 const EVENT_BAD_AUGURIES = 10 const EVENT_RAIDING_PARTIES = 11 const EVENT_PREPARING_FOR_WAR = 12 const EVENT_INFLATION = 13 const EVENT_GOOD_AUGURIES = 14 const EVENT_DIOCLETIAN = 15 // 12x const CARD_M1 = [ 1, 12 ] const CARD_S1 = [ 13, 24 ] const CARD_P1 = [ 25, 36 ] // 9x const CARD_M2 = [ 37, 45 ] const CARD_S2 = [ 46, 54 ] const CARD_P2 = [ 55, 63 ] // 8x const CARD_M3 = [ 64, 71 ] const CARD_S3 = [ 72, 79 ] const CARD_P3 = [ 80, 87 ] // 6x const CARD_M4 = [ 88, 93 ] const CARD_S4 = [ 94, 99 ] const CARD_P4 = [ 100, 105 ] function card_name(c) { if (c >= CARD_M1[0] && c <= CARD_M1[1]) return "M1" if (c >= CARD_M2[0] && c <= CARD_M2[1]) return "M2" if (c >= CARD_M3[0] && c <= CARD_M3[1]) return "M3" if (c >= CARD_M4[0] && c <= CARD_M4[1]) return "M4" if (c >= CARD_S1[0] && c <= CARD_S1[1]) return "S1" if (c >= CARD_S2[0] && c <= CARD_S2[1]) return "S2" if (c >= CARD_S3[0] && c <= CARD_S3[1]) return "S3" if (c >= CARD_S4[0] && c <= CARD_S4[1]) return "S4" if (c >= CARD_P1[0] && c <= CARD_P1[1]) return "P1" if (c >= CARD_P2[0] && c <= CARD_P2[1]) return "P2" if (c >= CARD_P3[0] && c <= CARD_P3[1]) return "P3" if (c >= CARD_P4[0] && c <= CARD_P4[1]) return "P4" return "??" } function card_cost(c) { if (c >= CARD_M1[0] && c <= CARD_M1[1]) return 1 if (c >= CARD_M2[0] && c <= CARD_M2[1]) return 2 if (c >= CARD_M3[0] && c <= CARD_M3[1]) return 3 if (c >= CARD_M4[0] && c <= CARD_M4[1]) return 4 if (c >= CARD_S1[0] && c <= CARD_S1[1]) return 1 if (c >= CARD_S2[0] && c <= CARD_S2[1]) return 2 if (c >= CARD_S3[0] && c <= CARD_S3[1]) return 3 if (c >= CARD_S4[0] && c <= CARD_S4[1]) return 4 if (c >= CARD_P1[0] && c <= CARD_P1[1]) return 1 if (c >= CARD_P2[0] && c <= CARD_P2[1]) return 2 if (c >= CARD_P3[0] && c <= CARD_P3[1]) return 3 if (c >= CARD_P4[0] && c <= CARD_P4[1]) return 4 return "??" } function card_event_name(c) { if (c >= CARD_M1[0] && c <= CARD_M1[1]) return "None" if (c >= CARD_M2[0] && c <= CARD_M2[1]) return "Castra" if (c >= CARD_M3[0] && c <= CARD_M3[1]) return "Flanking Maneuver" if (c >= CARD_M4[0] && c <= CARD_M4[1]) return "Praetorian Guard" if (c >= CARD_S1[0] && c <= CARD_S1[1]) return "None" if (c >= CARD_S2[0] && c <= CARD_S2[1]) return "Tribute" if (c >= CARD_S3[0] && c <= CARD_S3[1]) return "Foederati" if (c >= CARD_S4[0] && c <= CARD_S4[1]) return "Damnatio Memoriae" if (c >= CARD_P1[0] && c <= CARD_P1[1]) return "None" if (c >= CARD_P2[0] && c <= CARD_P2[1]) return "Quaestor" if (c >= CARD_P3[0] && c <= CARD_P3[1]) return "Mob" if (c >= CARD_P4[0] && c <= CARD_P4[1]) return "Pretender" return "None" } function add_card_ip(c) { if (c >= CARD_M1[0] && c <= CARD_M1[1]) return game.ip[MILITARY] += 1 if (c >= CARD_M2[0] && c <= CARD_M2[1]) return game.ip[MILITARY] += 2 if (c >= CARD_M3[0] && c <= CARD_M3[1]) return game.ip[MILITARY] += 3 if (c >= CARD_M4[0] && c <= CARD_M4[1]) return game.ip[MILITARY] += 4 if (c >= CARD_S1[0] && c <= CARD_S1[1]) return game.ip[SENATE] += 1 if (c >= CARD_S2[0] && c <= CARD_S2[1]) return game.ip[SENATE] += 2 if (c >= CARD_S3[0] && c <= CARD_S3[1]) return game.ip[SENATE] += 3 if (c >= CARD_S4[0] && c <= CARD_S4[1]) return game.ip[SENATE] += 4 if (c >= CARD_P1[0] && c <= CARD_P1[1]) return game.ip[POPULACE] += 1 if (c >= CARD_P2[0] && c <= CARD_P2[1]) return game.ip[POPULACE] += 2 if (c >= CARD_P3[0] && c <= CARD_P3[1]) return game.ip[POPULACE] += 3 if (c >= CARD_P4[0] && c <= CARD_P4[1]) return game.ip[POPULACE] += 4 } function is_region(where) { return where < AVAILABLE } function is_province(where) { return where < 12 } function is_sea(where) { return where >= MARE_OCCIDENTALE && where <= PONTUS_EUXINUS } function current_hand() { return game.hand[game.current] } function current_draw() { return game.draw[game.current] } function current_discard() { return game.discard[game.current] } // === BOARD STATE === function is_no_place_governor(province) { return province >= game.support.length } function get_support(province) { return game.support[province] } function set_support(province, level) { game.support[province] = level } function get_barbarian_location(id) { return game.barbarians[id] & 63 } function set_barbarian_location(id, loc) { game.barbarians[id] = loc } function is_barbarian_inactive(id) { return game.barbarians[id] & 64 } function is_barbarian_active(id) { return !is_barbarian_inactive(id) } function set_barbarian_inactive(id) { game.barbarians[id] |= 64 } function set_barbarian_active(id) { game.barbarians[id] &= 63 } function get_legion_location(ix) { return game.legions[ix] & 63 } function set_legion_location(ix, loc) { game.legions[ix] = loc } function is_legion_reduced(ix) { return game.legions[ix] & 64 } function set_legion_reduced(ix) { game.legions[ix] |= 64 } function set_legion_full_strength(ix) { game.legions[ix] &= 63 } function is_legion_unused(ix) { return game.legions[ix] === AVAILABLE } function get_governor_location(id, loc) { return game.governors[id] & 63 } function set_governor_location(id, loc) { game.governors[id] = loc } function get_general_location(id) { return game.generals[id] & 63 } function set_general_location(id, loc) { game.generals[id] = loc } function is_general_inside_capital(id) { return game.generals[id] & 64 } function set_general_inside_capital(id) { game.generals[id] |= 64 } function set_general_outside_capital(id) { game.generals[id] &= 63 } function has_general_castra(id) { return game.castra & (1 << id) } function add_general_castra(id) { game.castra |= (1 << id) } function remove_general_castra(id) { game.castra &= ~(1 << id) } function has_militia_castra(province) { return game.mcastra & (1 << province) } function add_militia_castra(province) { game.mcastra |= (1 << province) } function remove_militia_castra(province) { game.mcastra &= ~(1 << province) } function has_quaestor(province) { return game.quaestor & (1 << province) } function add_quaestor(province) { game.quaestor |= (1 << province) } function remove_quaestor(province) { game.quaestor &= ~(1 << province) } function has_militia(province) { return game.militia & (1 << province) } function add_militia(province) { game.militia |= (1 << province) } function remove_militia(province) { game.militia &= ~(1 << province) } function has_mob(province) { return game.mobs[province] > 0 } function count_mobs(province) { return game.mobs[province] } function add_one_mob(province) { game.mobs[province] ++ } function remove_one_mob(province) { game.mobs[province] -- } function remove_all_mobs(province) { game.mobs[province] = 0 } function has_amphitheater(province) { return game.amphitheater & (1 << province) } function has_basilica(province) { return game.basilica & (1 << province) } function has_limes(province) { return game.limes & (1 << province) } function add_amphitheater(province) { game.amphitheater |= (1 << province) } function add_basilica(province) { game.basilica |= (1 << province) } function add_limes(province) { game.limes |= (1 << province) } function is_breakaway(province) { return game.breakaway & (1 << province) } function add_breakaway(province) { game.breakaway |= (1 << province) } function remove_breakaway(province) { game.breakaway &= ~(1 << province) } function is_seat_of_power(province) { return game.seat_of_power & (1 << province) } function add_seat_of_power(province) { game.seat_of_power |= (1 << province) } function remove_seat_of_power(province) { game.seat_of_power &= ~(1 << province) } // === TRANSIENT STATE === function has_placed_governor(province) { return game.placed & (1 << province) } function set_placed_governor(province) { game.placed |= (1 << province) } function has_general_battled(id) { return game.battled & (1 << id) } function set_general_battled(id) { game.battled |= (1 << id) } function has_militia_battled(province) { return game.mbattled & (1 << province) } function set_militia_battled(province) { game.mbattled |= (1 << province) } // === COMPOUND STATE === function for_each_current_general(f) { let a = game.current * 6 for (let id = a; id < a + 6; ++id) f(id, get_general_location(id), is_general_inside_capital(id)) } function 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_inactive(id)) } function find_barbarian(f) { let n = game.barbarians.length for (let id = 0; id < n; ++id) if (f(id, get_barbarian_location(id), is_barbarian_active(id))) return id return -1 } function some_governor(f) { return find_governor(f) >= 0 } function some_general(f) { return find_general(f) >= 0 } function some_barbarian(f) { return find_barbarian(f) >= 0 } function next_player() { return (game.current + 1) % game.legacy.length } function find_unused_legion() { for (let ix = 0; ix < LEGION_COUNT; ++ix) if (get_legion_location(ix) === AVAILABLE) return ix return -1 } function can_build_improvement(province) { if (has_mob(province)) return false if (has_active_barbarians(province)) return false if (has_rival_emperor(province)) return false if (has_enemy_army_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 can_enter_capital(where) { // No Capital if (is_no_place_governor(where)) return false // Occupied by General if (some_general((id, loc, cap) => loc === where && cap)) return false // Occupied by opponent Militia if (has_militia(where)) { if (!is_own_province(where)) return false } return true } function is_own_general(id) { let a = game.current * 6 return id >= a && id < a + 6 } function is_enemy_general(id) { return id >= 0 && !is_own_general(id) } function is_own_governor(id) { let a = game.current * 6 return id >= a && id < a + 6 } function is_enemy_governor(id) { return id >= 0 && !is_own_governor(id) } function is_capital_free_of_enemy(where) { return is_enemy_general(get_capital_general(where)) } function get_province_governor(where) { return find_governor((id, loc) => loc === where) } function get_capital_general(where) { return find_general((id, loc, cap) => loc === where && cap) } function is_neutral_province(where) { if (is_no_place_governor(where)) return false return get_province_governor(where) < 0 } function has_active_barbarians(where) { if (has_barbarian_leader(where)) return true return some_barbarian((id, loc, active) => loc === where && active) } function has_barbarian_leader(where) { for (let i = 0; i < 3; ++i) if (game.barbarian_leaders[i] === where) return true return false } function has_rival_emperor(where) { for (let i = 0; i < 3; ++i) if (game.rival_emperors[i] === where) return true return false } function is_enemy_province(where) { return is_enemy_governor(get_province_governor(where)) } function is_own_province(where) { return is_own_governor(get_province_governor(where)) } function is_emperor(governor) { let emperor = get_province_governor(ITALIA) if (emperor >= 0 && governor >= 0) return (emperor / 6 | 0) === (governor / 6 | 0) return false } function has_enemy_army_in_capital(where) { if (is_enemy_general(get_capital_general(where))) return true if (has_militia(where) && is_enemy_governor(get_province_governor(where))) return true return false } function has_enemy_army_in_province(where) { if (some_general((id, loc) => loc === where && is_enemy_general(id))) return true return false } function spend_ip(type, n) { game.ip[type] -= n } function can_place_governor(where) { 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 i = 0; i < 10; ++i) if (get_barbarian_location(tribe * 10 + i) === home) if (is_barbarian_active(tribe * 10 + i)) return tribe * 10 + i return -1 } function count_active_barbarians_at_home(tribe) { let home = BARBARIAN_HOMELAND[tribe] let n = 0 for (let i = 0; i < 10; ++i) if (get_barbarian_location(tribe * 10 + i) === home) if (is_barbarian_active(tribe * 10 + i)) n += 1 return n } function find_inactive_barbarian_at_home(tribe) { let home = BARBARIAN_HOMELAND[tribe] for (let i = 0; i < 10; ++i) if (get_barbarian_location(tribe * 10 + i) === home) if (is_barbarian_inactive(tribe * 10 + i)) return tribe * 10 + i return -1 } function activate_one_barbarian(tribe) { let i = find_inactive_barbarian_at_home(tribe) if (i >= 0) set_barbarian_active(i) } function count_barbarians_in_province(tribe, where) { let n = 0 for (let i = 0; i < 10; ++i) if (get_barbarian_location(tribe * 10 + i) === where) n += 1 return n } function count_legions_in_army(id) { let n = 0 for (let i = 0; i < LEGION_COUNT; ++i) if (get_legion_location(i) === ARMY + id) ++n return n } function 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 count_units_in_army(id) { let n = 0 for (let i = 0; i < LEGION_COUNT; ++i) if (get_legion_location(i) === ARMY + id) ++n for (let loc of game.barbarians) if (loc === ARMY + id) ++n return n } function count_own_provinces() { let n = 0 for (let where = 0; where < 12; ++where) if (is_own_province(where)) ++n return n } function count_own_basilicas() { let n = 0 for (let where = 0; where < 12; ++where) if (is_own_province(where) && has_basilica(where)) ++n return n } function roll_dice(count, target) { let hits = 0 console.log("roll_dice", count, target) while (count > 0) { let summary = [] let sixes = 0 for (let i = 0; i < count; ++i) { let die = roll_die() if (die === 6) sixes += 1 if (die >= target) { summary.push("B" + die) hits += 1 } else { summary.push("W" + die) } } log("Rolled " + summary.join(" ")) count = sixes } return hits } // === SETUP === states.setup_province = { prompt() { view.prompt = "Select a starting Province." for (let where = 2; where <= 12; ++where) if (is_neutral_province(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]) goto_upkeep() } function goto_upkeep() { // TODO: manually remove Quaestor and Castra? for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i let where = get_governor_location(id) if (is_province(where) && has_quaestor(where)) { log("Removed Quaestor from S" + where) remove_quaestor(where) } if (has_militia_castra(where)) { log("Removed Castra from S" + where) remove_militia_castra(where) } if (has_general_castra(id)) { log("Removed Castra from S" + get_general_location(id)) remove_general_castra(id) } } goto_crisis() } // === CRISIS === // TODO: manual barbarian invasions! function goto_crisis() { game.dice[0] = roll_die() game.dice[1] = roll_die() log(`Crisis B${game.dice[0]} W${game.dice[1]}`) let sum = game.dice[0] + game.dice[1] return goto_take_actions() // XXX if (sum === 2) return goto_ira_deorum() if (sum === 12) return goto_pax_deorum() if (sum === 7) return goto_event() if (game.legacy.length === 2) return goto_barbarian_crisis(CRISIS_TABLE_2P[sum - 2]) if (game.legacy.length === 3) return goto_barbarian_crisis(CRISIS_TABLE_3P[sum - 2]) return goto_barbarian_crisis(CRISIS_TABLE_4P[sum - 2]) } function goto_ira_deorum() { logi("Ira Deorum") activate_one_barbarian(ALAMANNI) activate_one_barbarian(FRANKS) activate_one_barbarian(GOTHS) if (game.legacy.length > 3) activate_one_barbarian(NOMADS) if (game.legacy.length > 2) activate_one_barbarian(SASSANIDS) goto_take_actions() } function goto_pax_deorum() { logi("Pax Deorum") logi("TODO") goto_take_actions() } function goto_event() { logi("Event") logi("TODO") goto_take_actions() } function goto_barbarian_crisis(tribe) { logi(BARBARIAN_NAME[tribe]) activate_one_barbarian(tribe) let black = game.dice[2] = roll_die() let white = game.dice[3] = roll_die() logi(`B${black} W${white}`) if (black <= count_active_barbarians_at_home(tribe)) goto_barbarian_invasion(tribe, black, white) else goto_take_actions() } function invade_with_active_barbarian(tribe, where) { // TODO: move leaders first let b = find_active_barbarian_at_home(tribe) if (b >= 0) set_barbarian_location(tribe * 10 + b, where) } function goto_barbarian_invasion(tribe, black, white) { logi("Invasion!") let path = null for (let list of BARBARIAN_INVASION[tribe]) if (white >= list[0] && white <= list[1]) path = list[2] let k = 0 for (let i = 0; i < black;) { let n = count_barbarians_in_province(tribe, path[k]) if (n < 3) { invade_with_active_barbarian(tribe, path[k]) ++i } else { if (++k > path.length) break } } goto_take_actions() } // === TAKE ACTIONS === function goto_take_actions() { game.state = "take_actions" game.ip = [ 0, 0, 0 ] game.played = [] game.used = [] game.placed = 0 // only place governor once (and no place if recalled) } function prompt(s) { view.prompt = s } function prompt_take_actions(label, sel_gov, sel_gen) { let [ mip, sip, pip ] = game.ip prompt(`${label}: ${mip} Military, ${sip} Senate, ${pip} Populace.`) if (sel_gov >= 0) view.selected_governor = sel_gov if (sel_gen >= 0) view.selected_general = sel_gen for (let c of current_hand()) gen_action_card(c) for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i if (id !== sel_gov) { let where = get_governor_location(id) if ((where === UNAVAILABLE) && (sip >= i)) gen_action_governor(id) if ((where === AVAILABLE) && (sip >= 1)) gen_action_governor(id) if (is_region(where) && (sip >= 2 || pip >= 2)) gen_action_governor(id) } } for (let i = 0; i < 6; ++i) { let id = game.current * 6 + i if (id !== sel_gen) { let where = get_general_location(id) if (where === UNAVAILABLE && mip >= i) gen_action_general(id) if (where === AVAILABLE && mip >= 1) gen_action_general(id) if (is_region(where)) { if (mip >= 1) gen_action_general(id) else if (is_province(where)) { if (is_general_inside_capital(id) || can_enter_capital(where)) gen_action_general(id) } } } } view.actions.end_actions = 1 } function action_take_actions_card(c) { push_undo() let hand = current_hand() if (set_has(hand, c)) { set_delete(hand, c) set_add(game.played, c) add_card_ip(c) } else if (set_has(game.played, c)) { log("TODO - use event") set_delete(game.played, c) set_add(current_discard(), c) } } function action_take_actions_governor(id) { push_undo() game.who = id game.state = "take_actions_governor" } function action_take_actions_general(id) { push_undo() game.who = id game.state = "take_actions_general" } function action_take_actions_end_actions() { push_undo() goto_support_check() } states.take_actions = { prompt() { let player = game.current let [ mip, sip, pip ] = game.ip prompt_take_actions("Take Actions", -1, -1) }, end_actions: action_take_actions_end_actions, card: action_take_actions_card, governor: action_take_actions_governor, general: action_take_actions_general, } states.take_actions_governor = { prompt() { let [ mip, sip, pip ] = game.ip let where = get_governor_location(game.who) prompt_take_actions("Take Governor Actions", game.who, -1) // Recruit Governor if (where === UNAVAILABLE) { if (sip >= game.who % 6) view.actions.recruit = 1 } // Place Governor if (where === AVAILABLE) { if (sip >= 1) view.actions.place = 1 } if (is_province(where)) { // Recall Governor if (sip >= 2) view.actions.recall = 1 // Increase Support Level let support = game.support[where] if (where !== ITALIA && support < 4) { if (pip > support) view.actions.support = 1 } // Place Militia if (!has_militia(where) && is_capital_free_of_enemy(where)) { if (pip >= 2) view.actions.militia = 1 } // Hold Games if (has_mob(where)) { if (pip >= 2) view.actions.hold_games = 1 } // Build an Improvement if (!has_amphitheater(where) || !has_basilica(where) || !has_limes(where)) { if (can_build_improvement(where)) if (pip >= 3) view.actions.build_improvement = 1 } // TODO: Initiate Battle with Militia not stacked with General if (has_militia(where) && !is_own_general(get_capital_general(where))) { if (has_enemy_army_in_province(where)) if (mip >= 1) view.actions.initiate_battle = 1 } } }, end_actions: action_take_actions_end_actions, card: action_take_actions_card, governor: action_take_actions_governor, general: action_take_actions_general, recruit() { push_undo() log("Recruited Governor " + (game.who % 6) + ".") spend_ip(SENATE, game.who % 6) set_governor_location(game.who, AVAILABLE) }, place() { push_undo() spend_ip(SENATE, 1) game.state = "place_governor_where" game.misc = { spend: 1 } }, recall() { push_undo() let where = get_governor_location(game.who) log("Recalled Governor from S" + where + ".") spend_ip(SENATE, 2) set_placed_governor(where) remove_governor(where) update_neutral_italia() }, support() { push_undo() let where = get_governor_location(game.who) let support = game.support[where] log("Built Support in S" + where + ".") spend_ip(POPULACE, support + 1) game.support[where] = support + 1 }, hold_games() { push_undo() let where = get_governor_location(game.who) log("Held Games in S" + where + ".") spend_ip(POPULACE, 3) remove_one_mob(where) }, build_improvement() { push_undo() game.state = "build_improvement" spend_ip(POPULACE, 3) }, initiate_battle() { push_undo() game.state = "initiate_battle" game.misc = { attacker: -1, where: get_governor_location(game.who) } }, } states.take_actions_general = { prompt() { let [ mip, sip, pip ] = game.ip let where = get_general_location(game.who) prompt_take_actions("Take General Actions", -1, game.who) // Recruit General if (where === UNAVAILABLE) { if (mip >= game.who % 6) view.actions.recruit = 1 } // Create Army if (where === AVAILABLE) { if (mip >= 1 && find_unused_legion() >= 0) view.actions.create_army = 1 } if (is_region(where)) { // Add Legion to Army if (is_own_province(where)) { let cost = count_legions_in_army(game.who) + 1 if (mip >= cost) view.actions.add_legion_to_army = 1 } // Train Legions if (has_reduced_legions_in_army(game.who)) { if (mip >= 1) view.actions.train_legions = 1 } // Disperse Mob if (has_mob(where)) { if (mip >= 1) view.actions.disperse_mob = 1 } if (!has_general_battled(game.who)) { // Move Army if (mip >= 1) { for (let to of ADJACENT[where]) { if (!is_sea(to)) gen_move_to_region(to) else if (mip >= 2) gen_move_to_region(to) } } // Initiate Battle if (has_enemy_army_in_province(where)) if (mip >= 1) view.actions.initiate_battle = 1 // Free Action: Enter/Leave Capital if (is_province(where)) { if (is_general_inside_capital(game.who)) { view.actions.leave = 1 } else if (can_enter_capital(where)) { view.actions.enter = 1 gen_action_capital(where) } } } } }, end_actions: action_take_actions_end_actions, card: action_take_actions_card, governor: action_take_actions_governor, general: action_take_actions_general, recruit() { push_undo() log("Recruited General " + (game.who % 6) + ".") spend_ip(MILITARY, game.who % 6) set_general_location(game.who, AVAILABLE) }, create_army() { push_undo() spend_ip(MILITARY, 1) game.state = "create_army" }, add_legion_to_army() { push_undo() log("Added Legion to Army.") let cost = count_legions_in_army(game.who) + 1 spend_ip(MILITARY, cost) set_legion_location(find_unused_legion(), ARMY + game.who) }, train_legions() { push_undo() log("Trained Legions.") spend_ip(MILITARY, 1) set_legion_full_strength(find_reduced_legion_in_army(game.who)) }, region(to) { push_undo() move_army_to(game.who, to, false) }, capital(to) { push_undo() if (get_general_location(game.who) === to) set_general_inside_capital(game.who) else move_army_to(game.who, to, true) }, enter() { push_undo() set_general_inside_capital(game.who) }, leave() { push_undo() set_general_outside_capital(game.who) }, initiate_battle() { push_undo() game.state = "initiate_battle" game.misc = { attacker: game.who, where: get_general_location(game.who) } }, } // ACTION: PLACE GOVERNOR function reduce_support(where) { if (game.support[where] === 1) remove_governor(where) else game.support[where] -= 1 } function increase_support(where) { game.support[where] += 1 } function remove_governor(where) { log("Removed Governor from S" + where) remove_all_mobs(where) let old_governor = get_province_governor(where) if (old_governor >= 0) { set_governor_location(old_governor, AVAILABLE) if (where !== ITALIA && is_emperor(old_governor)) reduce_support(ITALIA) } if (where !== ITALIA) game.support[where] = 1 update_neutral_italia() } function place_governor(where, new_governor) { remove_all_mobs(where) let old_governor = get_province_governor(where) if (old_governor >= 0) { set_governor_location(old_governor, AVAILABLE) if (where !== ITALIA && is_emperor(old_governor)) reduce_support(ITALIA) } set_governor_location(new_governor, where) if (where === ITALIA) game.support[where] = count_own_provinces() else game.support[where] = Math.max(1, game.support[where] - 1) if (where !== ITALIA && is_emperor(new_governor)) increase_support(ITALIA) update_neutral_italia() } function calc_needed_votes(where) { 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) 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) console.log("VOTES", old_player, army_player, game.current) if (army_player === old_player) n += army_size else if (army_player === game.current) n -= army_size } if (has_militia(where)) n += 1 console.log("votes needed", where, n) return Math.max(1, n) } states.place_governor_where = { prompt() { prompt("Place Governor.") view.selected_governor = game.who for (let where = 0; where < 12; ++where) { if (can_place_governor(where)) gen_action_region(where) } }, region(where) { push_undo() game.misc.where = where game.state = "place_governor_spend" }, } states.place_governor_spend = { prompt() { let [ mip, sip, pip ] = game.ip let need = calc_needed_votes(game.misc.where) view.selected_governor = game.who view.selected_region = game.misc.where prompt("Place Governor: " + game.misc.spend + " IP spent; " + need + " votes needed.") if (sip >= 1) view.actions.spend = 1 else view.actions.spend = 0 view.actions.roll = 1 }, spend() { push_undo() spend_ip(SENATE, 1) game.misc.spend += 1 }, roll() { let need = calc_needed_votes(game.misc.where) let have = 0 set_placed_governor(game.misc.where) if (game.misc.where === ITALIA) game.misc.spend += count_own_basilicas() log("Place Governor in S" + game.misc.where) if (is_neutral_province(game.misc.where)) have = roll_dice(game.misc.spend, 1) else if (has_quaestor(game.misc.where)) have = roll_dice(game.misc.spend, 3) else have = roll_dice(game.misc.spend, 2) if (have >= need) { logi("Success!") place_governor(game.misc.where, game.who) } else { logi("Failed!") } game.misc = null game.state = "take_actions_governor" }, } // ACTION: BUILD IMPROVEMENT states.build_improvement = { prompt() { let where = get_governor_location(game.who) view.selected_governor = game.who prompt("Build Improvement.") if (!has_amphitheater(where)) view.actions.amphitheater = 1 if (!has_basilica(where)) view.actions.basilica = 1 if (!has_limes(where)) view.actions.limes = 1 }, amphitheater() { let where = get_governor_location(game.who) add_amphitheater(where) log("Built Amphitheater in S" + where + ".") game.state = "take_actions_governor" }, basilica() { let where = get_governor_location(game.who) add_basilica(where) log("Built Basilica in S" + where + ".") game.state = "take_actions_governor" }, limes() { let where = get_governor_location(game.who) add_limes(where) log("Built Limes in S" + where + ".") game.state = "take_actions_governor" }, } // ACTION: CREATE ARMY states.create_army = { prompt() { prompt("Create Army.") for (let i = 0; i < 6; ++i) { let where = get_governor_location(game.current * 6 + i) if (is_province(where)) gen_action_region(where) } }, region(where) { push_undo() log("Created Army in S" + where + ".") set_general_location(game.who, where) if (can_enter_capital(where)) set_general_inside_capital(game.who) set_legion_location(find_unused_legion(), ARMY + game.who) game.state = "take_actions_general" }, } // ACTION: MOVE ARMY function gen_move_to_region(to) { if (is_province(to) && can_enter_capital(to)) gen_action_capital(to) gen_action_region(to) } states.move_army_at_sea = { prompt() { let [ mip, sip, pip ] = game.ip prompt("Move Army.") view.selected_general = game.who let from = get_general_location(game.who) for (let to of ADJACENT[from]) { if (is_sea(to)) { if (mip >= 2) gen_move_to_region(to) } else { gen_move_to_region(to) } } }, region(to) { push_undo() move_army_to(game.who, to, false) }, capital(to) { push_undo() move_army_to(game.who, to, true) }, } function move_army_to(who, to, go_inside) { log("Moved Army to S" + to + ".") spend_ip(MILITARY, 1) set_general_location(who, to) if (can_enter_capital(to) && go_inside) set_general_inside_capital(who) if (is_sea(to)) game.state = "move_army_at_sea" else game.state = "take_actions_general" } // === ACTION: INITIATE BATTLE === states.initiate_battle = { prompt() { prompt("Initiate Battle in " + REGION_NAME[game.misc.where] + "!") for_each_general((id, loc) => { if (loc === game.misc.where && is_enemy_general(id)) gen_action_general(id) }) for_each_barbarian((id, loc) => { if (loc === game.misc.where) gen_action_barbarian(id) }) if (is_enemy_province(game.misc.where)) { if (has_militia(game.misc.where) && is_capital_free_of_enemy(where)) gen_action_militia(game.misc.where) } }, general(id) { log("Initiate Battle vs Gen" + id) }, barbarian(id) { log("Initiate Battle vs B" + id) }, militia(where) { log("Initiate Battle vs Militia" + id) }, } // === SUPPORT CHECK === function goto_support_check() { goto_expand_pretender_empire() } // === EXPAND PRETENDER EMPIRE === function goto_expand_pretender_empire() { goto_gain_legacy() } // === GAIN LEGACY === function goto_gain_legacy() { goto_buy_trash_cards() } // === BUY / TRASH CARDS === function 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.misc = { count: 0, pp: count_political_points(), } if (current_hand().length > 0) game.state = "buy_trash_discard" else game.state = "buy_trash" } states.buy_trash_discard = { prompt() { prompt("You may discard any number of cards.") for (let c of current_hand()) gen_action_card(c) view.actions.done = 1 }, card(c) { push_undo() set_delete(current_hand(), c) set_add(current_discard(), c) }, done() { push_undo() game.state = "buy_trash" }, } function find_market_with_card(c) { for (let m of game.market) if (set_has(m, c)) return m return null } states.buy_trash = { prompt() { prompt("Buy/Trash cards: " + game.misc.pp + "PP left.") let nprov = count_own_provinces() if (game.misc.pp >= 3) { for (let c of current_discard()) gen_action_card(c) } for (let m of game.market) { if (m.length > 0) { let c = m[0] let cost = card_cost(c) if (cost > nprov) cost *= 2 cost += game.misc.count if (game.misc.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.misc.pp -= 3 } else { log("Bought " + card_name(c)) set_add(current_discard(), c) set_delete(find_market_with_card(c), c) let cost = card_cost(c) if (cost > count_own_provinces()) cost *= 2 cost += game.misc.count game.misc.pp -= cost game.misc.count += 1 } }, done() { push_undo() goto_end_of_turn() }, } // === END OF TURN === function goto_end_of_turn() { // TODO: add mobs // TODO: flip inactive barbarians game.state = "refill_hand" } states.refill_hand = { prompt() { view.prompt = "Refill your 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) if (current_draw().length === 0) { game.draw[game.current] = game.discard[game.current] game.discard[game.current] = [] } }, done() { clear_undo() end_refill_hand() } } function end_refill_hand() { game.current = next_player() goto_start_turn() } // === SETUP === function setup_player_deck(player) { return [ CARD_M1[0] + (player * 3) + 0, CARD_M1[0] + (player * 3) + 1, CARD_M1[0] + (player * 3) + 2, CARD_S1[0] + (player * 3) + 0, CARD_S1[0] + (player * 3) + 1, CARD_S1[0] + (player * 3) + 2, CARD_P1[0] + (player * 3) + 0, CARD_P1[0] + (player * 3) + 1, CARD_P1[0] + (player * 3) + 2, ] } function setup_events() { let deck = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ] shuffle(deck) // Shuffle Diocletian with last 3 cards array_insert(deck, 11 + random(4), 15) return deck } function setup_market_pile(cards) { let pile = [] for (let c = cards[0]; c <= cards[1]; ++c) pile.push(c) return pile } function setup_barbarians(tribe, home) { for (let i = 0; i < 10; ++i) { set_barbarian_location(tribe * 10 + i, home) set_barbarian_inactive(tribe * 10 + i) } } exports.setup = function (seed, scenario, options) { let real_player_count = options.players || 4 let player_count = real_player_count if (player_count === 1) player_count = 4 let tribe_count = TRIBE_COUNT[player_count] game = { seed: seed, log: [], undo: [], active: 0, current: 0, state: "setup_province", first: 0, events: null, active_events: [], ip: [], who: -1, played: [], used: [], placed: 0, battled: 0, // grab bag of temporary data for the current procedure misc: null, support: new Array(12).fill(1), mobs: new Array(12).fill(0), militia: 0, quaestor: 0, castra: 0, mcastra: 0, amphitheater: 0, basilica: 0, limes: 0, breakaway: 0, seat_of_power: 0, governors: new Array(6 * player_count).fill(UNAVAILABLE), generals: new Array(6 * player_count).fill(UNAVAILABLE), legions: new Array(LEGION_COUNT).fill(AVAILABLE), barbarians: new Array(10 * tribe_count).fill(0), rival_emperors: [ UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ], barbarian_leaders: [ UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ], dice: [ 0, 0, 0, 0 ], // first two are crisis table dice, second two are barbarian homeland dice market: null, // per-player data legacy: new Array(player_count).fill(0), emperor_turns: new Array(player_count).fill(0), hand: [], draw: [], discard: [], } if (real_player_count === 1) game.solo = 1 game.events = setup_events() game.market = [ setup_market_pile(CARD_M2), setup_market_pile(CARD_S2), setup_market_pile(CARD_P2), setup_market_pile(CARD_M3), setup_market_pile(CARD_S3), setup_market_pile(CARD_P3), setup_market_pile(CARD_M4), setup_market_pile(CARD_S4), setup_market_pile(CARD_P4), ] setup_barbarians(ALAMANNI, ALAMANNI_HOMELAND) setup_barbarians(FRANKS, FRANKS_HOMELAND) setup_barbarians(GOTHS, GOTHS_HOMELAND) if (player_count >= 3) setup_barbarians(NOMADS, NOMADS_HOMELAND) if (player_count >= 4) setup_barbarians(SASSANIDS, SASSANIDS_HOMELAND) for (let player = 0; player < player_count; ++player) { game.hand[player] = [] game.draw[player] = setup_player_deck(player) game.discard[player] = [] } update_neutral_italia() game.first = game.current = random(player_count) log("First Player is " + PLAYER_NAMES[game.first] + "!") return save_game() } function load_game(state) { game = state } function save_game() { if (game.solo) game.active = "Solo" else game.active = PLAYER_NAMES[game.current] return game } exports.action = function (state, player, action, arg) { load_game(state) let S = states[game.state] if (action in S) { S[action](arg) } else { if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else throw new Error("Invalid action: " + action) } return save_game() } function is_current_player(player) { if (player === 4) return true return game.current === player } exports.view = function (state, player_name) { load_game(state) let player = PLAYER_INDEX[player_name] if (game.solo) player = game.current let player_count = game.legacy.length view = { log: game.log, current: game.current, prompt: null, support: game.support, mobs: game.mobs, militia: game.militia, quaestor: game.quaestor, castra: game.castra, amphitheater: game.amphitheater, basilica: game.basilica, limes: game.limes, governors: game.governors, generals: game.generals, legions: game.legions, barbarians: game.barbarians, rival_emperors: game.rival_emperors, barbarian_leaders: game.barbarian_leaders, dice: game.dice, events: game.active_events, played: game.played, market: game.market.map(m => m[0] | 0), legacy: game.legacy, emperor_turns: game.emperor_turns, } if (game.state === "game_over") { view.prompt = game.victory } else if (player !== game.current && player_name !== "Solo") { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${PLAYER_NAMES[game.current]} \u2014 ${inactive}...` } else { view.actions = {} states[game.state].prompt() if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } if (player >= 0 && player < player_count) { view.hand = game.hand[player] view.draw = game.draw[player] view.discard = game.discard[player] } save_game() return view } // === MISC === function log(msg) { game.log.push(msg) } function log_br() { if (game.log.length > 0 && game.log[game.log.length - 1] !== "") game.log.push("") } function log_h1(msg) { log_br() log(".h1 " + msg) log_br() } function log_h2(msg) { log_br() log(".h2 " + msg) log_br() } function logi(msg) { game.log.push(">" + msg) } function logii(msg) { game.log.push(">>" + msg) } // === COMMON LIBRARY === function roll_die() { 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(ix) { gen_action("general", ix) } function gen_action_governor(ix) { gen_action("governor", ix) } function gen_action_legion(ix) { gen_action("legion", ix) } function gen_action_barbarian(ix) { gen_action("barbarian", ix) } function gen_action_region(where) { gen_action("region", where) } function gen_action_capital(where) { gen_action("capital", where) } 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 } } }