From 9616bee63c7c65a1e860b0dea6c7e71e57bd3b42 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Tue, 1 Jun 2021 13:56:58 +0200 Subject: 300: Start implementing rules. --- play.html | 404 +++++++++++++ rules.js | 1895 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ui.js | 576 +++++++++++++++++++ 3 files changed, 2875 insertions(+) create mode 100644 play.html create mode 100644 rules.js create mode 100644 ui.js diff --git a/play.html b/play.html new file mode 100644 index 0000000..1251a43 --- /dev/null +++ b/play.html @@ -0,0 +1,404 @@ + + + + + +300: E&W + + + + + + + + + + +
+ +
+
Chat
+
+
+
+ +
+ +
+ + + +
+
+
+ +
Connecting...
+ + + + + + + + + +
+ +
+ +
+
Greece ($USER)
+
0 cards in hand
+
+ +
+
Persia ($USER)
+
0 cards in hand
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ +
+ diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..6295bf1 --- /dev/null +++ b/rules.js @@ -0,0 +1,1895 @@ +"use strict"; + +// Diary: 2021-04-23 - Friday Evening - Started game logic shell. +// Diary: 2021-04-24 - Saturday - Art, UI, preparation phase. +// Diary: 2021-04-25 - Sunday - Supply, movement and battle. +// Diary: 2021-04-26 - Monday Evening - Redid piece layout. Transport armies on fleets. + +// TODO: rewrite battle and movement states to common with is_friendly/etc +// TODO: undo in preparation phase +// TODO: separate land/port moves? + +exports.scenarios = [ + "Default" +]; + +const OBSERVER = "Observer"; +const GREECE = "Greece"; +const PERSIA = "Persia"; + +const RESERVE = "reserve"; +const ABYDOS = "Abydos"; +const EPHESOS = "Ephesos"; +const ATHENAI = "Athenai"; +const SPARTA = "Sparta"; +const PELLA = "Pella"; + +const SUDDEN_DEATH_OF_THE_GREAT_KING = 11; + +const CITIES = [ + "Abydos", + "Athenai", + "Delphi", + "Ephesos", + "Eretria", + "Korinthos", + "Larissa", + "Naxos", + "Pella", + "Sparta", + "Thebai", +]; + +const PORTS = [ + "Abydos", + "Athenai", + "Ephesos", + "Eretria", + "Naxos", + "Pella", + "Sparta", + "Thebai", +]; + +const SUPPLY = { + "Abydos": 3, + "Athenai": 2, + "Delphi": 1, + "Ephesos": 3, + "Eretria": 1, + "Korinthos": 1, + "Larissa": 1, + "Naxos": 1, + "Pella": 1, + "Sparta": 2, + "Thebai": 1, +}; + +const SCORE = { + "Abydos": 2, + "Athenai": 2, + "Delphi": 1, + "Ephesos": 2, + "Eretria": 1, + "Korinthos": 1, + "Larissa": 1, + "Naxos": 1, + "Pella": 1, + "Sparta": 2, + "Thebai": 1, +}; + +const ROADS = { + "Abydos": [ "Ephesos", "Pella" ], + "Athenai": [ "Korinthos", "Thebai" ], + "Delphi": [ "Larissa", "Thebai" ], + "Ephesos": [ "Abydos" ], + "Eretria": [], + "Korinthos": [ "Athenai", "Sparta" ], + "Larissa": [ "Delphi", "Pella", "Thebai" ], + "Naxos": [], + "Pella": [ "Abydos", "Larissa" ], + "Sparta": [ "Korinthos" ], + "Thebai": [ "Athenai", "Delphi", "Korinthos", "Larissa" ], +}; + +let states = {}; +let game = null; + +function $(msg) { + return msg + .replace(/ 1 cards/, " 1 card") + .replace(/ 1 armies/, " 1 army") + .replace(/ 1 fleets/, " 1 fleet") + .replace(/ 1 talents/, " 1 talent") + .replace(/ 1 points/, " 1 point"); +} + +function remove_from_array(array, item) { + let i = array.indexOf(item); + if (i >= 0) + array.splice(i, 1); +} + +function log(...args) { + let s = Array.from(args).join(""); + game.log.push($(s)); +} + + +function clear_undo() { + if (game.undo) + game.undo.length = 0; + else + game.undo = []; +} + +function push_undo() { + game.undo.push(JSON.stringify(game, (k,v) => { + if (k === 'undo') return undefined; + if (k === 'log') return v.length; + return v; + })); +} + +function pop_undo() { + let undo = game.undo; + let log = game.log; + Object.assign(game, JSON.parse(undo.pop())); + game.undo = undo; + log.length = game.log; + game.log = log; +} + +function gen_action_undo(view) { + if (!view.actions) + view.actions = {} + if (game.undo && game.undo.length > 0) + view.actions.undo = 1; + else + view.actions.undo = 0; +} + +function is_inactive_player(current) { + return current == OBSERVER || game.active != current; +} + +function gen_action(view, action, argument) { + if (!view.actions) + view.actions = {} + if (argument != undefined) { + if (!(action in view.actions)) { + view.actions[action] = [ argument ]; + } else { + if (!view.actions[action].includes(argument)) + view.actions[action].push(argument); + } + } else { + view.actions[action] = 1; + } +} + +function roll_d6() { + return Math.floor(Math.random() * 6) + 1; +} + +function create_deck() { + let deck = []; + for (let c = 1; c <= 16; ++c) + deck.push(c); + return deck; +} + +function reshuffle() { + log("The deck is reshuffled."); + while (game.discard.length > 0) + game.deck.push(game.discard.pop()); +} + +function draw_card(deck) { + if (deck.length == 0) + reshuffle(); + if (deck.length == 0) + throw Error("can't draw from empty deck"); + let k = Math.floor(Math.random() * deck.length); + let card = deck[k]; + deck.splice(k, 1); + return card; +} + +function can_draw_card(extra) { + return game.deck.length + game.discard.length - extra > 0; +} + +function discard_card(who, hand, card) { + log(who + " discards card " + card + "."); + remove_from_array(hand, card); + game.discard.push(card); +} + +function play_card(who, hand, card, reason) { + log(who + " plays card " + card + " " + reason); + remove_from_array(hand, card); + game.discard.push(card); +} + +function add_vp(delta) { + // greek vp is negative + game.vp += delta; + if (game.vp < -6) game.vp = -6; + if (game.vp > 6) game.vp = 6; +} + +function add_greek_vp(n=1) { + add_vp(-n); +} + +function add_persian_vp(n=1) { + add_vp(n); +} + +function count_greek_armies(where) { return game.units[where][0] | 0; } +function count_persian_armies(where) { return game.units[where][1] | 0; } +function count_greek_fleets(where) { return game.units[where][2] | 0; } +function count_persian_fleets(where) { return game.units[where][3] | 0; } + +function remove_greek_army(from) { + log("Greece removes army from " + from + "."); + game.units[from][0] -= 1; +} + +function remove_persian_army(from) { + log("Persia removes army from " + from + "."); + game.units[from][1] -= 1; +} + +function remove_greek_fleet(from) { + log("Greece removes fleet from " + from + "."); + game.units[from][2] -= 1; +} + +function remove_persian_fleet(from) { + log("Persia removes fleet from " + from + "."); + game.units[from][3] -= 1; +} + +function move_greek_army(from, to, n = 1) { + game.units[from][0] -= n; + game.units[to][0] += n; +} + +function move_persian_army(from, to, n = 1) { + game.units[from][1] -= n; + game.units[to][1] += n; +} + +function move_greek_fleet(from, to, n = 1) { + game.units[from][2] -= n; + game.units[to][2] += n; +} + +function move_persian_fleet(from, to, n = 1) { + game.units[from][3] -= n; + game.units[to][3] += n; +} + +function is_persian_control(where) { + if (where == ABYDOS || where == EPHESOS) + return count_greek_armies(where) == 0; + return count_persian_armies(where) > 0; +} + +function is_greek_control(where) { + if (where == ATHENAI || where == SPARTA) + return count_persian_armies(where) == 0; + return count_greek_armies(where) > 0; +} + +function gen_greek_cities(view) { + for (let city of CITIES) + if (is_greek_control(city)) + gen_action(view, 'city', city); +} + +function gen_persian_cities(view) { + for (let city of CITIES) + if (is_persian_control(city)) + gen_action(view, 'city', city); +} + +function gen_greek_armies(view) { + for (let city of CITIES) + if (count_greek_armies(city) > 0) + gen_action(view, 'city', city); +} + +function gen_persian_armies(view) { + for (let city of CITIES) + if (count_persian_armies(city) > 0) + gen_action(view, 'city', city); +} + +function gen_greek_fleets(view) { + for (let port of PORTS) + if (count_greek_fleets(port) > 0) + gen_action(view, 'port', port); +} + +function gen_persian_fleets(view) { + for (let port of PORTS) + if (count_persian_fleets(port) > 0) + gen_action(view, 'port', port); +} + +// DEATH OF A KING + +function discard_persian_hand() { + for (let c of game.persian.hand) + game.discard.push(c); + game.persian.hand.length = 0; +} + +function goto_sudden_death_of_darius() { + game.trigger.darius = 1; + log("Sudden Death of Darius!"); + game.state = 'sudden_death_of_darius'; + if (count_persian_armies(RESERVE) > 0) { + remove_persian_army(RESERVE); + game.remove_army = 0; + } else { + game.remove_army = 1; + } +} + +states.sudden_death_of_darius = { + prompt: function (view, current) { + view.prompt = "Sudden Death of Darius!"; + if (is_inactive_player(current)) + return; + if (game.remove_army) { + view.prompt += " Remove one army."; + gen_persian_armies(view); + } else { + gen_action(view, 'next'); + } + }, + city: function (space) { + remove_persian_army(space); + game.remove_army = 0; + }, + next: function () { + discard_persian_hand(); + reshuffle(); + end_campaign(); + }, +} + +function goto_assassination_of_xerxes() { + game.trigger.xerxes = 1; + log("Assassination of Xerxes!"); + game.state = 'assassination_of_xerxes'; + if (count_persian_armies(RESERVE) > 0) { + remove_persian_army(RESERVE); + game.remove_army = 0; + } else { + game.remove_army = 1; + } +} + +states.assassination_of_xerxes = { + prompt: function (view, current) { + view.prompt = "Assassination of Xerxes!"; + if (is_inactive_player(current)) + return; + if (game.remove_army) { + view.prompt += " Remove one army."; + gen_persian_armies(view); + } else { + gen_action(view, 'next'); + } + }, + city: function (space) { + remove_persian_army(space); + game.remove_army = 0; + }, + next: function () { + discard_persian_hand(); + reshuffle(); + end_campaign(); + }, +} + +// PREPARATION PHASE + +function start_campaign() { + log(""); + log("Start Campaign " + game.campaign); + goto_persian_preparation_draw(); +} + +function goto_persian_preparation_draw() { + game.active = PERSIA; + game.state = 'persian_preparation_draw'; + if (game.persian.hand.length > 0) + game.talents = 10; + else + game.talents = 12; + game.persian.draw = 0; +} + +function goto_greek_preparation_draw() { + game.active = GREECE; + game.state = 'greek_preparation_draw'; + game.talents = 6; + game.greek.draw = 0; +} + +states.persian_preparation_draw = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Preparation Phase."; + view.prompt = "Persian Preparation Phase: Draw up to 6 cards. " + game.talents + " talents left."; + if (game.persian.draw < 6 && game.talents >= 1 && can_draw_card(game.persian.draw)) + gen_action(view, 'draw'); + gen_action_undo(view); + gen_action(view, 'next'); + }, + draw: function () { + push_undo(); + --game.talents; + ++game.persian.draw; + }, + next: function () { + log("Persia draws " + game.persian.draw + " cards."); + let sudden_death = 0; + for (let i = 0; i < game.persian.draw; ++i) { + let card = draw_card(game.deck); + if (card == SUDDEN_DEATH_OF_THE_GREAT_KING) + sudden_death = 1; + game.persian.hand.push(card); + } + game.persian.draw = 0; + if (sudden_death) { + if (!game.trigger.darius) + return goto_sudden_death_of_darius(); + if (!game.trigger.xerxes) + return goto_assassination_of_xerxes(); + } + goto_persian_preparation_build(); + }, + undo: pop_undo, +} + +states.greek_preparation_draw = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Preparation Phase."; + view.prompt = "Greek Preparation Phase: Draw up to 6 cards. " + game.talents + " talents left."; + if (game.greek.draw < 6 && game.talents >= 1 && can_draw_card(game.greek.draw)) + gen_action(view, 'draw'); + gen_action_undo(view); + gen_action(view, 'next'); + }, + draw: function () { + push_undo(); + --game.talents; + ++game.greek.draw; + }, + next: function () { + log("Greece draws " + game.greek.draw + " cards."); + for (let i = 0; i < game.greek.draw; ++i) { + let card = draw_card(game.deck); + game.greek.hand.push(card); + } + game.greek.draw = 0; + goto_greek_preparation_build(); + }, + undo: pop_undo, +} + +function goto_persian_preparation_build() { + game.active = PERSIA; + game.state = 'persian_preparation_build'; + game.built_fleets = 0; +} + +states.persian_preparation_build = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Preparation Phase."; + view.prompt = "Persian Preparation Phase: Build fleets, armies, and/or the bridge. "; + view.prompt += game.talents + " talents left."; + if (game.talents >= 1 && count_persian_armies(RESERVE) > 0) { + for (let space of CITIES) + if (is_persian_control(space)) + gen_action(view, 'city', space); + } + if (game.built_fleets < 2 && game.talents >= 2 && count_persian_fleets(RESERVE) > 0) { + for (let space of PORTS) + if (is_persian_control(space) && count_greek_fleets(space) == 0) + gen_action(view, 'port', space); + } + if (!game.trigger.hellespont && game.talents >= 6 && is_persian_control(ABYDOS)) { + gen_action(view, 'build'); + } + gen_action(view, 'next'); + gen_action_undo(view); + }, + city: function (space) { + push_undo(); + log("Persia builds an army in " + space + "."); + game.talents -= 1; + move_persian_army(RESERVE, space); + }, + port: function (space) { + push_undo(); + log("Persia builds a fleet in " + space + "."); + game.built_fleets += 1; + game.talents -= 2; + move_persian_fleet(RESERVE, space); + }, + build: function () { + push_undo(); + log("Persia builds the pontoon bridge."); + game.talents -= 6; + game.trigger.hellespont = 1; + }, + next: function () { + clear_undo(); + game.talents = 0; + goto_greek_preparation_draw(); + }, + undo: pop_undo, +} + +function goto_greek_preparation_build() { + game.active = GREECE; + game.state = 'greek_preparation_build'; + game.built_fleets = 0; +} + +states.greek_preparation_build = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Preparation Phase."; + view.prompt = "Greek Preparation Phase: Build fleets and armies. "; + view.prompt += game.talents + " talents left."; + if (game.talents >= 1 && count_greek_armies(RESERVE) > 0) { + for (let space of CITIES) + if (is_greek_control(space)) + gen_action(view, 'city', space); + } + if (game.built_fleets < 2 && game.talents >= 1 && count_greek_fleets(RESERVE) > 0) { + for (let space of PORTS) + if (is_greek_control(space) && count_persian_fleets(space) == 0) + gen_action(view, 'port', space); + } + gen_action_undo(view); + gen_action(view, 'next'); + }, + city: function (space) { + push_undo(); + log("Greece builds an army in " + space + "."); + game.talents -= 1; + move_greek_army(RESERVE, space); + }, + port: function (space) { + push_undo(); + log("Greece builds a fleet in " + space + "."); + game.built_fleets += 1; + game.talents -= 1; + move_greek_fleet(RESERVE, space); + }, + next: function () { + clear_undo(); + game.talents = 0; + end_preparation_phase(); + }, + undo: pop_undo, +} + +function end_preparation_phase() { + game.persian.pass = 0; + game.greek.pass = 0; + goto_persian_operation(); +} + +// OPERATIONS PHASE + +function goto_greek_operation() { + if (game.greek.hand.length > 0) { + game.active = GREECE; + game.state = 'greek_operation'; + game.greek.pass = 0; + } else { + log("Greece passes automatically."); + game.greek.pass = 1; + end_greek_operation(); + } +} + +function goto_persian_operation() { + if (game.persian.hand.length > 0) { + game.active = PERSIA; + game.state = 'persian_operation'; + game.persian.pass = 0; + } else { + log("Persia passes automatically."); + game.persian.pass = 1; + end_persian_operation(); + } +} + +states.persian_operation = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Operation Phase."; + view.prompt = "Persian Operation Phase: Play a card or pass."; + for (let card of game.persian.hand) { + gen_action(view, 'card_move', card); + if (can_play_persian_event(card)) + gen_action(view, 'card_event', card); + } + gen_action(view, 'pass'); + }, + card_move: function (card) { + play_card("Persia", game.persian.hand, card, " for movement."); + game.state = 'persian_movement'; + }, + pass: function () { + log("Persia passes."); + game.persian.pass = 1; + end_persian_operation(); + }, +} + +states.greek_operation = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Operation Phase."; + view.prompt = "Greek Operation Phase: Play a card or pass."; + for (let card of game.greek.hand) { + gen_action(view, 'card_move', card); + if (can_play_greek_event(card)) + gen_action(view, 'card_event', card); + } + gen_action(view, 'pass'); + }, + card_move: function (card) { + play_card("Greece", game.greek.hand, card, " for movement."); + game.state = 'greek_movement'; + }, + pass: function () { + log("Greece passes."); + game.greek.pass = 1; + end_greek_operation(); + }, +} + +function end_persian_operation() { + game.move_list = null; + game.transport = 0; + game.attacker = 0; + if (game.persian.pass && game.greek.pass) + return end_operation_phase(); + goto_greek_operation(); +} + +function end_greek_operation() { + game.move_list = null; + game.transport = 0; + game.attacker = 0; + if (game.persian.pass && game.greek.pass) + return end_operation_phase(); + goto_persian_operation(); +} + +function end_operation_phase() { + game.persian.pass = 0; + game.greek.pass = 0; + goto_supply_phase(); +} + +// MOVEMENT + +function is_usable_road(from, to) { + if (from == ABYDOS && to == PELLA && !game.trigger.hellespont) + return false; + if (from == PELLA && to == ABYDOS && !game.trigger.hellespont) + return false; + return true; +} + +function list_persian_land_moves(seen, from) { + seen[from] = 1; + if (is_persian_control(from)) + for (let to of ROADS[from]) + if (is_usable_road(from, to) && !seen[to]) + list_persian_land_moves(seen, to); +} + +function list_greek_land_moves(seen, from) { + seen[from] = 1; + if (is_greek_control(from)) + for (let to of ROADS[from]) + if (is_usable_road(from, to) && !seen[to]) + list_greek_land_moves(seen, to); +} + +states.persian_movement = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Movement."; + view.prompt = "Persian Movement: Choose an origin."; + gen_persian_armies(view); + gen_persian_fleets(view); + gen_action(view, 'pass'); + }, + city: function (space) { + goto_persian_land_movement(space); + }, + port: function (space) { + game.from = space; + game.state = 'persian_naval_movement'; + }, + pass: function () { + end_persian_operation(); + }, +} + +states.greek_movement = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Movement."; + view.prompt = "Greek Movement: Choose an origin."; + gen_greek_armies(view); + gen_greek_fleets(view); + gen_action(view, 'pass'); + }, + city: function (space) { + goto_greek_land_movement(space); + }, + port: function (space) { + game.from = space; + game.state = 'greek_naval_movement'; + }, + pass: function () { + end_greek_operation(); + }, +} + +function goto_persian_land_movement(space) { + game.from = space; + game.state = 'persian_land_movement'; + list_persian_land_moves(game.move_list = {}, game.from); +} + +function goto_greek_land_movement(space) { + game.from = space; + game.state = 'greek_land_movement'; + list_greek_land_moves(game.move_list = {}, game.from); +} + +states.persian_land_movement = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Land Movement."; + view.prompt = "Persian Land Movement: Select armies to move and then a destination."; + view.land_movement = game.from; + for (let to in game.move_list) + if (to != game.from) + gen_action(view, 'city', to); + gen_action(view, 'undo'); + }, + city: function ([to, armies]) { + push_undo(); + log("Persia moves " + armies + " armies from " + game.from + " to " + to + "."); + move_persian_army(game.from, to, armies); + game.where = to; + game.state = 'persian_land_movement_confirm'; + }, + pass: function () { + end_persian_operation(); + }, + undo: function () { + game.from = 0; + game.state = 'persian_movement'; + }, +} + +states.greek_land_movement = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Land Movement."; + view.prompt = "Greek Land Movement: Select armies to move and then a destination."; + view.land_movement = game.from; + for (let to in game.move_list) + if (to != game.from) + gen_action(view, 'city', to); + gen_action(view, 'undo'); + }, + city: function ([to, armies]) { + push_undo(); + log("Greece moves " + armies + " armies from " + game.from + " to " + to + "."); + move_greek_army(game.from, to, armies); + game.where = to; + game.state = 'greek_land_movement_confirm'; + }, + pass: function () { + end_greek_operation(); + }, + undo: function () { + game.from = 0; + game.state = 'greek_movement'; + }, +} + +states.persian_land_movement_confirm = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Land Movement."; + view.prompt = "Persian Land Movement: Confirm destination."; + gen_action(view, 'city', game.where); + gen_action(view, 'next'); + gen_action(view, 'undo'); + }, + city: function () { + clear_undo(); + goto_persian_land_battle(); + }, + next: function () { + clear_undo(); + goto_persian_land_battle(); + }, + undo: pop_undo, +} + +states.greek_land_movement_confirm = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Land Movement."; + view.prompt = "Greek Land Movement: Confirm destination."; + gen_action(view, 'city', game.where); + gen_action(view, 'next'); + gen_action(view, 'undo'); + }, + city: function () { + clear_undo(); + goto_greek_land_battle(); + }, + next: function () { + clear_undo(); + goto_greek_land_battle(); + }, + undo: pop_undo, +} + +states.persian_naval_movement = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Naval Movement."; + view.prompt = "Persian Naval Movement: Select fleets to move, armies to transport, and then a destination."; + view.naval_movement = game.from; + for (let port of PORTS) + if (port != game.from) + gen_action(view, 'port', port); + gen_action(view, 'undo'); + }, + port: function ([to, fleets, armies]) { + push_undo(); + log("Persia moves " + fleets + " fleets and " + armies + " armies from " + game.from + " to " + to + "."); + move_persian_fleet(game.from, to, fleets); + move_persian_army(game.from, to, armies); + game.transport = armies; + game.attacker = PERSIA; + game.where = to; + game.state = 'persian_naval_movement_confirm'; + }, + pass: function () { + end_persian_operation(); + }, + undo: function () { + game.from = 0; + game.state = 'persian_movement'; + }, +} + +states.greek_naval_movement = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Naval Movement."; + view.prompt = "Greek Naval Movement: Select fleets to move, armies to transport, and then a destination."; + view.naval_movement = game.from; + for (let port of PORTS) + if (port != game.from) + gen_action(view, 'port', port); + gen_action(view, 'undo'); + }, + port: function ([to, fleets, armies]) { + push_undo(); + log("Greece moves " + fleets + " fleets and " + armies + " armies from " + game.from + " to " + to + "."); + move_greek_fleet(game.from, to, fleets); + move_greek_army(game.from, to, armies); + game.transport = armies; + game.attacker = GREECE; + game.where = to; + game.state = 'greek_naval_movement_confirm'; + }, + pass: function () { + end_greek_operation(); + }, + undo: function () { + game.from = 0; + game.state = 'greek_movement'; + }, +} + +states.persian_naval_movement_confirm = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Naval Movement."; + view.prompt = "Persian Naval Movement: Confirm destination."; + gen_action(view, 'port', game.where); + gen_action(view, 'next'); + gen_action(view, 'undo'); + }, + port: function () { + clear_undo(); + goto_persian_naval_battle(); + }, + next: function () { + clear_undo(); + goto_persian_naval_battle(); + }, + undo: pop_undo, +} + +states.greek_naval_movement_confirm = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Naval Movement."; + view.prompt = "Greek Naval Movement: Confirm destination."; + gen_action(view, 'port', game.where); + gen_action(view, 'next'); + gen_action(view, 'undo'); + }, + port: function () { + clear_undo(); + goto_greek_naval_battle(); + }, + next: function () { + clear_undo(); + goto_greek_naval_battle(); + }, + undo: pop_undo, +} + +// NAVAL BATTLE + +function roll_battle_dice(who, count, cap) { + count = Math.min(3, count); + let rolls = []; + let result = 0; + for (let i = 0; i < count; ++i) { + let die = roll_d6(); + rolls.push(die); + die = Math.min(die, cap); + if (die > result) + result = die; + } + log(who + " rolls " + rolls.join(", ") + " = " + result + "."); + return result; +} + +function goto_persian_naval_battle() { + game.naval_battle = 1; + if (count_greek_fleets(game.where) > 0 && count_persian_fleets(game.where) > 0) + goto_persian_naval_battle_react(); + else + goto_persian_land_battle(); +} + +function goto_greek_naval_battle() { + game.naval_battle = 1; + if (count_greek_fleets(game.where) > 0 && count_persian_fleets(game.where) > 0) + goto_greek_naval_battle_react(); + else + goto_greek_land_battle(); +} + +function goto_persian_naval_battle_react() { + game.active = GREECE; + game.state = 'persian_naval_battle_react'; +} + +function goto_greek_naval_battle_react() { + game.active = PERSIA; + game.state = 'greek_naval_battle_react'; +} + +states.persian_naval_battle_react = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Naval Battle: Persia attacks " + game.where + "!"; + view.prompt = "Persian Naval Battle: Persia attacks " + game.where + " with " + + count_persian_fleets(game.where) + " fleets and " + + count_persian_armies(game.where) + " armies!"; + gen_action(view, 'next'); + }, + next: function () { + game.active = PERSIA; + persian_naval_battle_round(); + }, +} + +states.greek_naval_battle_react = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Naval Battle: Greece attacks " + game.where + "!"; + view.prompt = "Greek Naval Battle: Greece attacks " + game.where + " with " + + count_greek_fleets(game.where) + " fleets and " + + count_greek_armies(game.where) + " armies!"; + gen_action(view, 'next'); + }, + next: function () { + game.active = GREECE; + greek_naval_battle_round(); + }, +} + +function persian_naval_battle_round() { + log("Persia attacks " + game.where + " at sea!"); + let p_max = (game.where == ABYDOS || game.where == EPHESOS) ? 5 : 4; + let g_max = 6; + let p_hit = roll_battle_dice("Persia", count_persian_fleets(game.where), p_max); + let g_hit = roll_battle_dice("Greece", count_greek_fleets(game.where), g_max); + if (p_hit >= g_hit) { + log("Greece loses one fleet."); + move_greek_fleet(game.where, RESERVE); + } + if (g_hit >= p_hit) { + log("Persia loses one fleet."); + move_persian_fleet(game.where, RESERVE); + while (count_persian_fleets(game.where) < game.transport) { + log("Persia loses one army."); + move_persian_army(game.where, RESERVE); + --game.transport; + } + } + if (count_greek_fleets(game.where) > 0 && count_persian_fleets(game.where) > 0) { + game.state = 'persian_naval_retreat_attacker'; + } else { + goto_persian_land_battle(game.where); + } +} + +function greek_naval_battle_round() { + log("Greece attacks " + game.where + " at sea!"); + let p_max = (game.where == ABYDOS || game.where == EPHESOS) ? 5 : 4; + let g_max = 6; + let p_hit = roll_battle_dice("Persia", count_persian_fleets(game.where), p_max); + let g_hit = roll_battle_dice("Greece", count_greek_fleets(game.where), g_max); + if (p_hit >= g_hit) { + log("Greece loses one fleet."); + move_greek_fleet(game.where, RESERVE); + while (count_greek_fleets(game.where) < game.transport) { + log("Greece loses one army."); + move_greek_army(game.where, RESERVE); + --game.transport; + } + } + if (g_hit >= p_hit) { + log("Persia loses one fleet."); + move_persian_fleet(game.where, RESERVE); + } + if (count_greek_fleets(game.where) > 0 && count_persian_fleets(game.where) > 0) { + game.state = 'greek_naval_retreat_attacker'; + } else { + goto_greek_land_battle(game.where); + } +} + +states.persian_naval_retreat_attacker = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Naval Battle: Attacker retreat?"; + view.prompt = "Persian Naval Battle: Continue the battle in " + game.from + " or retreat?"; + gen_action(view, 'port', game.from); + gen_action(view, 'port', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + port: function (to) { + if (to != game.where) { + log("Persia retreats to " + game.from + "."); + move_persian_fleet(game.where, game.from, count_persian_fleets(game.where)); + move_persian_army(game.where, game.from, game.transport); + end_battle(); + } else { + game.active = GREECE; + game.state = 'persian_naval_retreat_defender'; + } + }, + battle: function () { + game.active = GREECE; + game.state = 'persian_naval_retreat_defender'; + }, +} + +states.greek_naval_retreat_attacker = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Naval Battle: Attacker retreat?"; + view.prompt = "Greek Naval Battle: Continue the battle in " + game.from + " or retreat?"; + gen_action(view, 'port', game.from); + gen_action(view, 'port', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + port: function (to) { + if (to != game.where) { + log("Greece retreats to " + game.from + "."); + move_greek_fleet(game.where, game.from, count_greek_fleets(game.where)); + move_greek_army(game.where, game.from, game.transport); + end_battle(); + } else { + game.active = PERSIA; + game.state = 'greek_naval_retreat_defender'; + } + }, + battle: function () { + game.active = PERSIA; + game.state = 'greek_naval_retreat_defender'; + }, +} + +states.persian_naval_retreat_defender = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Naval Battle: Defender retreat?"; + view.prompt = "Persian Naval Battle: Continue the battle in " + game.from + " or retreat?"; + for (let port of PORTS) + if (is_greek_control(port)) + gen_action(view, 'port', port); + gen_action(view, 'port', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + port: function (to) { + game.active = PERSIA; + if (to != game.where) { + log("Greek fleets retreat to " + to + "."); + move_greek_fleet(game.where, to, count_greek_fleets(game.where)); + goto_persian_land_battle(); + } else { + persian_naval_battle_round(); + } + }, + battle: function () { + game.active = PERSIA; + persian_naval_battle_round(); + }, +} + +states.greek_naval_retreat_defender = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Naval Battle: Defender retreat?"; + view.prompt = "Greek Naval Battle: Continue the battle in " + game.from + " or retreat?"; + for (let port of PORTS) + if (is_greek_control(port)) + gen_action(view, 'port', port); + gen_action(view, 'port', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + port: function (to) { + game.active = GREECE; + if (to != game.where) { + log("Persian fleets retreat to " + to + "."); + move_greek_fleet(game.where, to, count_greek_fleets(game.where)); + goto_greek_land_battle(); + } else { + greek_naval_battle_round(); + } + }, + battle: function () { + game.active = GREECE; + greek_naval_battle_round(); + }, +} + +// LAND BATTLE + +function goto_persian_land_battle() { + game.transport = 0; + if (count_greek_armies(game.where) > 0 && count_persian_armies(game.where) > 0) { + goto_persian_land_battle_react(); + } else { + game.from = null; + end_persian_operation(); + } +} + +function goto_greek_land_battle() { + game.transport = 0; + if (count_greek_armies(game.where) > 0 && count_persian_armies(game.where) > 0) { + goto_greek_land_battle_react(); + } else { + game.from = null; + end_greek_operation(); + } +} + +function goto_persian_land_battle_react() { + game.active = GREECE; + game.state = 'persian_land_battle_react'; +} + +function goto_greek_land_battle_react() { + game.active = PERSIA; + game.state = 'greek_land_battle_react'; +} + +states.persian_land_battle_react = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Land Battle: Persia attacks " + game.where + "!"; + view.prompt = "Persian Land Battle: Persia attacks " + game.where + " with " + + count_persian_armies(game.where) + " armies!"; + gen_action(view, 'next'); + }, + next: function () { + game.active = PERSIA; + persian_land_battle_round(); + }, +} + +states.greek_land_battle_react = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Land Battle: Greece attacks " + game.where + "!"; + view.prompt = "Greek Land Battle: Greece attacks " + game.where + " with " + + count_greek_armies(game.where) + " armies!"; + gen_action(view, 'next'); + }, + next: function () { + game.active = GREECE; + greek_land_battle_round(); + }, +} + +function persian_land_battle_round() { + log("Persia attacks " + game.where + "!"); + let p_max = (game.where == ABYDOS || game.where == EPHESOS) ? 5 : 4; + let g_max = 6; + let p_hit = roll_battle_dice("Persia", count_persian_armies(game.where), p_max); + let g_hit = roll_battle_dice("Greece", count_greek_armies(game.where), g_max); + if (p_hit >= g_hit) { + log("Greece loses one army."); + move_greek_army(game.where, RESERVE); + } + if (g_hit >= p_hit) { + log("Persia loses one army."); + move_persian_army(game.where, RESERVE); + } + if (count_greek_armies(game.where) > 0 && count_persian_armies(game.where) > 0) + game.state = 'persian_land_retreat_attacker'; + else + end_battle(); +} + +function greek_land_battle_round() { + log("Greece attacks " + game.where + "!"); + let p_max = (game.where == ABYDOS || game.where == EPHESOS) ? 5 : 4; + let g_max = 6; + let p_hit = roll_battle_dice("Persia", count_persian_armies(game.where), p_max); + let g_hit = roll_battle_dice("Greece", count_greek_armies(game.where), g_max); + if (p_hit >= g_hit) { + log("Greece loses one army."); + move_greek_army(game.where, RESERVE); + } + if (g_hit >= p_hit) { + log("Persia loses one army."); + move_persian_army(game.where, RESERVE); + } + if (count_greek_armies(game.where) > 0 && count_persian_armies(game.where) > 0) + game.state = 'greek_land_retreat_attacker'; + else + end_battle(); +} + +states.persian_land_retreat_attacker = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Land Battle: Attacker retreat?"; + view.prompt = "Persian Land Battle: Continue the battle in " + game.from + " or retreat?"; + if (game.naval_battle) { + gen_action(view, 'port', game.from); + } else { + for (let city of ROADS[game.where]) + if (city in game.move_list) + gen_action(view, 'city', city); + } + gen_action(view, 'city', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + city: function (to) { + if (to != game.where) { + log("Persia retreats to " + to + "."); + move_persian_army(game.where, to, count_persian_armies(game.where)); + end_battle(); + } else { + game.active = GREECE; + game.state = 'persian_land_retreat_defender'; + } + }, + port: function (to) { + log("Persia retreats to " + to + "."); + move_persian_fleet(game.where, to, count_persian_fleets(game.where)); + move_persian_army(game.where, to, count_persian_armies(game.where)); + end_battle(); + }, + battle: function () { + game.active = GREECE; + game.state = 'persian_land_retreat_defender'; + }, +} + +states.greek_land_retreat_attacker = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Land Battle: Attacker retreat?"; + view.prompt = "Greek Land Battle: Continue the battle in " + game.from + " or retreat?"; + if (game.naval_battle) { + gen_action(view, 'port', game.from); + } else { + for (let city of ROADS[game.where]) + if (city in game.move_list) + gen_action(view, 'city', city); + } + gen_action(view, 'city', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + city: function (to) { + if (to != game.where) { + log("Greece retreats to " + to + "."); + move_greek_army(game.where, to, count_greek_armies(game.where)); + end_battle(); + } else { + game.active = PERSIA; + game.state = 'greek_land_retreat_defender'; + } + }, + port: function (to) { + log("Greece retreats to " + to + "."); + move_greek_fleet(game.where, to, count_greek_fleets(game.where)); + move_greek_army(game.where, to, count_greek_armies(game.where)); + end_battle(); + }, + battle: function () { + game.active = PERSIA; + game.state = 'greek_land_retreat_defender'; + }, +} + +states.persian_land_retreat_defender = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Land Battle: Defender retreat?"; + view.prompt = "Persian Land Battle: Continue the battle in " + game.from + " or retreat?"; + // retreat by land + for (let city of ROADS[game.where]) { + if (is_usable_road(game.where, city) && is_greek_control(city)) + gen_action(view, 'city', city); + } + // retreat by sea + if (count_greek_armies(game.where) <= count_greek_fleets(game.where)) { + for (let port of PORTS) + if (port != game.where && is_greek_control(port) && count_persian_fleets(port) == 0) + gen_action(view, 'port', port); + } + gen_action(view, 'city', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + city: function (to) { + game.active = PERSIA; + if (to != game.where) { + log("Greek armies retreat to " + to + "."); + move_greek_army(game.where, to, count_greek_armies(game.where)); + end_battle(); + } else { + persian_land_battle_round(); + } + }, + port: function (to) { + game.active = PERSIA; + log("Greek armies and fleets retreat to " + to + "."); + move_greek_fleet(game.where, to, count_greek_fleets(game.where)); + move_greek_army(game.where, to, count_greek_armies(game.where)); + end_battle(); + }, + battle: function () { + game.active = PERSIA; + persian_land_battle_round(); + }, +} + +states.greek_land_retreat_defender = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Land Battle: Defender retreat?"; + view.prompt = "Greek Land Battle: Continue the battle in " + game.from + " or retreat?"; + // retreat by land + for (let city of ROADS[game.where]) { + if (is_usable_road(game.where, city) && is_persian_control(city)) + gen_action(view, 'city', city); + } + // retreat by sea + if (count_persian_armies(game.where) <= count_persian_fleets(game.where)) { + for (let port of PORTS) + if (port != game.where && is_persian_control(port) && count_greek_fleets(port) == 0) + gen_action(view, 'port', port); + } + gen_action(view, 'city', game.where); // shortcut for battle + gen_action(view, 'battle'); + }, + city: function (to) { + game.active = GREECE; + if (to != game.where) { + log("Persian armies retreat to " + to + "."); + move_persian_army(game.where, to, count_persian_armies(game.where)); + end_battle(); + } else { + greek_land_battle_round(); + } + }, + port: function (to) { + game.active = GREECE; + log("Persian armies and fleets retreat to " + to + "."); + move_persian_fleet(game.where, to, count_persian_fleets(game.where)); + move_persian_army(game.where, to, count_persian_armies(game.where)); + end_battle(); + }, + battle: function () { + game.active = GREECE; + greek_land_battle_round(); + }, +} + +function end_battle() { + game.naval_battle = 0; + game.from = null; + if (game.active == PERSIA) { + game.where = null; + end_persian_operation(); + } else { + if (game.where == ABYDOS && is_greek_control(ABYDOS) && game.trigger.hellespont) { + game.where = null; + game.state = 'destroy_bridge'; + } else { + game.where = null; + end_greek_operation(); + } + } +} + +states.destroy_bridge = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Land Battle: Destroy bridge?"; + view.prompt = "Greek Land Battle: Destroy the Hellespont pontoon bridge?"; + gen_action(view, 'destroy'); + gen_action(view, 'pass'); + }, + destroy: function () { + log("Greece destroys the bridge!"); + game.trigger.hellespont = 0; + end_greek_operation(); + }, + pass: function () { + end_greek_operation(); + }, +} + +// PERSIAN EVENTS + +function can_play_persian_event(card) { + return false; +} + +// GREEK EVENTS + +function can_play_greek_event(card) { + return false; +} + +// SUPPLY PHASE + +function goto_supply_phase() { + start_persian_supply_phase(); +} + +function start_persian_supply_phase() { + if (game.campaign == 5 || game.persian.hand.length == 0) + return start_persian_attrition(); + game.active = PERSIA; + game.state = 'persian_cards_in_hand'; +} + +function start_greek_supply_phase() { + if (game.campaign == 5 || game.greek.hand.length == 0) + return start_greek_attrition(); + game.active = GREECE; + game.state = 'greek_cards_in_hand'; +} + +states.persian_cards_in_hand = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Supply Phase."; + view.prompt = "Persian Supply Phase: You may keep one card for the next campaign. Discard the rest."; + for (let card of game.persian.hand) + gen_action(view, 'discard', card); + if (game.persian.hand.length <= 1) + gen_action(view, 'next'); + }, + discard: function (card) { + discard_card("Persia", game.persian.hand, card); + }, + next: function (card) { + log("Persia keeps " + game.persian.hand.length + " cards."); + start_persian_attrition(); + }, +} + +states.greek_cards_in_hand = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Supply Phase."; + view.prompt = "Greek Supply Phase: You may keep up to 4 cards. Discard the rest."; + for (let card of game.greek.hand) + gen_action(view, 'discard', card); + if (game.greek.hand.length <= 4) + gen_action(view, 'next'); + }, + discard: function (card) { + discard_card("Greece", game.greek.hand, card); + }, + next: function (card) { + log("Greece keeps " + game.greek.hand.length + " cards."); + start_greek_attrition(); + }, +} + +function start_persian_attrition() { + let armies = 0; + let supply = 0; + for (let city of CITIES) { + if (city != EPHESOS && city != ABYDOS) { + armies += count_persian_armies(city); + if (is_persian_control(city)) + supply += SUPPLY[city]; + } + } + game.attrition = Math.max(0, armies - supply); + if (game.attrition > 0) { + log("Persia suffers " + game.attrition + " attrition."); + game.active = PERSIA; + game.state = 'persian_attrition'; + } else { + log("Persia suffers no attrition."); + end_persian_attrition(); + } +} + +function start_greek_attrition() { + let armies = 0; + let supply = 0; + for (let city of CITIES) { + armies += count_greek_armies(city); + if (is_greek_control(city)) + supply += SUPPLY[city]; + } + game.attrition = Math.max(0, armies - supply); + if (game.attrition > 0) { + log("Greece suffers " + game.attrition + " attrition."); + game.active = GREECE; + game.state = 'greek_attrition'; + } else { + log("Greece suffers no attrition."); + end_greek_attrition(); + } +} + +states.persian_attrition = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Persian Supply Phase."; + view.prompt = "Persian Supply Phase: Remove " + game.attrition + " armies."; + for (let city of CITIES) { + if (city != EPHESOS && city != ABYDOS) + if (count_persian_armies(city) > 0) + gen_action(view, 'city', city); + } + }, + city: function (space) { + log("Persia removes army from " + space + "."); + move_persian_army(space, RESERVE); + if (--game.attrition == 0) + end_persian_attrition(); + }, +} + +states.greek_attrition = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Greek Supply Phase."; + view.prompt = "Greek Supply Phase: Remove " + game.attrition + " armies."; + for (let city of CITIES) { + if (count_greek_armies(city) > 0) + gen_action(view, 'city', city); + } + }, + city: function (space) { + log("Greece removes army from " + space + "."); + move_greek_army(space, RESERVE); + if (--game.attrition == 0) + end_greek_attrition(); + }, +} + +function end_persian_attrition() { + persian_loc(); + start_greek_supply_phase(); +} + +function end_greek_attrition() { + greek_loc(); + goto_scoring_phase(); +} + +function gen_persian_land_move(view, seen, from) { + if (!seen[from]) + gen_action(view, 'city', from); + seen[from] = 1; + if (is_persian_control(from)) + for (let to of ROADS[from]) + if (is_usable_road(from, to) && !seen[to]) + gen_persian_land_move(view, seen, to); +} + +function persian_loc() { + let loc = {}; + function persian_loc_rec(from) { + loc[from] = 1; + for (let to of ROADS[from]) + if (is_usable_road(from, to) && !is_greek_control(to) && !loc[to]) + persian_loc_rec(to); + } + if (is_persian_control(ABYDOS)) + persian_loc_rec(ABYDOS); + if (is_persian_control(EPHESOS)) + persian_loc_rec(EPHESOS); + if ((is_persian_control(ABYDOS) && count_greek_fleets(ABYDOS) == 0) || + (is_persian_control(EPHESOS) && count_greek_fleets(EPHESOS))) + for (let port of PORTS) + if (count_persian_fleets(port) > 0) + loc[port] = 1; + for (let city of CITIES) { + if (!loc[city]) { + let n = count_persian_armies(city); + if (n > 0) { + log("Persia removes " + n + " armies in " + city + "."); + move_persian_army(city, RESERVE, n); + } + } + } +} + +function greek_loc() { + let loc = {}; + function greek_loc_rec(from) { + loc[from] = 1; + for (let to of ROADS[from]) + if (is_usable_road(from, to) && !is_persian_control(to) && !loc[to]) + greek_loc_rec(to); + } + if (is_greek_control(ATHENAI)) + greek_loc_rec(ATHENAI); + if (is_greek_control(SPARTA)) + greek_loc_rec(SPARTA); + if ((is_greek_control(ATHENAI) && count_persian_fleets(ATHENAI) == 0) || + (is_greek_control(SPARTA) && count_persian_fleets(SPARTA))) + for (let port of PORTS) + if (count_greek_fleets(port) > 0) + loc[port] = 1; + for (let city of CITIES) { + if (!loc[city]) { + let n = count_greek_armies(city); + if (n > 0) { + log("Greece removes " + n + " armies in " + city + "."); + move_greek_army(city, RESERVE, n); + } + } + } +} + +// SCORING PHASE + +function goto_scoring_phase() { + if (is_persian_control(ATHENAI) && is_persian_control(SPARTA)) { + game.victory = "Persia wins by controlling Athenai and Sparta!"; + game.state = 'game_over'; + return; + } + if (is_greek_control(ABYDOS) && is_greek_control(EPHESOS)) { + game.victory = "Greece wins by controlling Abydos and Ephesos!"; + game.state = 'game_over'; + return; + } + let greek_vp = 0; + let persian_vp = 0; + for (let city of CITIES) { + if (is_greek_control(city)) + greek_vp += SCORE[city]; + if (is_persian_control(city)) + persian_vp += SCORE[city]; + } + if (persian_vp > greek_vp) + log("Persia scores " + (persian_vp - greek_vp) + " points."); + else if (greek_vp > persian_vp) + log("Greece scores " + (greek_vp - persian_vp) + " points."); + else + log("Nobody scores any points."); + add_vp(persian_vp - greek_vp); + end_campaign(); +} + +function end_campaign() { + if (game.campaign == 5) { + if (game.vp < 0) { + game.victory = $("Greece wins with " + (-game.vp) + " points."); + game.result = "Greek"; + } else if (game.vp > 0) { + game.victory = $("Persia wins with " + game.vp + "points."); + game.result = "Persian"; + } else { + game.victory = "Nobody wins."; + game.result = "Tie"; + } + game.state = 'game_over'; + log(""); + log(game.victory); + } else { + ++game.campaign; + start_campaign(); + } +} + +states.game_over = { + prompt: function (view) { + return view.prompt = game.victory; + } +} + +exports.ready = function (scenario, players) { + return (players.length === 2); +} + +exports.setup = function (scenario, players) { + game = { + log: [], + undo: [], + + // game board state + campaign: 1, + vp: 0, + deck: create_deck(), + discard: [], + persian: { + hand: [], + draw: 0, + pass: 0, + }, + greek: { + hand: [], + draw: 0, + pass: 0, + }, + units: { + Abydos: [0,2,0,0], + Athenai: [1,0,1,0], + Delphi: [0,0], + Ephesos: [0,2,0,1], + Eretria: [0,0,0,0], + Korinthos: [1,0], + Larissa: [0,0], + Naxos: [0,0,0,0], + Pella: [0,0,0,0], + Sparta: [1,0,1,0], + Thebai: [0,0,0,0], + reserve: [6,20,3,5], + }, + trigger: { + darius: 0, + xerxes: 0, + artemisia: 0, + miltiades: 0, + themistocles: 0, + leonidas: 0, + hellespont: 0, + }, + + // transient action state + move_list: null, + talents: 0, + built_fleets: 0, + naval_battle: 0, + attrition: 0, + from: null, + where: null, + }; + + start_campaign(); + + return game; +} + +exports.action = function (state, current, action, arg) { + game = state; + // TODO: check current, action and argument against action list + if (true) { + let S = states[game.state]; + if (action in S) + S[action](arg, current); + else + throw new Error("Invalid action: " + action); + } + return state; +} + +exports.resign = function (state, current) { + game = state; + if (game.state != 'game_over') { + log(""); + log(current + " resigned."); + game.active = "None"; + game.state = 'game_over'; + game.victory = current + " resigned."; + if (game.current == PERSIA) + game.result = GREECE; + else + game.result = PERSIA; + } +} + +exports.view = function(state, current) { + game = state; + + let view = { + log: game.log, + active: game.active, + campaign: game.campaign, + vp: game.vp, + trigger: game.trigger, + units: game.units, + }; + + view.g_cards = game.greek.hand.length + game.greek.draw; + view.p_cards = game.persian.hand.length + game.persian.draw; + view.discard = game.discard.length > 0 ? game.discard[game.discard.length-1] : 0; + + states[game.state].prompt(view, current); + view.prompt = $(view.prompt); + + if (game.transport) + view.transport = { count: game.transport, where: game.where, who: game.attacker }; + + if (current == GREECE) { + view.hand = game.greek.hand; + view.draw = game.greek.draw; + } + if (current == PERSIA) { + view.hand = game.persian.hand; + view.draw = game.persian.draw; + } + + return view; +} diff --git a/ui.js b/ui.js new file mode 100644 index 0000000..67357c7 --- /dev/null +++ b/ui.js @@ -0,0 +1,576 @@ +"use strict"; + +const GREECE = "Greece"; +const PERSIA = "Persia"; + +const SPACES = [ + "Abydos", + "Athenai", + "Delphi", + "Ephesos", + "Eretria", + "Korinthos", + "Larissa", + "Naxos", + "Pella", + "Sparta", + "Thebai", + "reserve", + "extra", +]; + +const PORTS = { + "Abydos":{"x":866,"y":625,"w":138,"h":138,"layout_x":855,"layout_y":585,"wrap":4}, + "Ephesos":{"x":450,"y":765,"w":138,"h":138,"layout_x":424,"layout_y":743,"wrap":3}, + "Athenai":{"x":515,"y":353,"w":138,"h":138,"layout_x":521,"layout_y":379,"wrap":4}, + "Eretria":{"x":682,"y":481,"w":138,"h":138,"layout_x":683,"layout_y":510,"wrap":4}, + "Naxos":{"x":475,"y":575,"w":138,"h":138,"layout_x":503,"layout_y":581,"wrap":3}, + "Pella":{"x":931,"y":317,"w":138,"h":138,"layout_x":919,"layout_y":345,"wrap":4}, + "Sparta":{"x":259,"y":449,"w":138,"h":138,"layout_x":251,"layout_y":470,"wrap":4}, + "Thebai":{"x":689,"y":282,"w":138,"h":138,"layout_x":701,"layout_y":311,"wrap":4} +}; + +const CITIES = { + "Abydos":{"x":863,"y":654,"w":92,"h":90}, + "Ephesos":{"x":509,"y":766,"w":92,"h":90}, + "Athenai":{"x":537,"y":293,"w":84,"h":81}, + "Delphi":{"x":607,"y":92,"w":84,"h":81}, + "Eretria":{"x":668,"y":436,"w":84,"h":81}, + "Korinthos":{"x":442,"y":137,"w":84,"h":81}, + "Larissa":{"x":799,"y":107,"w":84,"h":81}, + "Naxos":{"x":408,"y":590,"w":84,"h":81}, + "Pella":{"x":960,"y":266,"w":84,"h":81}, + "Sparta":{"x":278,"y":344,"w":84,"h":81}, + "Thebai":{"x":671,"y":221,"w":84,"h":81} +}; + +let game; + +let ui = { + player: null, + cards: {}, + backs: {}, + cities: {}, + ports: {}, + greek_fleet: {}, + greek_army: {}, + persian_fleet: {}, + persian_army: {}, + all_fleets: [], + all_armies: [], + selected_armies: null, + selected_fleets: null, +}; + +function remove_from_array(array, item) { + let i = array.indexOf(item); + if (i >= 0) + array.splice(i, 1); +} + +function on_focus_bridge(evt) { document.getElementById("status").textContent = "Hellespont"; } +function on_focus_city(evt) { document.getElementById("status").textContent = evt.target.city; } +function on_focus_port(evt) { document.getElementById("status").textContent = evt.target.port + " (port)"; } +function on_blur(evt) { document.getElementById("status").textContent = ""; } + +function on_click_bridge(evt) { + send_action('destroy'); +} + +function on_click_army(evt) { + if (game.land_movement && ui.player) { + let here = (ui.player == PERSIA ? ui.persian_army : ui.greek_army)[game.land_movement]; + if (here.includes(evt.target)) { + if (ui.selected_armies && ui.selected_armies.includes(evt.target)) + remove_from_array(ui.selected_armies, evt.target); + else + ui.selected_armies.push(evt.target); + } + update_ui(); + } + if (game.naval_movement && ui.player) { + let here = (ui.player == PERSIA ? ui.persian_army : ui.greek_army)[game.naval_movement]; + if (here.includes(evt.target)) { + if (ui.selected_armies && ui.selected_armies.includes(evt.target)) { + remove_from_array(ui.selected_armies, evt.target); + } else { + if (ui.selected_armies.length < ui.selected_fleets.length) + ui.selected_armies.push(here[ui.selected_armies.length]); + } + } + update_ui(); + } +} + +function on_click_fleet(evt) { + if (game.naval_movement && ui.player) { + let here = (ui.player == PERSIA ? ui.persian_fleet : ui.greek_fleet)[game.naval_movement]; + if (here.includes(evt.target)) { + if (ui.selected_fleets && ui.selected_fleets.includes(evt.target)) { + remove_from_array(ui.selected_fleets, evt.target); + while (ui.selected_armies.length > ui.selected_fleets.length) + ui.selected_armies.pop(); + } else { + ui.selected_fleets.push(evt.target); + } + } + update_ui(); + } +} + +function on_click_city(evt) { + if (!game.land_movement) + return send_action('city', evt.target.city); + if (game.actions && game.actions.city && game.actions.city.includes(evt.target.city)) { + let armies = ui.selected_armies.length; + if (armies > 0) + socket.emit('action', 'city', [evt.target.city, armies]); + } +} + +function on_click_port(evt) { + if (!game.naval_movement) + send_action('port', evt.target.port); + if (game.actions && game.actions.port && game.actions.port.includes(evt.target.port)) { + let fleets = ui.selected_fleets.length; + if (fleets > 0) { + let armies = ui.selected_armies.length; + socket.emit('action', 'port', [evt.target.port, fleets, armies]); + } + } +} + +function build_ui() { + for (let c = 1; c <= 16; ++c) { + ui.cards[c] = document.getElementById("card_"+c); + ui.cards[c].card = c; + ui.cards[c].addEventListener("click", on_card); + ui.backs[c] = document.getElementById("back_"+c); + } + + for (let city in CITIES) { + let info = CITIES[city]; + let e = ui.cities[city] = document.getElementById("city_" + city); + e.city = city; + e.addEventListener("mouseenter", on_focus_city); + e.addEventListener("mouseleave", on_blur); + e.addEventListener("click", on_click_city); + e.style.left = Math.round(info.x - info.w/2) + "px"; + e.style.top = Math.round(info.y - info.h/2) + "px"; + e.style.width = info.w + "px"; + e.style.height = info.h + "px"; + } + + for (let port in PORTS) { + let info = PORTS[port]; + let e = ui.ports[port] = document.getElementById("port_" + port); + e.port = port; + e.addEventListener("mouseenter", on_focus_port); + e.addEventListener("mouseleave", on_blur); + e.addEventListener("click", on_click_port); + e.style.left = Math.round(info.x - info.w/2) + "px"; + e.style.top = Math.round(info.y - info.h/2) + "px"; + e.style.width = info.w + "px"; + e.style.height = info.h + "px"; + } + + for (let city in CITIES) { + ui.greek_army[city] = []; + ui.greek_fleet[city] = []; + ui.persian_army[city] = []; + ui.persian_fleet[city] = []; + } + + ui.greek_army.reserve = []; + ui.greek_fleet.reserve = []; + ui.persian_army.reserve = []; + ui.persian_fleet.reserve = []; + + ui.greek_army.extra = []; + ui.greek_fleet.extra = []; + ui.persian_army.extra = []; + ui.persian_fleet.extra = []; + + for (let i = 0; i < 9; ++i) { + let e = document.getElementById("ga"+(i+1)); + e.sort_index = i; + ui.greek_army.extra.push(e); + ui.all_armies.push(e); + e.addEventListener("click", on_click_army); + } + for (let i = 0; i < 5; ++i) { + let e = document.getElementById("gf"+(i+1)); + e.sort_index = i; + ui.greek_fleet.extra.push(e); + ui.all_fleets.push(e); + e.addEventListener("click", on_click_fleet); + } + for (let i = 0; i < 24; ++i) { + let e = document.getElementById("pa"+(i+1)); + e.sort_index = i; + ui.persian_army.extra.push(e); + ui.all_armies.push(e); + e.addEventListener("click", on_click_army); + } + for (let i = 0; i < 6; ++i) { + let e = document.getElementById("pf"+(i+1)); + e.sort_index = i; + ui.persian_fleet.extra.push(e); + ui.all_fleets.push(e); + e.addEventListener("click", on_click_fleet); + } + + document.getElementById("bridge").addEventListener("click", on_click_bridge); + document.getElementById("bridge").addEventListener("mouseenter", on_focus_bridge); + document.getElementById("bridge").addEventListener("mouseleave", on_blur); +} + +function greek_info() { + if (game.g_cards == 1) + return "1 card in hand"; + return game.g_cards + " cards in hand"; +} + +function persian_info() { + if (game.p_cards == 1) + return "1 card in hand"; + return game.p_cards + " cards in hand"; +} + +function show_marker(id, class_name, show = 1, enabled = 0) { + let elt = document.getElementById(id); + if (show) + class_name += " show"; + if (enabled) + class_name += " enabled"; + elt.className = class_name; +} + +function on_update(state, player) { + game = state; + ui.player = player; + + document.getElementById("greek_info").textContent = greek_info(); + document.getElementById("persian_info").textContent = persian_info(); + + if (!game.discard) + document.getElementById("last_played").className = "card show card_back"; + else + document.getElementById("last_played").className = "card show card_" + game.discard; + + show_action_button("#button_battle", "battle"); + show_action_button("#button_build", "build"); + show_action_button("#button_destroy", "destroy"); + show_action_button("#button_draw", "draw"); + show_action_button("#button_next", "next"); + show_action_button("#button_pass", "pass"); + show_action_button("#button_undo", "undo"); + + show_marker("bridge", "bridge", game.trigger.hellespont, game.actions && game.actions.destroy); + show_marker("darius", "persian_army", game.trigger.darius); + show_marker("xerxes", "persian_army", game.trigger.xerxes); + show_marker("artemisia", "persian_fleet", game.trigger.artemisia); + show_marker("miltiades", "greek_army", game.trigger.miltiades); + show_marker("themistocles", "greek_army", game.trigger.themistocles); + show_marker("leonidas", "greek_army", game.trigger.leonidas); + show_marker("campaign", "marker campaign_" + game.campaign); + if (game.vp < 0) + show_marker("vp", "marker vp_g" + (-game.vp)); + else if (game.vp > 0) + show_marker("vp", "marker vp_p" + game.vp); + else + show_marker("vp", "marker vp_0"); + + let hand = game.hand; + let draw = game.draw; + for (let c = 1; c <= 16; ++c) { + ui.cards[c].classList.remove('enabled'); + if (hand && hand.includes(c)) + ui.cards[c].classList.add('show'); + else + ui.cards[c].classList.remove('show'); + if (c <= draw) + ui.backs[c].classList.add('show'); + else + ui.backs[c].classList.remove('show'); + } + + function update_units(index, elements) { + let overflow = []; + let extra = elements.extra; + + // remove if too many + for (let space in game.units) { + let list = elements[space]; + let n = game.units[space][index] | 0; + while (list.length > n) + overflow.push(list.shift()); + } + + // add if not enough + for (let space in game.units) { + let list = elements[space]; + let n = game.units[space][index]; + while (list.length < n) { + if (overflow.length > 0) { + list.unshift(overflow.pop()); + } else { + let e = extra.pop(); + e.classList.add("show"); + list.unshift(e); + } + } + } + + // and hide the overflow + while (overflow.length > 0) { + let e = overflow.pop(); + e.classList.remove("show"); + extra.push(e); + } + } + + update_units(0, ui.greek_army); + update_units(1, ui.persian_army); + update_units(2, ui.greek_fleet); + update_units(3, ui.persian_fleet); + + ui.selected_armies = null; + if (game.land_movement) { + if (ui.player == PERSIA) + ui.selected_armies = ui.persian_army[game.land_movement].slice(); + if (ui.player == GREECE) + ui.selected_armies = ui.greek_army[game.land_movement].slice(); + } + + ui.selected_fleets = null; + if (game.naval_movement) { + if (ui.player == PERSIA) { + ui.selected_fleets = ui.persian_fleet[game.naval_movement].slice(); + ui.selected_armies = []; + } + if (ui.player == GREECE) { + ui.selected_fleets = ui.greek_fleet[game.naval_movement].slice(); + ui.selected_armies = []; + } + } + + for (let city in CITIES) + ui.cities[city].classList.remove('enabled'); + for (let port in PORTS) + ui.ports[port].classList.remove('enabled'); + + if (game.actions && game.actions.city) { + for (let city of game.actions.city) + ui.cities[city].classList.add('enabled'); + } + if (game.actions && game.actions.port) { + for (let port of game.actions.port) + ui.ports[port].classList.add('enabled'); + } + + update_ui(); +} + +function update_ui() { + function layout_fleets(a, b, xorig, yorig, wrap) { + if (a.length + b.length > 0) { + let w = 26; + let h = 20; + let xstep = w + 2; + let ystep = h + 0; + let stagger = 14; + let line, para = []; + let i = 0, k = 0; + para.push(line = []); + for (let e of a) { + if (i == wrap - k) { para.push(line = []); i = 0; k = 1 - k; } + line.push(e); + ++i; + } + if (i != 0 && b.length > 0) { para.push(line = []); i = 0; k = 1 - k; } + for (let e of b) { + if (i == wrap - k) { para.push(line = []); i = 0; k = 1 - k; } + line.push(e); + ++i; + } + let y = yorig - Math.floor(ystep * para.length / 2); + k = 0; + let cw = (para.length > 1 ? wrap : para[0].length); + for (let row = 0; row < para.length; ++row) { + let x = xorig - Math.floor(xstep * cw / 2) + k * stagger; + for (let col = 0; col < para[row].length; ++col) { + para[row][col].style.left = x + "px"; + para[row][col].style.top = y + "px"; + x += xstep; + } + y += ystep; + k = 1 - k; + } + } + } + + function layout_armies(list, xorig, yorig) { + const dx = 12; + const dy = 8; + if (list.length > 0) { + let ncol = Math.round(Math.sqrt(list.length)); + let nrow = Math.ceil(list.length / ncol); + function layout_army(row, col, e, z) { + let x = xorig - (row * dx - col * dx) - 10 + (nrow-ncol) * 6; + let y = yorig - (row * dy + col * dy) - 13 + (nrow-1) * 8; + e.style.left = x + "px"; + e.style.top = y + "px"; + e.style.zIndex = z; + } + let z = 50; + let i = 0; + if (ui.player == GREECE) + for (let row = nrow-1; row >= 0; --row) + for (let col = ncol-1; col >= 0 && i < list.length; --col) + layout_army(row, col, list[i++], z--); + else + for (let row = 0; row < nrow; ++row) + for (let col = 0; col < ncol && i < list.length; ++col) + layout_army(row, col, list[list.length-(++i)], z--); + } + } + + function list_armies(city) { + let ga = ui.greek_army[city]; + let pa = ui.persian_army[city]; + if (game.transport && game.transport.where == city) { + if (game.transport.who == GREECE) + ga = ga.slice(game.transport.count); + if (game.transport.who == PERSIA) + pa = pa.slice(game.transport.count); + } + if (game.naval_movement) { + ga = ga.filter(a => !ui.selected_armies.includes(a)); + pa = pa.filter(a => !ui.selected_armies.includes(a)); + } + return ga.concat(pa); + } + + layout_fleets(ui.greek_fleet.reserve, [], 95, 150, 5); + layout_fleets(ui.persian_fleet.reserve, [], 1240-95, 878-150, 6); + layout_armies(ui.greek_army.reserve, 80, 220); + layout_armies(ui.persian_army.reserve, 1240-80, 878-220) + + for (let port in PORTS) + layout_fleets(ui.greek_fleet[port], ui.persian_fleet[port], + PORTS[port].layout_x, PORTS[port].layout_y, PORTS[port].wrap); + + for (let city in CITIES) + layout_armies(list_armies(city), + CITIES[city].x, CITIES[city].y); + + function layout_transport(a, f) { + a.style.left = (parseInt(f.style.left) + 13 - 11) + "px"; + if (ui.player == GREECE) + a.style.top = (parseInt(f.style.top) + 10 - 13 + 10) + "px"; + else + a.style.top = (parseInt(f.style.top) + 10 - 13 - 10) + "px"; + a.style.zIndex = 2; + } + + if (game.transport) { + let city = game.transport.where; + let alist = (game.transport.who == GREECE ? ui.greek_army : ui.persian_army)[city]; + let flist = (game.transport.who == GREECE ? ui.greek_fleet : ui.persian_fleet)[city]; + for (let i = 0; i < game.transport.count; ++i) + layout_transport(alist[i], flist[i]); + } + if (game.naval_movement) { + for (let i = 0; i < ui.selected_armies.length; ++i) + layout_transport(ui.selected_armies[i], ui.selected_fleets[i]); + } + + for (let e of ui.all_armies) + if (ui.selected_armies && ui.selected_armies.includes(e)) + e.classList.add("selected"); + else + e.classList.remove("selected"); + + for (let e of ui.all_fleets) + if (ui.selected_fleets && ui.selected_fleets.includes(e)) + e.classList.add("selected"); + else + e.classList.remove("selected"); +} + +function on_destroy() { if (game.actions) { send_action('destroy', null); } } +function on_battle() { if (game.actions) { send_action('battle', null); } } +function on_build() { if (game.actions) { send_action('build', null); } } +function on_draw() { if (game.actions) { send_action('draw', null); } } +function on_next() { if (game.actions) { send_action('next', null); } } +function on_pass() { if (game.actions) { send_action('pass', null); } } +function on_undo() { if (game.actions) { send_action('undo', null); } } + +let current_popup_card = 0; + +function show_popup_menu(evt, list) { + document.querySelectorAll("#popup div").forEach(e => e.classList.remove('enabled')); + for (let item of list) { + let e = document.getElementById("menu_" + item); + e.classList.add('enabled'); + } + let popup = document.getElementById("popup"); + popup.style.display = 'block'; + popup.style.left = (evt.clientX-50) + "px"; + popup.style.top = (evt.clientY-12) + "px"; + ui.cards[current_popup_card].classList.add("selected"); +} + +function hide_popup_menu() { + let popup = document.getElementById("popup"); + popup.style.display = 'none'; + if (current_popup_card) { + ui.cards[current_popup_card].classList.remove("selected"); + current_popup_card = 0; + } +} + +function on_card_event() { + send_action('card_event', current_popup_card); + hide_popup_menu(); +} +function on_card_move() { + send_action('card_move', current_popup_card); + hide_popup_menu(); +} + +function is_card_action(action, card) { + return game.actions && game.actions[action] && game.actions[action].includes(card); +} + +function on_card(evt) { + if (game.actions) { + let card = evt.target.card; + if (is_card_action('discard', card)) { + send_action('discard', card); + } else { + let menu = []; + if (is_card_action('card_event', card)) + menu.push('card_event'); + if (is_card_action('card_move', card)) + menu.push('card_move'); + if (menu.length > 0) { + current_popup_card = card; + show_popup_menu(evt, menu); + } + } + } +} + +function toggle_markers() { + document.querySelector(".map").classList.toggle("hide_markers"); +} + +ui.player = new URLSearchParams(window.location.search).get("role"); +if (ui.player == GREECE) + document.getElementById("map").classList.add("greek"); + +build_ui(); +scroll_with_middle_mouse(".grid_center", 2); +init_client(["Greece", "Persia"]); -- cgit v1.2.3