"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 first_piece = data.first_piece const last_piece = data.last_piece const first_space = S_ANDHRA const last_space = S_PUNJAB const last_province = S_TAMILAKAM 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 ], cylinder: game.cylinder, pieces: game.pieces, tributary: game.tributary, control: game.control, rebel: game.rebel, } 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 control: [0, 0, 0], rebel: [null, 0, 0], // amir/raja rebel status pieces: Array(104).fill(AVAILABLE), // piece locations cavalry: [0, 0, 0], deck: [], op: { type: null, limited: null, free: null, spaces: [], where: null, }, decree: 0 } if (scenario === "Solo") game.solo = 1 // TODO: setup start pieces // goto_card() game.current = DS game.state = "todo" // Setup setup_standard() setup_deck() goto_card() return save_game() } function setup_deck() { game.deck = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ] } 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(BK, ELITE, 2, S_BK_INF_2) setup_piece(BK, ELITE, 2, S_BK_INF_4) setup_piece(VE, ELITE, 1, S_TAMILAKAM) setup_piece(VE, ELITE, 2, S_ANDHRA) setup_piece(VE, ELITE, 3, S_KARNATAKA) setup_piece(VE, ELITE, 2, S_VE_INF_1) setup_piece(VE, ELITE, 2, S_VE_INF_2) setup_piece(VE, ELITE, 2, S_VE_INF_3) setup_piece(VE, ELITE, 2, S_VE_INF_4) } function setup_piece(faction, type, count, where) { for (let p = first_piece[faction][type]; count > 0; ++p) { if (piece_space(p) < 0) { set_piece_space(p, where) --count } } } // === SEQUENCE OF PLAY === function this_card() { return game.deck[0] } function goto_card() { log_h1("C" + this_card()) if (this_card() === "todo") console.log("todo") else resume_event_card() } function resume_event_card() { clear_undo() if (game.cylinder.includes(ELIGIBLE)) goto_eligible() else end_card() } function end_card() { clear_undo() adjust_eligibility(DS) adjust_eligibility(BK) adjust_eligibility(VE) array_remove(game.deck, 0) goto_card() } function goto_eligible() { game.current = next_eligible_faction() if (game.current < 0) { end_card() } else { game.state = "eligible" game.op = { limited: 0, free: 0, spaces: [], where: -1, pass: 1 } } } function is_eligible(faction) { return game.cylinder[faction] === ELIGIBLE } function did_option(e) { return ( game.cylinder[DS] === e || game.cylinder[BK] === e || game.cylinder[VE] === e ) } function next_eligible_faction() { let order = data.card_order[this_card()] for (let faction of order) if (is_eligible(faction)) return faction return -1 } function adjust_eligibility(faction) { if (game.cylinder[faction] === INELIGIBLE || game.cylinder[faction] === SOP_PASS || game.cylinder[faction] === SOP_LIMITED_COMMAND) game.cylinder[faction] = ELIGIBLE else if (game.cylinder[faction] !== ELIGIBLE) game.cylinder[faction] = INELIGIBLE } function goto_pass() { push_undo() game.op = 0 game.sa = 0 game.cylinder[game.current] = SOP_PASS log_h2(faction_name[game.current] + " - Pass") if (game.current === DS) { log_resources(game.current, 3) add_resources(game.current, 3) } else { log_resources(game.current, 1) add_resources(game.current, 1) } resume_event_card() } function goto_build() { init_command("Build") game.state = "build" } function goto_conscript() { init_command("Conscript") game.state = "conscript" } function goto_conscript_space() { push_summary() if (conscript_count() === 1) { let p = find_piece(AVAILABLE, game.current, TROOPS) log_summary_place(p) place_piece(p, game.op.where) end_conscript_space() } else { game.op.count = 0 game.state = "conscript_space" } } function end_conscript_space() { log_space(game.op.where, "Conscript") pop_summary() game.state = "conscript" } function goto_rally() { init_command("Rally") game.state = "rally" } function goto_rally_space() { push_summary() if (rally_count() === 1) { let p = find_piece(AVAILABLE, game.current, ELITE) log_summary_place(p) place_piece(p, game.op.where) end_rally_space() } else { game.op.count = 0 game.state = "rally_space" } } function end_rally_space() { log_space(game.op.where, "Rally") pop_summary() game.state = "rally" } function goto_rebel() { init_command("Rebel") game.state = "rebel" } /* STATES */ states.eligible = { disable_negotiation: true, inactive: "Eligible Faction", prompt() { if (did_option(SOP_COMMAND_DECREE)) { view.prompt = `${data.card_title[this_card()]}: Event or Command.` view.actions.event_command = 1 } else if (did_option(SOP_EVENT_OR_COMMAND)) { view.prompt = `${data.card_title[this_card()]}: Command & Decree.` view.actions.command_decree = 1 } else { view.prompt = `${data.card_title[this_card()]}: Event or Command & Decree.` view.actions.command_decree = 1 view.actions.event_command = 1 } view.actions.lim_command = 1 view.actions.pass = 1 view.actions.undo = 0 }, pass: goto_pass, command_decree() { push_undo() game.cylinder[game.current] = SOP_COMMAND_DECREE game.decree = 1 game.state = "command_decree" }, event_command() { push_undo() game.cylinder[game.current] = SOP_EVENT_OR_COMMAND game.state = "event_command" }, lim_command() { push_undo() game.cylinder[game.current] = SOP_LIMITED_COMMAND game.op.limited = 1 game.state = "lim_command" }, } states.command_decree = { inactive: "Command & Decree", prompt() { view.prompt = "Select a Command and a Decree!" gen_any_command() if (game.decree === 1) gen_any_decree() }, build: goto_build, conscript: goto_conscript, rally: goto_rally, rebel: goto_rebel, } states.event_command = { inactive: "Event or Command", prompt() { view.prompt = "Select the card Event or a Command!" gen_any_command() }, conscript: goto_conscript, rally: goto_rally, rebel: goto_rebel, } states.lim_command = { inactive: "Limited command", prompt() { view.prompt = "Select a limited Command!" gen_any_command() }, conscript: goto_conscript, rally: goto_rally, rebel: goto_rebel, } states.build = { prompt() { if (game.current === BK) view.prompt = "Build: Select a Province with an Amir" else if (game.current === VE) view.prompt = "Build: Select a Province with a Raja" if (can_build()) { for (let s = first_space; s <= last_space; ++s) { if (can_build_in_space(s)) gen_action_space(s) } } }, space(s) { push_undo() push_summary() let p = find_piece(AVAILABLE, game.current, DISC) log_summary_place(p) place_piece(p, s) log_space(s, "Build") pop_summary() game.decree = 0 game.state = "command_decree" } } states.conscript = { prompt() { view.prompt = "Conscript: Select Tributaries, Qasbah or Dehli to place troops." if (can_select_cmd_space(1) && can_conscript()) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_conscript_in_space(s)) gen_action_space(s) } } view.actions.end_conscript = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) goto_conscript_space() }, end_conscript: end_command, } states.conscript_space = { prompt() { view.prompt = `Conscript: Place up to ${conscript_count()} Troops.` view.where = game.op.where view.actions.next = 1 gen_place_piece(DS, TROOPS) }, piece(p) { log_summary_place(p) place_piece(p, game.op.where) if (++game.op.count >= conscript_count()) end_conscript_space() }, next() { end_conscript_space() } } states.rally = { prompt() { if (game.current === BK) view.prompt = "Rally: Select Maharashtra or a Province with your presence" else if (game.current === VE) view.prompt = "Rally: Select Karnataka or a Province with your presence" if (can_select_cmd_space(1) && can_rally()) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_rally_in_space(s)) gen_action_space(s) } } view.actions.end_rally = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) goto_rally_space() }, end_rally: end_command, } states.rally_space = { prompt() { view.prompt = `Rally: Place up to ${rally_count()} ${PIECE_FACTION_TYPE_NAME[game.current][ELITE]}` view.where = game.op.where view.actions.next = 1 gen_place_piece(game.current, ELITE) }, piece(p) { log_summary_place(p) place_piece(p, game.op.where) if (++game.op.count >= rally_count()) end_rally_space() }, next() { end_rally_space() } } states.rebel = { prompt() { view.prompt = "Rebel: Select a Province where you are in majority." if (can_select_cmd_space(1)) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_rebel_in_space(s)) gen_action_space(s) } } view.actions.end_rebel = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) remove_tributary(s) to_rebel_space(s, game.current) log_space(s, "Rebel") }, end_rebel: end_command, } /* COMMANDS */ function init_command(type) { push_undo() if (game.op.limited) log_h2(faction_name[game.current] + " - Limited " + type) else log_h2(faction_name[game.current] + " - " + type) game.op.type = type } function gen_any_command() { if (game.current === DS) { view.actions.conscript = can_conscript() ? 1 : 0 } else if (game.current === BK || game.current === VE) { view.actions.rally = can_rally() ? 1 : 0 view.actions.rebel = can_rebel() ? 1 : 0 } } function can_select_cmd_space(cost) { if (!game.op.free && game.resources[game.current] < cost) return false if (game.op.limited) return game.op.spaces.length === 0 return true } function select_cmd_space(s, cost) { set_add(game.op.spaces, s) if (!game.op.free) game.resources[game.current] -= cost game.op.where = s } function can_conscript() { return has_piece(AVAILABLE, DS, TROOPS) } function can_conscript_in_space(s) { if (s === S_DELHI || has_piece(s, DS, DISC) || is_tributary(s)) return true } function conscript_count() { if (game.op.where === S_DELHI) return 5 if (has_piece(game.op.where, DS, DISC)) { return 2 } return 1 } function can_rally() { return has_piece(AVAILABLE, game.current, ELITE) } function can_rally_in_space(s) { if (game.current === BK) { return (s === S_MAHARASHTRA || has_piece(s, BK, ELITE) || has_piece(s, BK, DISC)) } else if (game.current === VE) { return (s === S_KARNATAKA || has_piece(s, VE, ELITE) || has_piece(s, VE, DISC)) } } function rally_count() { // TODO : Add rally with control + 1 let count = 1 if (game.current === BK) { count += (game.op.where === S_MAHARASHTRA ? 1 : 0) } else if (game.current === VE) { count += (game.op.where === S_KARNATAKA ? 1 : 0) count += (has_piece(game.op.where, VE, DISC) ? 1 : 0) } return count } function can_rebel() { // todo: implement dynasty logic for (let s = first_space; s <= last_space; ++s) { if (can_rebel_in_space(s)) return true } return false } function can_rebel_in_space(s) { if (is_tributary(s) && has_majority(s) === game.current) return true return false } function prompt_end_cmd(cost) { if (!view.actions.space) { if (game.op.limited && game.op.spaces && game.op.spaces.length > 0) view.prompt = game.op.type + ": Done." else if (!game.op.free && game.resources[game.current] < cost) view.prompt = game.op.type + ": No resources." else view.prompt = game.op.type + ": Done." } return (game.op.spaces.length > 0) ? 1 : 0 } function end_command() { log_br() game.op = null resume_event_card() } /* DECREES */ function gen_any_decree() { if (game.current === DS) { // view.actions.conscript = can_conscript() ? 1 : 0 } else if (game.current === BK || game.current === VE) { view.actions.build = can_build() ? 1 : 0 } } function can_build() { return has_piece(AVAILABLE, game.current, DISC) } function can_build_in_space(s) { return has_piece(s, game.current, ELITE) } /* TRIBUTARY AND REBELS */ function add_tributary(s) { game.tributary |= (1 << s) update_control() } function is_tributary(s) { return game.tributary & (1 << s) } function remove_tributary(s) { game.tributary &= ~(1 << s) update_control() } function to_obedient(p) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] game.rebel[faction] |= ~(1 << piece_index) } function to_rebel(p) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] console.log("with p ", p) console.log("with piece_index ", piece_index) console.log(game.rebel) game.rebel[faction] |= (1 << piece_index) console.log(game.rebel) } function to_rebel_space(s, faction) { let first = first_piece[faction][ELITE] let last = last_piece[faction][ELITE] for (let p = first; p <= last; ++p) if (piece_space(p) === s) to_rebel(p) } /* MISC SPACE + PIECE QUERIES */ function count_pieces(s, faction, type) { let first = first_piece[faction][type] let last = last_piece[faction][type] let n = 0 for (let p = first; p <= last; ++p) if (piece_space(p) === s) ++n return n } function count_faction_pieces(s, faction) { switch (faction) { case DS: return count_pieces(s, DS, TROOPS) + count_pieces(s, DS, ELITE) + count_pieces(s, DS, DISC) case BK: return count_pieces(s, BK, ELITE) + count_pieces(s, BK, DISC) case VE: return count_pieces(s, VE, ELITE) + count_pieces(s, VE, DISC) case MI: return count_pieces(s, MI, TROOPS) } } function has_majority(s) { let d = count_faction_pieces(s, DS) let b = count_faction_pieces(s, BK) let v = count_faction_pieces(s, VE) let m = count_faction_pieces(s, MI) if (d > b + v + m) return DS else if (b > d + v + m) return BK else if (v > b + d + m) return VE else if (m > b + d+ v) return MI else return -1 } function has_piece(s, faction, type) { let first = first_piece[faction][type] let last = last_piece[faction][type] for (let p = first; p <= last; ++p) if (piece_space(p) === s) return true return false } function find_piece(s, faction, type) { let first = first_piece[faction][type] let last = last_piece[faction][type] for (let p = first; p <= last; ++p) if (piece_space(p) === s) return p return -1 } function gen_place_piece(faction, type) { let p0 = first_piece[faction][type] let p1 = last_piece[faction][type] let can_place = false for (let p = p0; p <= p1; ++p) { if (piece_space(p) === AVAILABLE) { gen_action_piece(p) can_place = true if (type === DISC) break } } return can_place } function is_selected_cmd_space(s) { return game.op.spaces && set_has(game.op.spaces, s) } function place_piece(p, s) { if (piece_space(p) === AVAILABLE) p = find_piece(AVAILABLE, piece_faction(p), piece_type(p)) set_piece_space(p, s) update_control() } function piece_faction(p) { if (p >= first_piece[MI][TROOPS] && p <= last_piece[MI][TROOPS]) return MI if (p >= first_piece[DS][TROOPS] && p <= last_piece[DS][TROOPS]) return DS if (p >= first_piece[DS][ELITE] && p <= last_piece[DS][ELITE]) return DS if (p >= first_piece[BK][ELITE] && p <= last_piece[BK][ELITE]) return BK if (p >= first_piece[VE][ELITE] && p <= last_piece[VE][ELITE]) return VE if (p >= first_piece[DS][DISC] && p <= last_piece[DS][DISC]) return DS if (p >= first_piece[BK][DISC] && p <= last_piece[BK][DISC]) return BK if (p >= first_piece[VE][DISC] && p <= last_piece[VE][DISC]) return VE throw "IMPOSSIBLE" } function piece_name(p) { return PIECE_FACTION_TYPE_NAME[piece_faction(p)][piece_type(p)] } function piece_type(p) { if (p >= first_piece[MI][TROOPS] && p <= last_piece[MI][TROOPS]) return TROOPS if (p >= first_piece[DS][TROOPS] && p <= last_piece[DS][TROOPS]) return TROOPS if (p >= first_piece[DS][ELITE] && p <= last_piece[DS][ELITE]) return ELITE if (p >= first_piece[BK][ELITE] && p <= last_piece[BK][ELITE]) return ELITE if (p >= first_piece[VE][ELITE] && p <= last_piece[VE][ELITE]) return ELITE if (p >= first_piece[DS][DISC] && p <= last_piece[DS][DISC]) return DISC if (p >= first_piece[BK][DISC] && p <= last_piece[BK][DISC]) return DISC if (p >= first_piece[VE][DISC] && p <= last_piece[VE][DISC]) return DISC throw "IMPOSSIBLE" } /* UTILS */ function add_resources(faction, n) { game.resources[faction] = Math.max(0, Math.min(24, game.resources[faction] + n)) } function update_control() { game.control = [0, 0, 0] for (let s = first_space; s <= last_province; ++s) { if (is_tributary(s)) continue let c = has_majority(s) if (c <= VE) game.control[c] |= (1 << s) } } /* ACTIONS */ function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_piece(p) { gen_action("piece", p) } function gen_action_space(s) { gen_action("space", s) } /* 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)) } function log_resources(faction, n) { if (n > 0) log(faction_name[faction] + " Resources +" + n + ".") else log(faction_name[faction] + " Resources " + n + ".") } // === 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", "Governor", "Troops" ], [ "Fort", "Amir", null ], [ "Temple", "Raja", null ], [ null, null, "Invader" ] ] // 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 ===