diff options
-rw-r--r-- | play.html | 404 | ||||
-rw-r--r-- | rules.js | 1895 | ||||
-rw-r--r-- | ui.js | 576 |
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&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()">🐞 Save</div> + <div class="menu_item" onclick="send_restore()">🐞 Restore</div> + <div class="menu_separator"></div> + <div class="menu_item" onclick="send_restart('Historical')">⚠ 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; +} @@ -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"]); |