"use strict" let states = {} let game = null let view = null /* DATA */ const data = require("./data.js") const space_name = data.space_name function get_space_id(name) { return space_name.indexOf(name); } const S_ANDHRA = get_space_id("Andhra") const S_BENGAL = get_space_id("Bengal") const S_GONDWANA = get_space_id("Gondwana") const S_GUJARAT = get_space_id("Gujarat") const S_JAUNPUR = get_space_id("Jaunpur") const S_KARNATAKA = get_space_id("Karnataka") const S_MADHYADESH = get_space_id("Madhyadesh") const S_MAHARASHTRA = get_space_id("Maharashtra") const S_MALWA = get_space_id("Malwa") const S_ORISSA = get_space_id("Orissa") const S_RAJPUT_KINGDOMS = get_space_id("Rajput Kingdoms") const S_SINDH = get_space_id("Sindh") const S_TAMILAKAM = get_space_id("Tamilakam") const S_DELHI = get_space_id("Delhi") const S_MOUNTAIN_PASSES = get_space_id("Mountain Passes") const S_PUNJAB = get_space_id("Punjab") const S_MONGOL_INVADERS = 16 const S_DS_AVAILABLE = 17 const S_BK_AVAILABLE = 18 const S_VE_AVAILABLE = 19 const S_BK_INF_2 = 20 const S_BK_INF_4 = 21 const S_VE_INF_1 = 22 const S_VE_INF_2 = 23 const S_VE_INF_3 = 24 const S_VE_INF_4 = 25 const faction_name = [ "Delhi Sultanate", "Bahmani Kingdom", "Vijayanagara Empire", "Mongol Invaders" ] exports.scenarios = [ "Standard", "Solo" ] exports.roles = function (scenario, _options) { if (scenario === "Solo") return [ NAME_SOLO ] return [ NAME_DS, NAME_BK, NAME_VE ] } function is_current_role(role) { if (role === NAME_SOLO) return true if (role === NAME_DS) return game.current === DS if (role === NAME_BK) return game.current === BK if (role === NAME_VE) return game.current === VE return false } function load_game(state) { game = state } function save_game() { if (game.solo) { game.active = NAME_SOLO return game } if (game.current === DS) game.active = NAME_DS if (game.current === BK) game.active = NAME_BK if (game.current === VE) game.active = NAME_VE return game } exports.view = function (state, role) { load_game(state) let this_card = game.deck[0] | 0 let deck_size = Math.max(0, game.deck.length - 1) view = { prompt: null, actions: null, log: game.log, current: game.current, vp: game.vp, resources: game.resources, bk_inf: 0, ve_inf: 0, deck: [ this_card, deck_size, game.of_gods_and_kings ], pieces: game.pieces, } if (game.result) { view.prompt = game.victory } else if (!is_current_role(role)) { let inactive = states[game.state].inactive if (!inactive) { if (game.vm) inactive = "Event" else inactive = game.state } view.prompt = `Waiting for ${faction_name[game.current]} \u2014 ${inactive}.` } else { view.actions = {} if (states[game.state]) states[game.state].prompt() else view.prompt = "Unknown state: " + game.state if (states[game.state] && !states[game.state].disable_negotiation) { view.actions.ask_resources = 1 if (game.resources[game.current] > 0) view.actions.transfer_resources = 1 else view.actions.transfer_resources = 0 if (game.cavalry[game.current] > 0) view.actions.transfer_cavalry = 1 else view.actions.transfer_cavalry = 0 if (true) // TODO: can_ask_cavalry() view.actions.ask_cavalry = 1 else view.actions.ask_cavalry = 0 } if (view.actions.undo === undefined) { if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } } save_game() return view } exports.action = function (state, role, action, arg) { load_game(state) let S = states[game.state] if (S && action in S) { S[action](arg) } else { if (action === "undo" && game.undo && game.undo.length > 0) pop_undo() else if (action === "ask_resources") action_ask_resources() else if (action === "ask_cavalry") action_ask_cavalry() else if (action === "transfer_resources") action_transfer_resources() else if (action === "transfer_cavalry") action_transfer_cavalry() else throw new Error("Invalid action: " + action) } return save_game() } /* SETUP */ exports.setup = function (seed, scenario, _options) { game = { seed, log: [], undo: [], active: null, current: 0, state: null, cylinder: [ ELIGIBLE, ELIGIBLE, ELIGIBLE ], resources: [ 12, 6, 7 ], vp: [ 18, 0, 0 ], bk_inf: 0, ve_inf: 0, tributary: 8191, // all 13 provinces rebel: 0, // amir/raja rebel status pieces: Array(103).fill(AVAILABLE), // piece locations deck: [], } if (scenario === "Solo") game.solo = 1 // TODO: setup start pieces // goto_card() game.current = DS game.state = "todo" // Setup setup_standard() return save_game() } function setup_standard() { setup_piece(DS, DISC, 1, "S_ANDHRA") setup_piece(DS, ELITE, 1, S_MALWA) setup_piece(DS, TROOPS, 4, S_DELHI) setup_piece(DS, TROOPS, 4, S_PUNJAB) setup_piece(DS, TROOPS, 3, S_MALWA) setup_piece(DS, TROOPS, 2, S_JAUNPUR) setup_piece(DS, TROOPS, 2, S_MADHYADESH) setup_piece(DS, TROOPS, 2, S_MOUNTAIN_PASSES) setup_piece(DS, TROOPS, 1, S_ANDHRA) setup_piece(DS, TROOPS, 1, S_GUJARAT) setup_piece(DS, TROOPS, 1, S_RAJPUT_KINGDOMS) setup_piece(DS, TROOPS, 1, S_SINDH) setup_piece(DS, TROOPS, 1, S_TAMILAKAM) setup_piece(BK, ELITE, 1, S_GONDWANA) setup_piece(BK, ELITE, 1, S_GUJARAT) setup_piece(BK, ELITE, 2, S_MADHYADESH) setup_piece(BK, ELITE, 4, S_MAHARASHTRA) setup_piece(VE, ELITE, 1, S_TAMILAKAM) setup_piece(VE, ELITE, 2, S_ANDHRA) setup_piece(VE, ELITE, 3, S_KARNATAKA) } function setup_piece(faction, type, count, where) { for (let p = data.first_piece[faction][type]; count > 0; ++p) { if (piece_space(p) < 0) { set_piece_space(p, where) --count } } } /* LOGGING */ function log(msg) { game.log.push(msg) } function logi(msg) { log(">" + 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_action(msg) { log_br() log(msg) } function log_transfer(msg) { log_br() log(".n " + msg) log_br() } function log_transfer_resources(from, to, n) { log_transfer(`${faction_name[from]} gave ${n} Resources to ${faction_name[to]}.`) } function log_space(s, action) { if (action) log_action("S" + s + " - " + action) else log_action("S" + s) } function push_summary() { if (game.summary) throw "TOO MANY SUMMARIES" game.summary = [] } function log_summary(msg) { for (let item of game.summary) { if (item[1] === msg) { item[0]++ return } } game.summary.push([1, msg]) } function pop_summary() { if (game.summary.length > 0) { for (let [n, msg] of game.summary) { log(">" + msg.replace("%", String(n))) } } else { log(">Nothing") } game.summary = null } function log_summary_place(p) { let from = piece_space(p) if (from !== AVAILABLE) log_summary("% " + piece_name(p) + " from S" + from) else log_summary("% " + piece_name(p)) } function log_summary_move_to_from(p, to) { log_summary("% " + piece_name(p) + " to S" + to + " from S" + piece_space(p)) } function log_summary_remove(p) { log_summary("Removed % " + piece_name(p)) } // === MISC PIECE QUERIES === function piece_space(p) { return game.pieces[p] } function set_piece_space(p, s) { game.pieces[p] = s } /* COMMON LIBRARY */ function clear_undo() { if (game.undo) { game.undo.length = 0 } } function push_undo() { if (game.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() { if (game.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_remove_item(array, item) { let n = array.length for (let i = 0; i < n; ++i) if (array[i] === item) return array_remove(array, i) } 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, 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 { array_remove_pair(map, m<<1) return } } } function object_diff(a, b) { if (a === b) return false if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { if (Array.isArray(a)) { if (!Array.isArray(b)) return true let a_length = a.length if (b.length !== a_length) return true for (let i = 0; i < a_length; ++i) if (object_diff(a[i], b[i])) return true return false } for (let key in a) if (object_diff(a[key], b[key])) return true for (let key in b) if (!(key in a)) return true return false } return true } // same as Object.groupBy function object_group_by(items, callback) { let groups = {} if (typeof callback === "function") { for (let item of items) { let key = callback(item) if (key in groups) groups[key].push(item) else groups[key] = [ item ] } } else { for (let item of items) { let key = item[callback] if (key in groups) groups[key].push(item) else groups[key] = [ item ] } } return groups } // === CONST === // Factions const DS = 0 const BK = 1 const VE = 2 const MI = 3 // Role names const NAME_DS = "DS" const NAME_BK = "BK" const NAME_VE = "VE" const NAME_SOLO = "Solo" // Player pieces types const DISC = 0 const ELITE = 1 const TROOPS = 2 // Pieces status const AVAILABLE = -1 const OUT_OF_PLAY = -2 const ANY_PIECES = [ DISC, ELITE, TROOPS ] const PIECE_FACTION_TYPE_NAME = [ [ "Qasbah", "Governors", "Troops" ], [ "Fort", "Amirs", null ], [ "Temple", "Rajas", null ], [ null, null, "Invaders"] ] // Sequence of Play options const ELIGIBLE = 0 const SOP_LIMITED_COMMAND = 1 const SOP_COMMAND_DECREE = 2 const SOP_EVENT_OR_COMMAND = 3 const SOP_PASS = 4 const INELIGIBLE = 5 // === CONST ===