"use strict" // === CONSTANTS AND DATA === const P1 = "Red" const P2 = "Blue" const P3 = "Yellow" const P4 = "Green" const PLAYER_NAMES = [ P1, P2, P3, P4 ] const PLAYER_INDEX = { [P1]: 0, [P2]: 1, [P3]: 2, [P4]: 3, "Observer": -1, } const NO_PLACE_GOVERNOR = -1 const OFF_MAP = -1 const AVAILABLE = -1 const UNAVAILABLE = -2 const ITALIA = 0 const AEGYPTUS = 1 const AFRICA = 2 const ASIA = 3 const BRITANNIA = 4 const GALATIA = 5 const GALLIA = 6 const HISPANIA = 7 const MACEDONIA = 8 const PANNONIA = 9 const SYRIA = 10 const THRACIA = 11 const ALAMANNI_HOMELAND = 12 const FRANKS_HOMELAND = 13 const GOTHS_HOMELAND = 14 const NOMADS_HOMELAND = 15 const SASSANIDS_HOMELAND = 16 const ARMY = [ [ 100, 101, 102, 103, 104, 105 ], [ 200, 201, 202, 203, 204, 205 ], [ 300, 301, 302, 303, 304, 305 ], [ 400, 401, 402, 403, 404, 405 ] ] const REGION_NAME = [ "Italia", "Aegyptus", "Africa", "Asia", "Britannia", "Galatia", "Gallia", "Hispania", "Macedonia", "Pannonia", "Syria", "Thracia", "Alamanni Homeland", "Franks Homeland", "Goths Homeland", "Nomads Homeland", "Sassanids Homeland", ] const ALAMANNI = 0 const FRANKS = 1 const GOTHS = 2 const NOMADS = 3 const SASSANIDS = 4 const BARBARIAN_NAME = [ "Alamanni", "Franks", "Goths", "Nomads", "Sassanids", ] const ALAMANNI_UNITS = [ 0, 9 ] const FRANKS_UNITS = [ 10, 19 ] const GOTHS_UNITS = [ 20, 29 ] const NOMADS_UNITS = [ 30, 39 ] const SASSANIDS_UNITS = [ 40, 49 ] const LEGION_UNITS = [ 50, 82 ] const BARBARIAN_UNITS = [ ALAMANNI_UNITS, FRANKS_UNITS, GOTHS_UNITS, NOMADS_UNITS, SASSANIDS_UNITS, ] const BARBARIAN_HOMELAND = [ ALAMANNI_HOMELAND, FRANKS_HOMELAND, GOTHS_HOMELAND, NOMADS_HOMELAND, SASSANIDS_HOMELAND, ] // 12x const CARD_M1 = [ 1, 12 ] const CARD_S1 = [ 13, 24 ] const CARD_P1 = [ 25, 36 ] // 9x const CARD_M2 = [ 37, 45 ] const CARD_S2 = [ 46, 54 ] const CARD_P2 = [ 55, 63 ] // 8x const CARD_M3 = [ 64, 71 ] const CARD_S3 = [ 72, 79 ] const CARD_P3 = [ 80, 87 ] // 6x const CARD_M4 = [ 88, 93 ] const CARD_S4 = [ 94, 99 ] const CARD_P4 = [ 100, 105 ] const EVENT_PLAGUE_OF_CYPRIAN = 1 const EVENT_ARDASHIR = 2 const EVENT_PRIEST_KING = 3 const EVENT_PALMYRA_ALLIES = 4 const EVENT_SHAPUR_I = 5 const EVENT_POSTUMUS = 6 const EVENT_LUDI_SAECULARES = 7 const EVENT_CNIVA = 8 const EVENT_ZENOBIA = 9 const EVENT_BAD_AUGURIES = 10 const EVENT_RAIDING_PARTIES = 11 const EVENT_PREPARING_FOR_WAR = 12 const EVENT_INFLATION = 13 const EVENT_GOOD_AUGURIES = 14 const EVENT_DIOCLETIAN = 15 const CRISIS_TABLE_4P = [ 0, 0, "Ira Deorum", SASSANIDS, FRANKS, SASSANIDS, GOTHS, "Event", ALAMANNI, NOMADS, FRANKS, NOMADS, "Pax Deorum" ] const CRISIS_TABLE_3P = [ 0, 0, "Ira Deorum", FRANKS, SASSANIDS, SASSANIDS, FRANKS, "Event", ALAMANNI, GOTHS, GOTHS, ALAMANNI, "Pax Deorum" ] const CRISIS_TABLE_2P = [ 0, 0, "Ira Deorum", FRANKS, ALAMANNI, FRANKS, GOTHS, "Event", GOTHS, FRANKS, ALAMANNI, ALAMANNI, "Pax Deorum" ] // === var game var view const states = {} 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 ] } function setup_player_deck(pi) { return [ CARD_M1[0] + (pi * 3) + 0, CARD_M1[0] + (pi * 3) + 1, CARD_M1[0] + (pi * 3) + 2, CARD_S1[0] + (pi * 3) + 0, CARD_S1[0] + (pi * 3) + 1, CARD_S1[0] + (pi * 3) + 2, CARD_P1[0] + (pi * 3) + 0, CARD_P1[0] + (pi * 3) + 1, CARD_P1[0] + (pi * 3) + 2, ] } 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) { for (let i = BARBARIAN_UNITS[0]; i <= BARBARIAN_UNITS[1]; ++i) game.barbarians[i] = BARBARIAN_HOMELAND[i] } function remove_barbarians(tribe) { for (let i = BARBARIAN_UNITS[0]; i <= BARBARIAN_UNITS[1]; ++i) game.barbarians[i] = OFF_MAP } exports.setup = function (seed, scenario, options) { let player_count = options.players || 4 game = { seed: seed, log: [], undo: [], active: 0, state: "none", players: [], events: setup_events(), active_events: [], has_militia: new Array(12).fill(0), support: new Array(12).fill(1), legions: new Array(33).fill(OFF_MAP), is_legion_reduced: new Array(33).fill(0), barbarians: new Array(50).fill(OFF_MAP), is_barbarian_inactive: new Array(50).fill(0), barbarian_leaders: [ OFF_MAP, OFF_MAP, OFF_MAP ], rival_emperors: [ OFF_MAP, OFF_MAP, OFF_MAP ], 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) setup_barbarians(FRANKS) setup_barbarians(GOTHS) setup_barbarians(NOMADS) setup_barbarians(SASSANIDS) for (let pi = 0; pi < player_count; ++pi) { game.players[pi] = { legacy: 0, emperor_turns: 0, hand: [], draw: setup_player_deck(pi), discard: [], generals: [ AVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ], governors: [ AVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE, UNAVAILABLE ], } } if (player_count === 4) { game.support[ITALIA] = 8 } if (player_count === 3) { game.support[ITALIA] = 6 game.support[HISPANIA] = NO_PLACE_GOVERNOR game.support[AFRICA] = NO_PLACE_GOVERNOR game.support[AEGYPTUS] = NO_PLACE_GOVERNOR remove_barbarians(NOMADS) } if (player_count === 2) { game.support[ITALIA] = 4 game.support[BRITANNIA] = NO_PLACE_GOVERNOR game.support[HISPANIA] = NO_PLACE_GOVERNOR game.support[AFRICA] = NO_PLACE_GOVERNOR game.support[AEGYPTUS] = NO_PLACE_GOVERNOR game.support[SYRIA] = NO_PLACE_GOVERNOR game.support[GALATIA] = NO_PLACE_GOVERNOR remove_barbarians(NOMADS) remove_barbarians(SASSANIDS) } return save_game() } function load_game(state) { game = state game.active = PLAYER_INDEX[game.active] } function save_game() { game.active = PLAYER_NAMES[game.active] 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() } exports.view = function (state, player_name) { let player = PLAYER_INDEX[player_name] load_game(state) view = { log: game.log, active: PLAYER_NAMES[game.active], prompt: null, } if (game.state === "game_over") { view.prompt = game.victory } else if (game.active !== player) { let inactive = states[game.state].inactive || game.state view.prompt = `Waiting for ${PLAYER_NAMES[game.active]} \u2014 ${inactive}...` } else { view.actions = {} states[game.state].prompt() view.prompt = PLAYER_NAMES[game.active] + ": " + view.prompt if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } save_game() return view } // === MISC === function log(msg) { game.log.push(msg) } // === STATES === states.none = { prompt() {} } // === COMMON LIBRARY === 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 } } }