From d868bb1185b7800bf1f2b7f739d7061bfd9d9b60 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Tue, 22 Jun 2021 23:02:35 +0200 Subject: crusader: Deployment and nicer Assassin event handling. --- rules.js | 233 ++++++++++++++++++++++++++++++++++++++++++++++----------------- ui.js | 8 ++- 2 files changed, 177 insertions(+), 64 deletions(-) diff --git a/rules.js b/rules.js index 18acc0d..f75cd9c 100644 --- a/rules.js +++ b/rules.js @@ -1,23 +1,13 @@ "use strict"; -// TODO: alternate home seats when drawing -// TODO: frank seat adjustment at setup -// TODO: saladin seat adjustment at setup - // TODO: optional rule - iron bridge // TODO: optional rule - force marches -// TODO: crusader arrival movement - -// TODO: Assassins -- move assassin block and show special battle screen! - -// TODO: replace game.steps and game.location etc with block_steps() and block_location() - exports.scenarios = [ "Third Crusade" ]; -const { CARDS, BLOCKS, TOWNS, PORTS, ROADS } = require('./data'); +const { CARDS, BLOCKS, TOWNS, PORTS, ROADS, SHIELDS } = require('./data'); const FRANKS = "Franks"; const SARACENS = "Saracens"; @@ -28,7 +18,6 @@ const BOTH = "Both"; const DEAD = "Dead"; const F_POOL = "FP"; const S_POOL = "SP"; - const ENGLAND = "England"; const FRANCE = "France"; const GERMANIA = "Germania"; @@ -37,6 +26,9 @@ const TRIPOLI = "Tripoli"; const ALEPPO = "Aleppo"; const ANTIOCH = "Antioch"; const ST_SIMEON = "St. Simeon"; +const DAMASCUS = "Damascus"; +const MASYAF = "Masyaf"; +const SALADIN = "Saladin"; const INTRIGUE = 3; const WINTER_CAMPAIGN = 6; @@ -44,6 +36,7 @@ const WINTER_CAMPAIGN = 6; const ENGLISH_CRUSADERS = [ "Richard", "Robert", "Crossbows" ]; const FRENCH_CRUSADERS = [ "Philippe", "Hugues", "Fileps" ]; const GERMAN_CRUSADERS = [ "Barbarossa", "Frederik", "Leopold" ]; +const SALADIN_FAMILY = [ "Saladin", "Al Adil", "Al Aziz", "Al Afdal", "Al Zahir" ]; const VICTORY_TOWNS = [ "Aleppo", "Damascus", "Egypt", @@ -213,6 +206,16 @@ function deal_cards(deck, n) { return hand; } +function select_random_block(where) { + let list = []; + for (let b in BLOCKS) + if (game.location[b] == where) + list.push(b); + if (list.length == 0) + return null; + return list[Math.floor(Math.random() * list.length)]; +} + function block_plural(who) { return BLOCKS[who].plural; } @@ -234,6 +237,21 @@ function block_home(who) { return home; } +function list_seats(who) { + if (is_saladin_family(who)) + who = SALADIN; + switch (block_type(who)) { + case 'nomads': return [ block_home(who) ]; + case 'turcopoles': who = "Turcopoles"; break; + case 'military_orders': who = BLOCKS[who].name; break; + } + let list = []; + for (let town in SHIELDS) + if (SHIELDS[town].includes(who)) + list.push(town); + return list; +} + function block_pool(who) { if (BLOCKS[who].owner == FRANKS) return F_POOL; @@ -841,6 +859,94 @@ function reduce_block(who) { } } +// DEPLOYMENT + +function is_valid_frank_deployment() { + for (let town in TOWNS) + if (!is_within_castle_limit(town)) + return false; + return true; +} + +function goto_frank_deployment() { + game.active = FRANKS; + game.state = 'frank_deployment'; +} + +states.frank_deployment = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Deployment: Waiting for " + game.active + "."; + view.prompt = "Deployment: You may make seat adjustments."; + gen_action_undo(view); + if (is_valid_frank_deployment()) + gen_action(view, 'next'); + for (let b in BLOCKS) { + if (block_owner(b) == game.active && is_block_on_land(b)) + if (list_seats(b).length > 1) + gen_action(view, 'block', b); + } + }, + block: function (who) { + push_undo(); + game.who = who; + game.state = 'frank_deployment_to'; + }, + next: goto_saracen_deployment, + undo: pop_undo +} + +states.frank_deployment_to = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Deployment: Waiting for " + game.active + "."; + view.prompt = "Deployment: You may make seat adjustments."; + gen_action_undo(view); + gen_action(view, 'block', game.who); + let from = game.location[game.who]; + for (let town of list_seats(game.who)) + if (town != from) + gen_action(view, 'town', town); + }, + town: function (where) { + game.location[game.who] = where; + game.who = null; + game.state = 'frank_deployment'; + }, + block: pop_undo, + undo: pop_undo +} + +function goto_saracen_deployment() { + for (let i = 0; i < 4; ++i) { + let nomad = select_random_block(S_POOL); + log(BLOCKS[nomad].name + " arrive in " + block_home(nomad) + "."); + deploy(nomad, block_home(nomad)); + } + game.active = SARACENS; + game.state = 'saracen_deployment'; + game.who = SALADIN; +} + +states.saracen_deployment = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Deployment: Waiting for " + game.active + "."; + view.prompt = "Deployment: You may swap places with Saladin and any other block of his family." + gen_action(view, 'pass'); + for (let b of SALADIN_FAMILY) + if (b != SALADIN && game.location[b] != game.location[SALADIN]) + gen_action(view, 'block', b); + }, + block: function (who) { + game.location[SALADIN] = game.location[who]; + game.location[who] = DAMASCUS; + game.who = null; + start_year(); + }, + pass: start_year +} + // GAME TURN function is_friendly_town_for_vp(town) { @@ -1098,16 +1204,6 @@ function goto_assassins() { game.who = ASSASSINS; } -function select_random_block(where) { - let list = []; - for (let b in BLOCKS) - if (game.location[b] == where) - list.push(b); - if (list.length == 0) - return null; - return list[Math.floor(Math.random() * list.length)]; -} - states.assassins = { prompt: function (view, current) { if (is_inactive_player(current)) @@ -1119,31 +1215,59 @@ states.assassins = { } }, block: function (who) { - let where = game.location[who]; - - who = select_random_block(where); - - let hits = 0; - let rolls = []; - for (let i = 0; i < 3; ++i) { - let die = roll_d6(); - if (die <= 3) { - rolls.push(DIE_HIT[die]); - ++hits; - } else { - rolls.push(DIE_MISS[die]); - } - } - hits = Math.min(hits, game.steps[who]); + game.where = game.location[who]; + game.who = select_random_block(game.where); + game.location[ASSASSINS] = game.where; + game.state = 'assassins_show_1'; + }, +} - log("Assassins hit " + who + " in " + where + ": " + rolls.join("") + "."); - for (let i = 0; i < hits; ++i) - reduce_block(who); +states.assassins_show_1 = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Assassins: Waiting for " + game.active + "."; + view.prompt = "Assassins: The assassins target " + game.who + " in " + game.where + "."; + view.assassinate = game.who; + gen_action(view, 'next'); + }, + next: function () { + assassinate(game.who, game.where); + game.state = 'assassins_show_2'; + }, +} +states.assassins_show_2 = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Assassins: Waiting for " + game.active + "."; + view.prompt = "Assassins: The assassins hit " + game.who + " in " + game.where + "."; + view.assassinate = game.who; + gen_action(view, 'next'); + }, + next: function () { + game.location[ASSASSINS] = MASYAF; game.who = null; + game.where = null; end_player_turn(); }, - undo: pop_undo +} + +function assassinate(who, where) { + let hits = 0; + let rolls = []; + for (let i = 0; i < 3; ++i) { + let die = roll_d6(); + if (die <= 3) { + rolls.push(DIE_HIT[die]); + ++hits; + } else { + rolls.push(DIE_MISS[die]); + } + } + hits = Math.min(hits, game.steps[who]); + log("Assassins hit " + who + " in " + where + ": " + rolls.join("") + "."); + for (let i = 0; i < hits; ++i) + reduce_block(who); } function goto_guide() { @@ -2719,20 +2843,6 @@ function start_draw_phase() { } } -function list_seats(who) { - if (is_saladin_family(who)) - who = "Saladin"; - if (block_type(who) == 'turcopoles') - who = "Turcopoles"; - if (block_type(who) == 'nomads') - return [ block_home(who) ]; - let list = []; - for (let town in SHIELDS) - if (SHIELDS[town].includes(who)) - list.push(town); - return list.join(", "); -} - states.draw_phase = { prompt: function (view, current) { if (is_inactive_player(current)) @@ -2754,7 +2864,7 @@ states.draw_phase = { case 'emirs': case 'nomads': view.prompt = "Draw Phase: Place " + BLOCKS[game.who].name + " at full strength in " - + list_seats(game.who) + " or at strength 1 in any friendly town."; + + list_seats(game.who).join(", ") + " or at strength 1 in any friendly town."; for (let town in TOWNS) if (is_friendly_town(town)) gen_action(view, 'town', town); @@ -3062,9 +3172,6 @@ function setup_game() { deploy(b, block_pool(b)); break; case 'crusaders': - if (b != "Philippe") - deploy(b, block_home(b)); - else deploy(b, block_pool(b)); break; default: @@ -3083,6 +3190,7 @@ function setup_game() { } } count_victory_points(); + goto_frank_deployment(); } // VIEW @@ -3143,6 +3251,10 @@ exports.ready = function (scenario, players) { exports.setup = function (scenario, players) { game = { + s_hand: [], + f_hand: [], + s_card: 0, + f_card: 0, attacker: {}, road_limit: {}, last_used: {}, @@ -3162,7 +3274,6 @@ exports.setup = function (scenario, players) { undo: [], } setup_game(); - start_year(); return game; } diff --git a/ui.js b/ui.js index c93a9b8..dffb958 100644 --- a/ui.js +++ b/ui.js @@ -335,8 +335,10 @@ function build_town(t, town) { function update_map_layout() { for (let t in TOWNS) { let element = ui.towns[t]; - element.style.left = (town_x(t) - 35) + "px"; - element.style.top = (town_y(t) - 35) + "px"; + let xo = Math.round(element.offsetWidth/2) + let yo = Math.round(element.offsetHeight/2) + element.style.left = (town_x(t) - xo) + "px"; + element.style.top = (town_y(t) - yo) + "px"; } } @@ -562,7 +564,7 @@ function update_map() { let moved = game.moved[b] ? " moved" : ""; if (town == DEAD) moved = " moved"; - if (info.owner == player || info.owner == ASSASSINS) { + if (info.owner == player || info.owner == ASSASSINS || b == game.assassinate) { let image = " block_" + info.image; let steps = " r" + (info.steps - game.steps[b]); let known = " known" -- cgit v1.2.3