From bb756e1f3d0014cc8d4eb3666849264daf108bdb Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Thu, 18 May 2023 00:38:35 +0200 Subject: Add Solo play. --- rules.js | 412 ++++++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 326 insertions(+), 86 deletions(-) (limited to 'rules.js') diff --git a/rules.js b/rules.js index 2ed6982..f1b960e 100644 --- a/rules.js +++ b/rules.js @@ -2,67 +2,6 @@ // === CONSTANTS AND DATA === -// Barbarian possible locations: -/* -FRANKS - BRITANNIA - GALLIA - HISPANIA - PANNONIA - ITALIA - -ALAMANNI - PANNONIA - ITALIA - THRACIA - MACEDONIA - -GOTHS - THRACIA - MACEDONIA - ASIA - GALATIA - SYRIA - - -SASSANIDS - GALATIA - ASIA - SYRIA - AEGYPTUS - -NOMADS - AFRCIA - HISPANIA - AEGYPTUS - SYRIA - -NOMAD STACKS in PROVINCES - AEGYPTUS NOMADS - AEGYPTUS SASSANIDS - AFRICA NOMADS - ASIA GOTHS - ASIA SASSANIDS - BRITANNIA FRANKS - GALATIA GOTHS - GALATIA SASSANIDS - GALLIA FRANKS - HISPANIA FRANKS - HISPANIA NOMADS - ITALIA ALAMANNI - ITALIA FRANKS - MACEDONIA ALAMANNI - MACEDONIA GOTHS - PANNONIA ALAMANNI - PANNONIA FRANKS - SYRIA GOTHS - SYRIA NOMADS - SYRIA SASSANIDS - THRACIA ALAMANNI - THRACIA GOTHS -*/ - - const P1 = "Red" const P2 = "Blue" const P3 = "Yellow" @@ -75,6 +14,7 @@ const PLAYER_INDEX = { [P2]: 1, [P3]: 2, [P4]: 3, + "Solo": 4, "Observer": -1, } @@ -170,6 +110,36 @@ const BARBARIAN_HOMELAND = [ SASSANIDS_HOMELAND, ] +const BARBARIAN_INVASION = [ + // Alamanni + [ + [ 1, 3, [ PANNONIA, ITALIA ] ], + [ 4, 6, [ THRACIA, MACEDONIA ] ], + ], + // Franks + [ + [ 1, 2, [ BRITANNIA, GALLIA ] ], + [ 3, 4, [ GALLIA, HISPANIA ] ], + [ 5, 6, [ PANNONIA, ITALIA ] ], + ], + // Goths + [ + [ 1, 2, [ THRACIA, MACEDONIA ] ], + [ 3, 4, [ ASIA, MACEDONIA ] ], + [ 5, 6, [ GALATIA, SYRIA ] ], + ], + // Nomads + [ + [ 1, 3, [ AFRICA, HISPANIA ] ], + [ 4, 6, [ AEGYPTUS, SYRIA ] ], + ], + // Sassanids + [ + [ 1, 3, [ GALATIA, ASIA ] ], + [ 4, 6, [ SYRIA, AEGYPTUS ] ], + ], +] + // 12x const CARD_M1 = [ 1, 12 ] const CARD_S1 = [ 13, 24 ] @@ -207,45 +177,45 @@ const EVENT_GOOD_AUGURIES = 14 const EVENT_DIOCLETIAN = 15 const CRISIS_TABLE_4P = [ 0, 0, - "Ira Deorum", + 0, SASSANIDS, FRANKS, SASSANIDS, GOTHS, - "Event", + 0, ALAMANNI, NOMADS, FRANKS, NOMADS, - "Pax Deorum" + 0, ] const CRISIS_TABLE_3P = [ 0, 0, - "Ira Deorum", + 0, FRANKS, SASSANIDS, SASSANIDS, FRANKS, - "Event", + 0, ALAMANNI, GOTHS, GOTHS, ALAMANNI, - "Pax Deorum" + 0, ] const CRISIS_TABLE_2P = [ 0, 0, - "Ira Deorum", + 0, FRANKS, ALAMANNI, FRANKS, GOTHS, - "Event", + 0, GOTHS, FRANKS, ALAMANNI, ALAMANNI, - "Pax Deorum" + 0, ] // === @@ -258,6 +228,8 @@ const states = {} exports.scenarios = [ "Standard" ] exports.roles = function (scenario, options) { + if (options.players == 1) + return [ "Solo" ] if (options.players == 2) return [ P1, P2 ] if (options.players == 3) @@ -312,7 +284,8 @@ exports.setup = function (seed, scenario, options) { log: [], undo: [], active: 0, - state: "setup", + current: 0, + state: "setup_province", first: 0, events: null, active_events: [], @@ -353,6 +326,11 @@ exports.setup = function (seed, scenario, options) { setup_barbarians(NOMADS) setup_barbarians(SASSANIDS) + if (player_count === 1) { + game.solo = 1 + player_count = 4 + } + for (let pi = 0; pi < player_count; ++pi) { game.players[pi] = { legacy: 0, @@ -391,7 +369,7 @@ exports.setup = function (seed, scenario, options) { remove_barbarians(SASSANIDS) } - game.first = game.active = random(player_count) + game.first = game.current = random(player_count) log(PLAYER_NAMES[game.first] + " is First Player!") return save_game() @@ -399,11 +377,13 @@ exports.setup = function (seed, scenario, options) { function load_game(state) { game = state - game.active = PLAYER_INDEX[game.active] } function save_game() { - game.active = PLAYER_NAMES[game.active] + if (game.solo) + game.active = "Solo" + else + game.active = PLAYER_NAMES[game.current] return game } @@ -421,6 +401,12 @@ exports.action = function (state, player, action, arg) { return save_game() } +function is_current_player(player) { + if (player === 4) + return true + return game.current === player +} + exports.view = function (state, player_name) { let player = PLAYER_INDEX[player_name] @@ -428,7 +414,7 @@ exports.view = function (state, player_name) { view = { log: game.log, - active: PLAYER_NAMES[game.active], + current: game.current, prompt: null, militia: game.militia, @@ -462,23 +448,25 @@ exports.view = function (state, player_name) { if (game.state === "game_over") { view.prompt = game.victory - } else if (game.active !== player) { + } else if (player !== game.current && player_name !== "Solo") { let inactive = states[game.state].inactive || game.state - view.prompt = `Waiting for ${PLAYER_NAMES[game.active]} \u2014 ${inactive}...` + view.prompt = `Waiting for ${PLAYER_NAMES[game.current]} \u2014 ${inactive}...` } else { - view.hand = game.players[player].hand - view.draw = game.players[player].draw - view.discard = game.players[player].discard - view.actions = {} states[game.state].prompt() - view.prompt = PLAYER_NAMES[game.active] + ": " + view.prompt + view.prompt = PLAYER_NAMES[game.current] + ": " + view.prompt if (game.undo && game.undo.length > 0) view.actions.undo = 1 else view.actions.undo = 0 } + if (player >= 0 && player <= game.players.length) { + view.hand = game.players[player].hand + view.draw = game.players[player].draw + view.discard = game.players[player].discard + } + save_game() return view } @@ -489,8 +477,37 @@ function log(msg) { game.log.push(msg) } +function log_br() { + if (game.log.length > 0 && game.log[game.log.length - 1] !== "") + game.log.push("") +} + +function log_h1(msg) { + log_br() + log(".h1 " + msg) + log_br() +} + +function log_h2(msg) { + log_br() + log(".h2 " + msg) + log_br() +} + +function logi(msg) { + game.log.push(">" + msg) +} + +function logii(msg) { + game.log.push(">>" + msg) +} + // === STATES === +function next_player() { + return (game.current + 1) % game.players.length +} + function get_governor(r) { for (let p = 0; p < game.players.length; ++p) { for (let i = 0; i < 6; ++i) @@ -511,6 +528,14 @@ function find_legion() { return -1 } +function current_hand() { + return game.players[game.current].hand +} + +function current_draw() { + return game.players[game.current].draw +} + function add_militia(r) { game.militia |= (1 << r) } @@ -527,7 +552,7 @@ function set_support(r, level) { game.support[r] = level } -states.setup = { +states.setup_province = { prompt() { view.prompt = "Select a starting Province." for (let r = 1; r < 12; ++r) @@ -535,21 +560,236 @@ states.setup = { gen_action("capital", r) }, capital(r) { - let p = game.active + push_undo() + let p = game.current game.players[p].generals[0] = r game.players[p].governors[0] = r game.players[p].capital = 1 game.legions[find_legion()] = ARMY[p][0] add_militia(r) + game.state = "setup_hand" + }, +} - game.active = (game.active + 1) % game.players.length - if (game.active === game.first) +states.setup_hand = { + prompt() { + view.prompt = "Draw your initial hand." + let hand = current_hand() + if (hand.length < 5) { + for (let c of current_draw()) + gen_action("card", c) + } else { + view.actions.done = 1 + } + }, + card(c) { + push_undo() + set_delete(current_draw(), c) + set_add(current_hand(), c) + }, + done() { + clear_undo() + game.state = "setup_province" + game.current = next_player() + if (game.current === game.first) goto_start_turn() }, } +function goto_start_turn() { + log_h2(PLAYER_NAMES[game.current]) + goto_upkeep() +} + +function goto_upkeep() { + goto_crisis() +} + +// === CRISIS === + +function goto_crisis() { + game.dice[0] = roll_die() + game.dice[1] = roll_die() + + log(`Crisis B${game.dice[0]} W${game.dice[1]}`) + + let sum = game.dice[0] + game.dice[1] + + if (sum === 2) + return goto_ira_deorum() + if (sum === 12) + return goto_pax_deorum() + if (sum === 7) + return goto_event() + + if (game.players.length === 2) + return goto_barbarian_crisis(CRISIS_TABLE_2P[sum]) + if (game.players.length === 3) + return goto_barbarian_crisis(CRISIS_TABLE_3P[sum]) + return goto_barbarian_crisis(CRISIS_TABLE_4P[sum]) +} + +function goto_ira_deorum() { + logi("Ira Deorum") + activate_one_barbarian(ALAMANNI) + activate_one_barbarian(FRANKS) + activate_one_barbarian(GOTHS) + activate_one_barbarian(NOMADS) + activate_one_barbarian(SASSANIDS) + goto_take_actions() +} + +function goto_pax_deorum() { + logi("Pax Deorum") + logi("TODO") + goto_take_actions() +} + +function goto_event() { + logi("Event") + logi("TODO") + goto_take_actions() +} + +function is_barbarian_active(i) { + return !game.is_barbarian_inactive[i] +} + +function is_barbarian_inactive(i) { + return game.is_barbarian_inactive[i] +} + +function set_barbarian_active(i) { + game.is_barbarian_inactive[i] = 0 +} + +function set_barbarian_inactive(i) { + game.is_barbarian_inactive[i] = 1 +} + +function find_active_barbarian(tribe) { + let home = BARBARIAN_HOMELAND[tribe] + for (let i = tribe * 10; i < tribe * 10 + 10; ++i) + if (game.barbarians[i] === home && is_barbarian_active(i)) + return i + return -1 +} + +function find_inactive_barbarian(tribe) { + let home = BARBARIAN_HOMELAND[tribe] + for (let i = tribe * 10; i < tribe * 10 + 10; ++i) + if (game.barbarians[i] === home && is_barbarian_inactive(i)) + return i + return -1 +} + +function count_active_barbarians_at_home(tribe) { + let home = BARBARIAN_HOMELAND[tribe] + let n = 0 + for (let i = tribe * 10; i < tribe * 10 + 10; ++i) + if (game.barbarians[i] === home && is_barbarian_active(i)) + n += 1 + return n +} + +function count_barbarians(tribe, region) { + // TODO: count leaders + let n = 0 + for (let i = tribe * 10; i < tribe * 10 + 10; ++i) + if (game.barbarians[i] === region) + n += 1 + return n +} + +function activate_one_barbarian(tribe) { + let i = find_inactive_barbarian(tribe) + if (i >= 0) + set_barbarian_active(i) +} + +function goto_barbarian_crisis(tribe) { + logi(BARBARIAN_NAME[tribe]) + activate_one_barbarian(tribe) + + let black = game.dice[2] = roll_die() + let white = game.dice[3] = roll_die() + + logi(`B${black} W${white}`) + + if (black <= count_active_barbarians_at_home(tribe)) + goto_barbarian_invasion(tribe, black, white) + else + goto_take_actions() +} + +function invade_with_active_barbarian(tribe, region) { + // TODO: move leaders first + let i = find_active_barbarian(tribe) + if (i >= 0) + game.barbarians[i] = region +} + +function goto_barbarian_invasion(tribe, black, white) { + logi("Invasion!") + + let path = null + for (let list of BARBARIAN_INVASION[tribe]) + if (white >= list[0] && white <= list[1]) + path = list[2] + + let k = 0 + for (let i = 0; i < black;) { + let n = count_barbarians(tribe, path[k]) + if (n < 3) { + invade_with_active_barbarian(tribe, path[k]) + ++i + } else { + if (++k > path.length) + break + } + } + + goto_take_actions() +} + +// === TAKE ACTIONS === + +function goto_take_actions() { + game.state = "take_actions" +} + +states.take_actions = { + prompt() { + view.actions.end_actions = 1 + }, + end_actions() { + goto_expand_pretender_empire() + }, +} + +function goto_expand_pretender_empire() { + goto_gain_legacy() +} + +function goto_gain_legacy() { + goto_buy_trash_cards() +} + +function goto_buy_trash_cards() { + goto_end_of_turn() +} + +function goto_end_of_turn() { + game.current = next_player() + goto_start_turn() +} + // === COMMON LIBRARY === +function roll_die() { + return random(6) + 1 +} + function gen_action(action, argument) { if (!(action in view.actions)) view.actions[action] = [] -- cgit v1.2.3