summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2021-06-01 13:56:58 +0200
committerTor Andersson <tor@ccxvii.net>2023-02-18 12:42:59 +0100
commit9616bee63c7c65a1e860b0dea6c7e71e57bd3b42 (patch)
treeb7ed1dc67a4fc0471d98ac3cd53e870bce1fee97
parente90f60862e4d0e06f5600f64b528f3c0b37d02a4 (diff)
download300-earth-and-water-9616bee63c7c65a1e860b0dea6c7e71e57bd3b42.tar.gz
300: Start implementing rules.
-rw-r--r--play.html404
-rw-r--r--rules.js1895
-rw-r--r--ui.js576
3 files changed, 2875 insertions, 0 deletions
diff --git a/play.html b/play.html
new file mode 100644
index 0000000..1251a43
--- /dev/null
+++ b/play.html
@@ -0,0 +1,404 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
+<meta charset="UTF-8">
+<title>300: E&amp;W</title>
+<link rel="icon" href="Achaemenid_Falcon.png">
+<link rel="stylesheet" href="/fonts/fonts.css">
+<link rel="stylesheet" href="/common/grid.css">
+<script defer src="/socket.io/socket.io.min.js"></script>
+<script defer src="/common/client.js"></script>
+<script defer src="ui.js"></script>
+<style>
+
+.grid_center { background-color: slategray; }
+.grid_role { background-color: gainsboro; }
+.grid_log { background-color: gainsboro; }
+.grid_top { background-color: silver; }
+.grid_top.Persia.your_turn { background-color: skyblue; }
+.grid_top.Greece.your_turn { background-color: salmon; }
+.grid_top.disconnected { background-color: red; }
+.role_info { background-color: silver; }
+.one .role_name { background-color: salmon; }
+.two .role_name { background-color: skyblue; }
+
+.role_info {
+ padding: 10px 20px;
+ background-color: gainsboro;
+}
+.last_played {
+ background-color: silver;
+}
+.last_played .card {
+ margin: 15px auto;
+}
+.grid_role {
+ width: 240px;
+}
+
+/* CARDS */
+
+.card_back { background-image: url('cards/card_back.jpg'); }
+.card_1 { background-image: url('cards/card_en_01.jpg'); }
+.card_2 { background-image: url('cards/card_en_02.jpg'); }
+.card_3 { background-image: url('cards/card_en_03.jpg'); }
+.card_4 { background-image: url('cards/card_en_04.jpg'); }
+.card_5 { background-image: url('cards/card_en_05.jpg'); }
+.card_6 { background-image: url('cards/card_en_06.jpg'); }
+.card_7 { background-image: url('cards/card_en_07.jpg'); }
+.card_8 { background-image: url('cards/card_en_08.jpg'); }
+.card_9 { background-image: url('cards/card_en_09.jpg'); }
+.card_10 { background-image: url('cards/card_en_10.jpg'); }
+.card_11 { background-image: url('cards/card_en_11.jpg'); }
+.card_12 { background-image: url('cards/card_en_12.jpg'); }
+.card_13 { background-image: url('cards/card_en_13.jpg'); }
+.card_14 { background-image: url('cards/card_en_14.jpg'); }
+.card_15 { background-image: url('cards/card_en_15.jpg'); }
+.card_16 { background-image: url('cards/card_en_16.jpg'); }
+
+.card {
+ background-repeat: no-repeat;
+ width: 250px;
+ height: 350px;
+ border-radius: 12px;
+}
+
+.role_info .card {
+ width: 200px;
+ height: 280px;
+ border-radius: 10px;
+}
+
+/* CARD ACTION POPUP MENU */
+
+#popup {
+ position: absolute;
+ user-select: none;
+ background-color: #ddd;
+ left: 10px;
+ top: 100px;
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.3);
+ z-index: 200;
+ min-width: 20ex;
+ white-space: nowrap;
+}
+#popup div { padding: 3pt 8pt; display: none; }
+#popup div.enabled { padding: 3pt 8pt; display: block; }
+#popup div:hover { background-color: teal; color: white; }
+
+/* MAP WITH ARMIES, FLEETS, AND MARKERS */
+
+.map.greek, .map.greek > div {
+ transform: rotate(180deg);
+}
+
+.map {
+ position: relative;
+ width: 1240px;
+ height: 878px;
+ background-image: url("map.jpg");
+ background-size: cover;
+}
+
+.map.hide_markers > div {
+ visibility: hidden;
+}
+
+.port {
+ position: absolute;
+ border-radius: 50%;
+ z-index: 1;
+}
+.port.enabled {
+ box-shadow: 0 0 20px 2px white, inset 0 0 5px 2px white;
+ z-index: 3;
+}
+
+.persian_city {
+ position: absolute;
+ border-radius: 50%;
+ z-index: 4;
+}
+.persian_city.enabled {
+ box-shadow: 0 0 10px 2px white, inset 0 0 5px 2px white;
+ z-index: 100;
+}
+
+.greek_city {
+ position: absolute;
+ border-radius: 5px;
+ z-index: 4;
+}
+.greek_city.enabled {
+ box-shadow: 0 0 10px 4px white, inset 0 0 5px 4px white;
+ z-index: 100;
+}
+
+.greek_army, .greek_fleet, .persian_army, .persian_fleet, .marker {
+ transition-property: top, left, transform;
+ transition-duration: 700ms;
+ transition-timing-function: ease;
+ z-index: 2;
+}
+
+.greek_army, .greek_fleet, .persian_army, .persian_fleet, .marker, #bridge {
+ position: absolute;
+ display: none;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.marker { background-image: url("icons/black_cube.png"); }
+.greek_army { background-image: url("icons/red_cube.png"); }
+.greek_fleet { background-image: url("icons/red_disk.png"); }
+.persian_army { background-image: url("icons/blue_cube.png"); }
+.persian_fleet { background-image: url("icons/blue_disk.png"); }
+.greek_army, .persian_army, .marker { width: 22px; height: 26px; }
+.greek_fleet, .persian_fleet { width: 26px; height: 20px; }
+
+.campaign_1 { left: 1074px; top: 368px; }
+.campaign_2 { left: 1077px; top: 402px; }
+.campaign_3 { left: 1078px; top: 435px; }
+.campaign_4 { left: 1077px; top: 467px; }
+.campaign_5 { left: 1072px; top: 501px; }
+.vp_g6 { left: 181px; top: 228px; }
+.vp_g5 { left: 168px; top: 259px; }
+.vp_g4 { left: 157px; top: 291px; }
+.vp_g3 { left: 148px; top: 323px; }
+.vp_g2 { left: 142px; top: 357px; }
+.vp_g1 { left: 138px; top: 389px; }
+.vp_0 { left: 138px; top: 425px; }
+.vp_p1 { left: 138px; top: 460px; }
+.vp_p2 { left: 142px; top: 493px; }
+.vp_p3 { left: 148px; top: 526px; }
+.vp_p4 { left: 157px; top: 559px; }
+.vp_p5 { left: 168px; top: 590px; }
+.vp_p6 { left: 181px; top: 622px; }
+
+.greek_fleet.show, .persian_fleet.show, .greek_army.show, .persian_army.show, .marker.show, #bridge.show {
+ display: block;
+ filter: drop-shadow(1px 2px 3px rgba(0,0,0,0.5));
+}
+
+.greek_fleet.selected, .persian_fleet.selected, .greek_army.selected, .persian_army.selected {
+ filter: brightness(150%) drop-shadow(0 0 3px white);
+}
+#bridge.enabled {
+ filter: brightness(150%) drop-shadow(0 0 10px skyblue);
+}
+
+#darius { left: 77px; top: 562px; }
+#xerxes { left: 61px; top: 717px; }
+#artemisia { left: 167px; top: 799px; }
+#miltiades { left: 1179px; top: 305px; }
+#themistocles { left: 1156px; top: 153px; }
+#leonidas { left: 1068px; top: 41px; }
+#bridge {
+ background-image: url("icons/bridge.png");
+ width: 48px; height: 36px;
+ left: 932px; top: 655px;
+}
+
+</style>
+</head>
+<body>
+
+<div class="status" id="status"></div>
+
+<div class="chat_window">
+<div class="chat_header">Chat</div>
+<div class="chat_text"></div>
+<form class="chat_form" action=""><input id="chat_input" autocomplete="off"></form>
+</div>
+
+<div class="grid_window">
+
+ <div class="grid_top">
+
+ <div class="menu">
+ <div class="menu_title"><img src="/images/cog.svg"></div>
+ <div class="menu_popup">
+ <div class="menu_item" onclick="toggle_fullscreen()">Fullscreen</div>
+ <div class="menu_separator"></div>
+ <div class="menu_item" onclick="window.open('info/notes.html', '_blank')">Notes</div>
+ <div class="menu_item" onclick="window.open('info/rules.html', '_blank')">Rules</div>
+ <div class="menu_item" onclick="window.open('info/cards.html', '_blank')">Cards</div>
+ <div class="menu_separator"></div>
+ <div class="menu_item" onclick="confirm_resign()">Resign</div>
+ <div class="menu_separator"></div>
+ <div class="menu_item" onclick="send_save()">&#x1F41E; Save</div>
+ <div class="menu_item" onclick="send_restore()">&#x1F41E; Restore</div>
+ <div class="menu_separator"></div>
+ <div class="menu_item" onclick="send_restart('Historical')">&#x26a0; Restart</div>
+ </div>
+ </div>
+
+ <div class="image_button" onclick="toggle_markers()"><img src="/images/earth-africa-europe.svg"></div>
+ <div class="image_button" onclick="toggle_log()"><img src="/images/scroll-quill.svg"></div>
+ <div class="image_button chat_button" onclick="toggle_chat()"><img src="/images/chat-bubble.svg"></div>
+
+ <div id="prompt" class="prompt">Connecting...</div>
+
+ <button id="button_build" onclick="on_build()" class="hide">Build bridge</button>
+ <button id="button_destroy" onclick="on_destroy()" class="hide">Destroy bridge</button>
+ <button id="button_draw" onclick="on_draw()" class="hide">Draw</button>
+ <button id="button_battle" onclick="on_battle()" class="hide">Battle</button>
+ <button id="button_pass" onclick="on_pass()" class="hide">Pass</button>
+ <button id="button_undo" onclick="on_undo()" class="hide">Undo</button>
+ <button id="button_next" onclick="on_next()" class="hide">Next</button>
+
+ </div>
+
+ <div class="grid_role">
+
+ <div class="role one">
+ <div class="role_name">Greece (<span class="role_user">$USER</span>)</div>
+ <div class="role_info" id="greek_info">0 cards in hand</div>
+ </div>
+
+ <div class="role two">
+ <div class="role_name">Persia (<span class="role_user">$USER</span>)</div>
+ <div class="role_info" id="persian_info">0 cards in hand</div>
+ </div>
+
+ <div class="role_info last_played"><div id="last_played" class="card show card_back"></div></div>
+ </div>
+
+ <div class="grid_log">
+ <div class="log" id="log"></div>
+ </div>
+
+ <div class="grid_center">
+
+ <div id="map" class="map">
+
+ <div id="campaign" class="marker campaign_1"></div>
+ <div id="bridge" class="bridge"></div>
+ <div id="vp" class="marker vp_0"></div>
+ <div id="darius" class="persian_army"></div>
+ <div id="xerxes" class="persian_army"></div>
+ <div id="artemisia" class="persian_fleet"></div>
+ <div id="miltiades" class="greek_army"></div>
+ <div id="themistocles" class="greek_army"></div>
+ <div id="leonidas" class="greek_army"></div>
+
+ <div id="port_Abydos" class="port"></div>
+ <div id="port_Ephesos" class="port"></div>
+ <div id="port_Athenai" class="port"></div>
+ <div id="port_Eretria" class="port"></div>
+ <div id="port_Naxos" class="port"></div>
+ <div id="port_Pella" class="port"></div>
+ <div id="port_Sparta" class="port"></div>
+ <div id="port_Thebai" class="port"></div>
+
+ <div id="gf1" class="greek_fleet"></div>
+ <div id="gf2" class="greek_fleet"></div>
+ <div id="gf3" class="greek_fleet"></div>
+ <div id="gf4" class="greek_fleet"></div>
+ <div id="gf5" class="greek_fleet"></div>
+
+ <div id="pf1" class="persian_fleet"></div>
+ <div id="pf2" class="persian_fleet"></div>
+ <div id="pf3" class="persian_fleet"></div>
+ <div id="pf4" class="persian_fleet"></div>
+ <div id="pf5" class="persian_fleet"></div>
+ <div id="pf6" class="persian_fleet"></div>
+
+ <div id="city_Abydos" class="persian_city"></div>
+ <div id="city_Ephesos" class="persian_city"></div>
+ <div id="city_Athenai" class="greek_city"></div>
+ <div id="city_Delphi" class="greek_city"></div>
+ <div id="city_Eretria" class="greek_city"></div>
+ <div id="city_Korinthos" class="greek_city"></div>
+ <div id="city_Larissa" class="greek_city"></div>
+ <div id="city_Naxos" class="greek_city"></div>
+ <div id="city_Pella" class="greek_city"></div>
+ <div id="city_Sparta" class="greek_city"></div>
+ <div id="city_Thebai" class="greek_city"></div>
+
+ <div id="ga1" class="greek_army"></div>
+ <div id="ga2" class="greek_army"></div>
+ <div id="ga3" class="greek_army"></div>
+ <div id="ga4" class="greek_army"></div>
+ <div id="ga5" class="greek_army"></div>
+ <div id="ga6" class="greek_army"></div>
+ <div id="ga7" class="greek_army"></div>
+ <div id="ga8" class="greek_army"></div>
+ <div id="ga9" class="greek_army"></div>
+
+ <div id="pa1" class="persian_army"></div>
+ <div id="pa2" class="persian_army"></div>
+ <div id="pa3" class="persian_army"></div>
+ <div id="pa4" class="persian_army"></div>
+ <div id="pa5" class="persian_army"></div>
+ <div id="pa6" class="persian_army"></div>
+ <div id="pa7" class="persian_army"></div>
+ <div id="pa8" class="persian_army"></div>
+ <div id="pa9" class="persian_army"></div>
+ <div id="pa10" class="persian_army"></div>
+ <div id="pa11" class="persian_army"></div>
+ <div id="pa12" class="persian_army"></div>
+ <div id="pa13" class="persian_army"></div>
+ <div id="pa14" class="persian_army"></div>
+ <div id="pa15" class="persian_army"></div>
+ <div id="pa16" class="persian_army"></div>
+ <div id="pa17" class="persian_army"></div>
+ <div id="pa18" class="persian_army"></div>
+ <div id="pa19" class="persian_army"></div>
+ <div id="pa20" class="persian_army"></div>
+ <div id="pa21" class="persian_army"></div>
+ <div id="pa22" class="persian_army"></div>
+ <div id="pa23" class="persian_army"></div>
+ <div id="pa24" class="persian_army"></div>
+
+ </div>
+
+ <div class="hand">
+ <div id="card_1" class="card card_1"></div>
+ <div id="card_2" class="card card_2"></div>
+ <div id="card_3" class="card card_3"></div>
+ <div id="card_4" class="card card_4"></div>
+ <div id="card_5" class="card card_5"></div>
+ <div id="card_6" class="card card_6"></div>
+ <div id="card_7" class="card card_7"></div>
+ <div id="card_8" class="card card_8"></div>
+ <div id="card_9" class="card card_9"></div>
+ <div id="card_10" class="card card_10"></div>
+ <div id="card_11" class="card card_11"></div>
+ <div id="card_12" class="card card_12"></div>
+ <div id="card_13" class="card card_13"></div>
+ <div id="card_14" class="card card_14"></div>
+ <div id="card_15" class="card card_15"></div>
+ <div id="card_16" class="card card_16"></div>
+ <div id="back_1" class="card card_back"></div>
+ <div id="back_2" class="card card_back"></div>
+ <div id="back_3" class="card card_back"></div>
+ <div id="back_4" class="card card_back"></div>
+ <div id="back_5" class="card card_back"></div>
+ <div id="back_6" class="card card_back"></div>
+ <div id="back_7" class="card card_back"></div>
+ <div id="back_8" class="card card_back"></div>
+ <div id="back_9" class="card card_back"></div>
+ <div id="back_10" class="card card_back"></div>
+ <div id="back_11" class="card card_back"></div>
+ <div id="back_12" class="card card_back"></div>
+ <div id="back_13" class="card card_back"></div>
+ <div id="back_14" class="card card_back"></div>
+ <div id="back_15" class="card card_back"></div>
+ <div id="back_16" class="card card_back"></div>
+ </div>
+
+ <div id="popup" onmouseleave="hide_popup_menu()">
+ <div id="menu_card_event" onclick="on_card_event()">
+ Play for Event
+ </div>
+ <div id="menu_card_move" onclick="on_card_move()">
+ Play for Movement
+ </div>
+ </div>
+
+ </div>
+
+</div>
+</body>
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"]);