diff options
author | Tor Andersson <tor@ccxvii.net> | 2023-05-18 00:38:35 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2023-07-07 18:39:23 +0200 |
commit | bb756e1f3d0014cc8d4eb3666849264daf108bdb (patch) | |
tree | 7f6d8544965169b6d880140cf21daaa3f624d180 | |
parent | 18de9c65450661610d29f151e6ef31ab05905ac3 (diff) | |
download | time-of-crisis-bb756e1f3d0014cc8d4eb3666849264daf108bdb.tar.gz |
Add Solo play.
-rw-r--r-- | create.html | 1 | ||||
-rw-r--r-- | play.css | 19 | ||||
-rw-r--r-- | play.html | 8 | ||||
-rw-r--r-- | play.js | 13 | ||||
-rw-r--r-- | rules.js | 412 |
5 files changed, 363 insertions, 90 deletions
diff --git a/create.html b/create.html index dffd67b..bad574a 100644 --- a/create.html +++ b/create.html @@ -5,5 +5,6 @@ Player count: <option value="">4 Player</option> <option value="3">3 Player</option> <option value="2">2 Player</option> +<option value="1">Solo</option> </select> @@ -9,6 +9,11 @@ header.your_turn { background-color: orange; } #turn_info { background-color: gainsboro; } .role_vp { float: right; } +header.your_turn.player_red { background-color: salmon; } +header.your_turn.player_blue { background-color: skyblue; } +header.your_turn.player_yellow { background-color: khaki; } +header.your_turn.player_green { background-color: darkseagreen; } + .action { cursor: pointer; } @@ -346,6 +351,20 @@ body.p2 #Galatia_Governor { display: none } box-shadow: 0 0 0 1px #444, 0 0 4px #000; } +.card.action { + transition: transform 100ms ease; + box-shadow: 0 0 0px 3px #fff; + transform: translate(0px, 0px); +} + +.card.action:hover { + transform: translate(0px, -12px); +} + +#hand, #draw, #discard { + min-height: 350px; +} + .card.event_back{background-image:url(cards.1x/event_back.jpg)} .card.event_1{background-image:url(cards.1x/event_01.jpg)} .card.event_2{background-image:url(cards.1x/event_02.jpg)} @@ -78,10 +78,10 @@ <div id="pieces"> -<div id="crisis_die_1" class="hide" style="right:175px;top:525px"></div> -<div id="crisis_die_2" class="hide" style="right:125px;top:525px"></div> -<div id="barbarian_die_1" class="hide" style="right:175px;top:605px"></div> -<div id="barbarian_die_2" class="hide" style="right:125px;top:605px"></div> +<div id="crisis_die_1" class="hide" style="right:175px;top:530px"></div> +<div id="crisis_die_2" class="hide" style="right:125px;top:530px"></div> +<div id="barbarian_die_1" class="hide" style="right:155px;top:585px"></div> +<div id="barbarian_die_2" class="hide" style="right:105px;top:585px"></div> <div id="red_emperor_turns" class="red emperor_turns hide" style="top:30px;left:41px;"></div> <div id="blue_emperor_turns" class="blue emperor_turns hide" style="top:50px;left:41px;"></div> @@ -87,6 +87,7 @@ let ui = { barbarian_leaders: [], rival_emperors: [], body: document.querySelector("body"), + header: document.querySelector("header"), available_generals: document.getElementById("available_generals"), available_governors: document.getElementById("available_governors"), hand: document.getElementById("hand"), @@ -416,8 +417,15 @@ function layout_stack(id, list, region, in_capital) { function on_update() { stack_cache = {} + ui.body.classList.toggle("p1", view.solo === 1) ui.body.classList.toggle("p2", view.players.length === 2) ui.body.classList.toggle("p3", view.players.length === 3) + ui.body.classList.toggle("p4", view.players.length === 4) + + ui.header.classList.toggle("player_red", view.current === 0) + ui.header.classList.toggle("player_blue", view.current === 1) + ui.header.classList.toggle("player_yellow", view.current === 2) + ui.header.classList.toggle("player_green", view.current === 3) if (view.players.length < 4) hide(document.getElementById("role_Green")) @@ -670,6 +678,11 @@ function on_update() { for (let e of action_register) e.classList.toggle("action", is_action(e.my_action, e.my_id)) + + action_button("end_actions", "End Actions") + + action_button("done", "Done") + action_button("undo", "Undo") } on_init() @@ -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] = [] |