"use strict" let states = {} let game = null let view = null /* DATA */ const data = require("./data.js") const SPACES = data.spaces 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" ] const faction_acronyms = [ "ds", "bk", "ve", "mi" ] 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 = { state: game.state, prompt: null, actions: null, log: game.log, current: game.current, vp: game.vp, resources: game.resources, inf: game.inf, deck: [ this_card, deck_size, game.of_gods_and_kings ], cavalry: game.cavalry, cylinder: game.cylinder, pieces: game.pieces, tributary: game.tributary, control: game.control, rebel: game.rebel, order: game.order, } 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 ], prosperity: [ 0, 0, 0 ], inf: [null, 0, 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: Array(10).fill(AVAILABLE), deck: [], order: [], cmd: { type: null, limited: null, free: null, spaces: [], pieces: [], where: null, who: null, event: 0 }, cav: null, inf_shift: null, decree: null, vm: null, } if (scenario === "Solo") game.solo = 1 // TODO: setup start pieces // goto_card() game.current = DS game.state = "todo" // Setup setup_standard() update_control() setup_deck() goto_card() return save_game() } function setup_deck() { game.deck = [ 1, 37, 2, 41, 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) // Two cavalry to DS set_cavalry_faction(0, DS) set_cavalry_faction(1, DS) } 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() { if (this_card() >= 37 && this_card() <= 39) { log_h1("Mongol Invaders C" + this_card()) goto_mongol_invaders(BK) } else if (this_card() >= 40 && this_card() <= 42) { log_h1("Mongol Invaders C" + this_card()) goto_mongol_invaders(VE) } else { log_h1("C" + this_card()) adjust_eligibility(DS) adjust_eligibility(BK) adjust_eligibility(VE) resume_event_card() } } function resume_event_card() { clear_undo() if (game.cylinder.includes(ELIGIBLE)) goto_eligible() else end_card() } function end_card() { clear_undo() 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.cmd = { 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() { game.order = data.card_order[this_card()] for (let faction of game.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.cmd = 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_advance() { init_command("Advance") game.state = "advance" } function goto_amass() { init_command("Amass") game.state = "amass" } function goto_cavalry(n, next) { game.cav = { "n": n, "next": next } game.state = "cavalry" } function goto_collect() { init_decree("Collect") game.state = "collect" } function goto_compromising_gifts() { if (game.inf[game.cmd.who] === 0) end_card() else game.state = "compromising_gifts" } 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.cmd.where) end_conscript_space() } else { game.cmd.count = 0 game.state = "conscript_space" } } function end_conscript_space() { log_space(game.cmd.where, "Conscript") pop_summary() game.state = "conscript" } function goto_demand() { init_decree("Demand Obedience") game.state = "demand" } function goto_govern() { init_command("Govern") game.state = "govern" } function goto_govern_space() { push_summary() if (count_pieces(game.cmd.where, DS, ELITE) === 0) { let p = find_piece(AVAILABLE, DS, ELITE) log_summary_place(p) place_piece(p, game.cmd.where) end_govern_space() } else { game.cmd.count = [0, 0] game.state = "govern_space" } } function end_govern_space() { log_space(game.cmd.where, "Govern") pop_summary() game.state = "govern" } function goto_march() { init_command("March") game.cmd.pieces = [] game.state = "march" } function goto_march_space() { push_summary() game.cmd_count = 0 game.state = "march_space" } function end_march_space() { log_space(game.cmd.where, "March") pop_summary() game.state = "march" } function goto_mi_attack() { init_command("Attack & Plunder") game.state = "mi_attack" } function goto_mongol_invaders(faction) { game.cmd = {} game.current = faction game.cmd.free = 2 goto_strategic_assistance() } function goto_strategic_assistance() { if (game.cmd.free > 0) { game.state = "strategic_assistance" } else { game.cmd.free = 1 goto_compromising_gifts() } } 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.cmd.where) end_rally_space() } else { game.cmd.count = 0 game.state = "rally_space" } } function end_rally_space() { log_space(game.cmd.where, "Rally") pop_summary() game.state = "rally" } function goto_rebel() { init_command("Rebel") game.state = "rebel" } function goto_tax() { init_decree("Tax") game.state = "tax" } /* 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 = "main_phase" }, event_command() { push_undo() game.cylinder[game.current] = SOP_EVENT_OR_COMMAND game.cmd.event = 1 game.state = "main_phase" }, lim_command() { push_undo() game.cylinder[game.current] = SOP_LIMITED_COMMAND game.cmd.limited = 1 game.state = "main_phase" }, } states.main_phase = { inactive: "Eligible Faction", prompt() { if (game.cmd && game.cmd.limited === 1) { view.prompt = "Select a limited Command." gen_any_command() } else if (game.cmd && game.decree === 1) { view.prompt = "Select a Command and a Decree." gen_any_command() gen_any_decree() } else if (game.cmd && game.cmd.event === 1) { view.prompt = "Select a Command or the Event." gen_any_command() // gen_event() } else if (game.cmd) { view.prompt = "Select a Command." gen_any_command() } else if (game.decree === 1) { view.prompt = "Select a Decree." gen_any_decree() } else { view.prompt = "End of turn." view.actions.end_of_turn = 1 } }, build: goto_build, collect: goto_collect, conscript: goto_conscript, demand: goto_demand, govern: goto_govern, march: goto_march, rally: goto_rally, rebel: goto_rebel, tax: goto_tax, trade: goto_trade, end_of_turn: resume_event_card, } states.advance = { prompt() { view.prompt = "Advance: Select the Advance destination" if (has_piece(S_MOUNTAIN_PASSES, MI, TROOPS)) gen_action_space(S_PUNJAB) if (has_piece(S_PUNJAB, MI, TROOPS)) gen_action_space(S_DELHI) }, space(s) { push_summary() game.cmd.where = s game.state = "advance_space" } } states.advance_space = { prompt() { view.prompt = "Advance: Move any number of Mongol Invaders into destination" let origin = game.cmd.where === S_DELHI ? S_PUNJAB : S_MOUNTAIN_PASSES for (let p = first_piece[MI][TROOPS]; p <= last_piece[MI][TROOPS]; ++p) if (piece_space(p) === origin) gen_action_piece(p) view.actions.end_advance = 1 }, piece(p) { log_summary_move_from(p) place_piece(p, game.cmd.where) }, end_advance() { game.cmd.free -= 1 log_space(game.cmd.where, "Advance") pop_summary() goto_strategic_assistance() } } states.amass = { prompt() { view.prompt = "Amass: Place up to three invaders into the Mountain Passes" gen_action_space(S_MONGOL_INVADERS) }, space(s) { amass() game.cmd.free -= 1 goto_strategic_assistance() } } states.cavalry = { prompt() { if (game.cav.n > 0) { view.prompt = `Gain Cavalry: Take ${game.cav.n} calvary tokens.` gen_take_cavalry(game.current) } else { view.prompt = "Gain Cavalry: Done." view.actions.end_cavalry = 1 } }, token(c) { game.cav.n -= 1 set_cavalry_faction(c, game.current) // TODO: cavalry log }, end_cavalry() { if (game.cav.next) game.state = game.cav.next else goto_eligible() } } states.collect = { prompt() { if (game.decree > 0) { view.prompt = `Collect Tribute: Collect ${collect_count()} from half the Tributaries prosperity` gen_action_resources(DS) } else { view.prompt = "Collect Tribute: Done." view.actions.end_collect = 1 } }, resources(f) { let c = collect_count() add_resources(DS, c) logi_resources(DS, c) game.decree = 0 goto_cavalry(2, "collect") }, end_collect: end_decree, } states.compromising_gifts = { prompt() { view.prompt = "Compromising gifts: Reduce your influence by one to gain two Resources and two Cavalry tokens" gen_action_influence(game.current) view.actions.end_gifts = 1 }, inf() { log_h2(faction_acronyms[game.current] + "Compromising Gifts") logi_resources(game.current, 2) add_resources(game.current, 2) game.inf_shift = { "next": "cavalry", } game.cav = { "n": 2, "next": null } remove_influence(game.current) }, end_gifts() { end_card() } } 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.cmd.where view.actions.next = 1 gen_place_piece(DS, TROOPS) }, piece(p) { log_summary_place(p) place_piece(p, game.cmd.where) if (++game.cmd.count >= conscript_count()) end_conscript_space() }, next() { end_conscript_space() } } states.demand = { prompt() { view.prompt = "Demand Obedience: Select a Controlled province with a Governor" if (can_select_cmd_space(0) && can_demand()) for (let s = first_space; s <= last_space; ++s) if (!is_selected_cmd_space(s) && can_demand_in_space(s)) gen_action_space(s) view.actions.end_demand = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 0) add_resources(DS, SPACES[s].pop) add_tributary(s) to_obedient_space(s) log_space(s, "Demand Obedience") }, end_demand: end_decree, } states.govern = { prompt() { view.prompt = "Govern: Select a Tributary, Controlled, Mongol Invader region or Dehli." if (can_select_cmd_space(1) && can_govern()) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_govern_in_space(s)) gen_action_space(s) } } view.actions.end_govern = prompt_end_cmd(1) }, space(s) { push_undo() select_cmd_space(s, 1) goto_govern_space() }, end_govern: end_command, } states.govern_space = { prompt() { view.prompt = "Govern: Place a Governor or place a Qasbah and remove two Obedient Units." view.where = game.cmd.where view.actions.next = 1 if (has_piece(AVAILABLE, DS, ELITE) && game.cmd.count[0] + game.cmd.count[1] === 0) gen_place_piece(DS, ELITE) if ( has_piece(AVAILABLE, DS, DISC) && has_piece(game.cmd.where, DS, ELITE) && !has_piece(game.cmd.where, DS, DISC) ) { gen_place_piece(DS, DISC) } if (has_piece(game.cmd.where, DS, ELITE) && game.cmd.count[1] < 2) { gen_action_obedient_in_space(game.cmd.where, BK) gen_action_obedient_in_space(game.cmd.where, VE) } }, piece(p) { let t = piece_type(p) let f = piece_faction(p) if (f === DS) { log_summary_place(p) place_piece(p, game.cmd.where) if (t === ELITE) end_govern_space() else { game.cmd.count[0] += 1 } } else { log_summary_remove(p) remove_piece(p) game.cmd.count[1] += 1 } }, next() { end_govern_space() } } states.march = { prompt() { view.prompt = "March: Select a space to move into." if (can_select_cmd_space(1)) { for (let s = first_space; s <= last_space; ++s) { if (!is_selected_cmd_space(s) && can_march_in_space(s)) gen_action_space(s) } } view.actions.end_march = prompt_end_cmd(1) }, space(s) { push_undo select_cmd_space(s, 1) goto_march_space() }, end_march: end_command } states.march_space = { prompt() { view.prompt = "March: Move pieces from adjacent spaces." view.where = game.cmd.where view.actions.next = 1 for_each_movable(DS, p => { if ( set_has(SPACES[game.cmd.where].adjacent, piece_space(p)) && !set_has(game.cmd.pieces, p) ) gen_action_piece(p) }) }, piece(p) { log_summary_move_from(p) set_add(game.cmd.pieces, p) place_piece(p, game.cmd.where) }, next() { end_march_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.cmd.where view.actions.next = 1 gen_place_piece(game.current, ELITE) }, piece(p) { log_summary_place(p) place_piece(p, game.cmd.where) if (++game.cmd.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, } states.strategic_assistance = { prompt() { let n_command = (game.cmd.free === 2) ? "two" : "one" view.prompt = `Strategic Assistance: ${faction_name[game.current]} must execute ${n_command} Mongol Invaders Commands` gen_mi_command() }, amass: goto_amass, advance: goto_advance, mi_attack: goto_mi_attack, } states.tax = { prompt() { if (game.decree === 1) { view.prompt = `Tax: Collect ${tax_count()} from Controlled Prosperity and Temples.` gen_action_resources(VE) } else { view.prompt = "Tax: Done." view.actions.end_tax = 1 } }, resources(f) { let t = tax_count() add_resources(game.current, t) logi_resources(VE, t) game.decree = 0 }, end_tax: end_decree } /* COMMANDS */ function init_command(type) { push_undo() if (game.cmd.limited) log_h2(faction_name[game.current] + " - Limited " + type) else log_h2(faction_name[game.current] + " - " + type) game.cmd.type = type } function gen_any_command() { if (game.current === DS) { view.actions.conscript = can_conscript() ? 1 : 0 view.actions.govern = can_govern() ? 1 : 0 view.actions.march = can_march() ? 1 : 0 // view.actions.attack = can_attack() ? 1 : 0 } else if (game.current === BK || game.current === VE) { view.actions.rally = can_rally() ? 1 : 0 // view.actions.migrate = can_migrate() ? 1 : 0 view.actions.rebel = can_rebel() ? 1 : 0 // view.actions.attack = can_attack() ? 1 : 0 } } function can_select_cmd_space(cost) { if (!game.cmd.free && game.resources[game.current] < cost) return false if (game.cmd.limited) return game.cmd.spaces.length === 0 return true } function select_cmd_space(s, cost) { set_add(game.cmd.spaces, s) if (!game.cmd.free) game.resources[game.current] -= cost game.cmd.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.cmd.where === S_DELHI) return 5 if (has_piece(game.cmd.where, DS, DISC)) { return 2 } return 1 } function can_collect() { return collect_count() > 0 } function collect_count() { let c = Math.floor(game.prosperity[DS]) return Math.floor(game.prosperity[0] / 2) } function can_govern() { for (let s = first_space; s <= last_space; ++s) if (can_govern_in_space(s)) return true return false } function can_govern_in_space(s) { let is_space = (is_tributary(s) || s === S_DELHI || s === S_MOUNTAIN_PASSES || s === S_PUNJAB || is_faction_control(s, DS)) if (!is_space) return false if (has_piece(AVAILABLE, DS, ELITE)) return true if (has_piece(s, DS, ELITE) && has_piece(AVAILABLE, DS, DISC)) return true return false } function can_march() { for (let s = first_space; s <= last_space; ++s) if (can_march_in_space(s)) return true return false } function can_march_in_space(s) { for (let adj of SPACES[s].adjacent) if (has_unmoved_piece(adj, DS)) return true return false } 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.cmd.where === S_MAHARASHTRA ? 1 : 0) } else if (game.current === VE) { count += (game.cmd.where === S_KARNATAKA ? 1 : 0) count += (has_piece(game.cmd.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.cmd.limited && game.cmd.spaces && game.cmd.spaces.length > 0) view.prompt = game.cmd.type + ": Done." else if (!game.cmd.free && game.resources[game.current] < cost) view.prompt = game.cmd.type + ": No resources." else view.prompt = game.cmd.type + ": Done." } return (game.cmd.spaces.length > 0) ? 1 : 0 } function end_command() { log_br() game.cmd = null game.state = "main_phase" } function end_decree() { log_br() game.decree = 0 game.state = "main_phase" } /* DECREES */ function init_decree(type) { push_undo() log_h2(faction_name[game.current] + " - " + type) } function gen_any_decree() { if (game.current === DS) { view.actions.collect = can_collect() ? 1 : 0 // view.actions.campaign = can_campaign() ? 1 : 0 view.actions.demand = can_demand() ? 1 : 0 } else if (game.current === BK) { view.actions.trade = can_trade() ? 1 : 0 view.actions.build = can_build() ? 1 : 0 view.actions.conspire = can_conspire() ? 1 : 0 } else if (game.current === VE) { view.actions.tax = can_tax() ? 1 : 0 view.actions.build = can_build() ? 1 : 0 // view.actions.compel = can_compel() ? 1 : 0 } } function can_demand() { for (let s = first_space; s <= last_province; ++s) if (can_demand_in_space(s)) return true return false } function can_demand_in_space(s) { return is_faction_control(s, DS) && has_piece(s, DS, ELITE) } function can_tax() { return tax_count() > 0 } function tax_count() { return count_pieces_on_map(VE, DISC) + game.prosperity[VE] } /* REBELS DECREES */ function can_build() { if (!has_piece(AVAILABLE, game.current, DISC)) return false for (let s = first_space; s <= last_province; ++s) if (can_build_in_space(s)) return true return false } function can_build_in_space(s) { return has_piece(s, game.current, ELITE) && !has_piece(s, game.current, DISC) } function goto_build() { init_decree("Build") game.state = "build" } 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 (game.decree === 1) { for (let s = first_space; s <= last_space; ++s) { if (can_build_in_space(s)) gen_action_space(s) } } else { view.prompt = "Build: Done." view.actions.end_build = 1 } }, 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 }, end_build: end_decree, } function can_trade() { return trade_count() > 0 } function trade_count() { let count = 0 for (let s = first_space; s <= last_province; ++s) count += (count_pieces(s, BK, DISC) + count_pieces(s, BK, ELITE)) > 0 ? 1 : 0 return count } function goto_trade() { init_decree("Trade") game.state = "trade" } states.trade = { prompt() { if (game.decree === 1) { view.prompt = `Trade: Collect ${trade_count()} from Provinces with your presence.` gen_action_resources(BK) } else { view.prompt = "Trade: Done." view.actions.end_trade = 1 } }, resources(f) { let t = trade_count() add_resources(game.current, t) logi_resources(BK, t) game.decree = 0 goto_cavalry(trade_cavalry_count(), "trade") }, end_trade: end_decree, } /* MONGOL INVADERS COMMANDS */ function gen_mi_command() { view.actions.amass = can_amass() ? 1 : 0 view.actions.advance = can_advance() ? 1 : 0 view.actions.mi_attack = can_mi_attack() ? 1 : 0 } function can_advance() { return (has_piece(S_MOUNTAIN_PASSES, MI, TROOPS) || has_piece(S_PUNJAB, MI, TROOPS)) } function can_amass() { return has_piece(AVAILABLE, MI, TROOPS) } function can_mi_attack() { let p0 = first_piece[MI][TROOPS] let p1 = last_piece[MI][TROOPS] for (let p = p0; p <= p1; ++p) if (piece_space(p) >= 0) return true return false } function amass() { let n_available = Math.min(count_faction_pieces(AVAILABLE, MI), 3) // Add three to mountain pass log_space(S_MOUNTAIN_PASSES, "Amass") push_summary() for (let i = 0; i < n_available; ++i) { let p = find_piece(AVAILABLE, MI, TROOPS) log_summary_place(p) place_piece(p, S_MOUNTAIN_PASSES) } pop_summary() amass_trinkle_down(S_MOUNTAIN_PASSES, S_PUNJAB) amass_trinkle_down(S_PUNJAB, S_DELHI) } function amass_trinkle_down(s_source, s_dest) { let n = count_faction_pieces(s_source, MI) - 4 if (n > 0) { log_space(s_dest, "Amass") push_summary() for (let i = n; i > 0; --i) { let p = find_piece(s_source, MI, TROOPS) log_summary_place(p) place_piece(p, s_dest) } pop_summary() } } /* TRIBUTARY AND REBELS */ function add_tributary(s) { game.tributary |= (1 << s) update_control() } function is_tributary(s) { return game.tributary & (1 << s) } function is_faction_control(s, faction) { return game.control[faction] & (1 << s) } function remove_tributary(s) { game.tributary &= ~(1 << s) update_control() } function is_rebel(p) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] return game.rebel[faction] & (1 << piece_index) } 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_obedient_space(s) { to_obedient_space_faction(s, BK) to_obedient_space_faction(s, VE) } function to_obedient_space_faction(s, faction) { for_each_piece(faction, ELITE, p => { if (piece_space(p) === s) to_obedient(p) }) } function to_rebel(p) { let faction = piece_faction(p) let piece_index = p - first_piece[faction][ELITE] game.rebel[faction] |= (1 << piece_index) } function to_rebel_space(s, faction) { for_each_piece(faction, ELITE, p => { if (piece_space(p) === s) to_rebel(p) }) } function update_rebel(p, s) { let faction = piece_faction(p) if (is_faction_control(s, faction)) to_rebel(p) } function gen_action_obedient_in_space(s, faction) { for_each_piece(faction, ELITE, p => { if (piece_space(p) === s && !is_rebel(p)) gen_action_piece(p) }) } /* MISC SPACE + PIECE QUERIES */ function count_pieces(s, faction, type) { let n = 0 for_each_piece(faction, type, p => { if (piece_space(p) === s) ++n }) return n } function count_pieces_on_map(faction, type) { let n = 0 for_each_piece(faction, type, p => { if (piece_space(p) >= 0) ++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 find_piece(s, faction, type) { let p0 = first_piece[faction][type] let p1 = last_piece[faction][type] for (let p = p0; p <= p1; ++p) if (piece_space(p) === s) return p return -1 } function has_piece(s, faction, type) { return find_piece(s, faction, type) != -1 } function has_unmoved_piece(s, faction) { let unmoved = false for_each_movable(faction, p => { if (piece_space(p) === s) if (!game.cmd.pieces || game.cmd.pieces.length === 0) unmoved = true else if (!set_has(game.cmd.pieces, p)) unmoved = true }) return unmoved } function gen_place_piece(faction, type) { for_each_piece(faction, type, p => { if (piece_space(p) === AVAILABLE) { gen_action_piece(p) } }) } function move_all_faction_piece_from(faction, type, from, to) { for_each_piece(faction, type, p => { if (piece_space(p) === from) set_piece_space(p, to) }) } function is_selected_cmd_space(s) { return game.cmd.spaces && set_has(game.cmd.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() update_rebel(p, s) } function remove_piece(p) { set_piece_space(p, AVAILABLE) 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" } /* CAVALRY */ function available_cavalry() { let n = 0 for (let c = 0; c <= LAST_CAVALRY; ++c) if (game.cavalry[c] === AVAILABLE) n += 1 return n } function gen_take_cavalry(faction) { let can_place = false for (let c = 0; c <= LAST_CAVALRY; ++c) { if (game.cavalry[c] === AVAILABLE) { gen_action_token(c) can_place = true } } if (!can_place && faction === game.current) { for (let c = 0; c <= LAST_CAVALRY; ++c) { if (game.cavalry[c] !== faction) gen_action_token(c) } } return can_place } function set_cavalry_faction(c, f) { game.cavalry[c] = f } function trade_cavalry_count() { if (game.inf[BK] === 0) return 1; if (game.inf[BK] <= 2) return 2; return 3; } /* INFLUENCE */ function add_influence(faction) { if (game.inf[faction] === 4) return game.inf[faction]++ if (faction === BK && game.inf[faction] === 2) move_all_faction_piece_from(BK, ELITE, S_BK_INF_2, AVAILABLE) else if (faction === BK && game.inf[faction] === 4) move_all_faction_piece_from(BK, ELITE, S_BK_INF_4, AVAILABLE) else if (faction === VE) move_all_faction_piece_from(VE, ELITE, S_VE_INF_1 + game.inf[faction] - 1, AVAILABLE) } function remove_influence(faction) { if (game.inf[faction] === 0) return game.inf[faction]-- if (faction === BK && game.inf[faction] === 3) troops_to_inf_track(BK, S_BK_INF_4) else if (faction === BK && game.inf[faction] === 1) troops_to_inf_track(BK, S_BK_INF_2) else if (faction === VE) troops_to_inf_track(VE, S_VE_INF_1 + game.inf[faction]) } function troops_to_inf_track(faction, s) { let n_available = Math.min(count_pieces(AVAILABLE, faction, ELITE), 2) let moved = 0 let p = null while (n_available > moved) { p = find_piece(AVAILABLE, faction, ELITE) place_piece(p, s) moved++ } if (moved < 2) { goto_return_troops(faction, 2-moved, s) } else { game.state = game.inf_shift.next } } function goto_return_troops(faction, n_troops, s) { push_summary() game.inf_shift.n = n_troops game.inf_shift.s = s game.inf_shift.save_current = game.current game.current = faction game.state = "return_troops" } states.return_troops = { prompt() { if (game.inf_shift.n > 0) { view.prompt = "Deccan Influence dwindles: Return troops to the influence track." for_each_piece(game.current, ELITE, p => { if (piece_space(p) >= 0 && piece_space(p) <= S_PUNJAB) gen_action_piece(p) }) } else { view.prompt = "Deccan Influence dwindles: Done." view.actions.end_return = 1 } }, piece(p) { log_summary_move_from(p) place_piece(p, game.inf_shift.s) game.inf_shift.n -= 1 }, end_return() { pop_summary() game.current = game.inf_shift.save_current game.state = game.inf_shift.next } } /* 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] game.prosperity = [0, 0, 0] for (let s = first_space; s <= last_province; ++s) { if (is_tributary(s)) game.prosperity[DS] += SPACES[s].pop else { let c = has_majority(s) if (c <= VE) game.control[c] |= (1 << s) if (c === BK || c === VE) game.prosperity[c] += SPACES[s].pop } } update_vp() } function update_vp() { game.vp[DS] = game.prosperity[DS] game.vp[BK] = game.prosperity[BK] + count_pieces_on_map(BK, DISC) + game.inf[BK] game.vp[VE] = game.prosperity[VE] + count_pieces_on_map(VE, DISC) + game.inf[VE] } /* ACTIONS */ function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] set_add(view.actions[action], argument) } function gen_action_influence(faction) { gen_action("inf", faction) } function gen_action_piece(p) { gen_action("piece", p) } function gen_action_resources(faction) { gen_action("resources", faction) } function gen_action_space(s) { gen_action("space", s) } function gen_action_token(t) { gen_action("token", t) } /* 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_from(p) { log_summary("% " + piece_name(p) + " from S" + piece_space(p)) } function log_summary_remove(p) { log_summary("Removed % " + piece_name(p)) } function logi_resources(faction, n) { if (n > 0) logi(faction_name[faction] + " Resources +" + n) else logi(faction_name[faction] + " Resources " + n) } 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 } // === ITERATORS AND ACTION GENERATORS === function for_each_piece(faction, type, f) { let p0 = first_piece[faction][type] let p1 = last_piece[faction][type] for (let p = p0; p <= p1; ++p) f(p) } function for_each_movable(faction, f) { if (faction === BK) for_each_piece(BK, ELITE, f) else if (faction === VE) for_each_piece(BK, ELITE, f) else if (faction === DS) { for_each_piece(DS, TROOPS, f) for_each_piece(DS, ELITE, f) } } // === 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" ] ] const LAST_CAVALRY = 9 // 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 ===