diff options
-rw-r--r-- | data.js | 97 | ||||
-rw-r--r-- | play.html | 398 | ||||
-rw-r--r-- | play.js | 366 | ||||
-rw-r--r-- | rules.js | 583 |
4 files changed, 1444 insertions, 0 deletions
@@ -0,0 +1,97 @@ +"use strict" + +const data = { + + cards: [ + null, + { side: "Versailles", ops: 1, name: "Jules Decatel" }, + { side: "Versailles", ops: 1, name: "The Murder of Vincenzini" }, + { side: "Versailles", ops: 1, name: "Brassardiers" }, + { side: "Versailles", ops: 1, name: "Jules Ferry" }, + { side: "Versailles", ops: 1, name: "Le Figaro" }, + { side: "Versailles", ops: 1, name: "Général Louis Valentin" }, + { side: "Versailles", ops: 1, name: "Général Espivent" }, + { side: "Versailles", ops: 1, name: "Les Amis de l'Ordre" }, + { side: "Versailles", ops: 2, name: "Socialist Newspaper Ban" }, + { side: "Versailles", ops: 2, name: "Fortification of Mont-Valérien" }, + { side: "Versailles", ops: 2, name: "Adolphe Thiers" }, + { side: "Versailles", ops: 2, name: "Otto von Bismarck" }, + { side: "Versailles", ops: 2, name: "Général Ernest de Cissey" }, + { side: "Versailles", ops: 2, name: "Colonel de Lochner" }, + { side: "Versailles", ops: 2, name: "Jules Favre" }, + { side: "Versailles", ops: 2, name: "Hostage Decree" }, + { side: "Versailles", ops: 4, name: "Maréchal Macmahon" }, + { side: "Commune", ops: 1, name: "Paule Minck" }, + { side: "Commune", ops: 1, name: "Walery Wroblewski" }, + { side: "Commune", ops: 1, name: "Banque de France" }, + { side: "Commune", ops: 1, name: "Le Réveil" }, + { side: "Commune", ops: 1, name: "Execution of Generals" }, + { side: "Commune", ops: 1, name: "Les Cantinières" }, + { side: "Commune", ops: 1, name: "Eugène Protot" }, + { side: "Commune", ops: 1, name: "Paul Cluseret" }, + { side: "Commune", ops: 2, name: "Gaston Crémieux" }, + { side: "Commune", ops: 2, name: "Luise Michel" }, + { side: "Commune", ops: 2, name: "Jaroslav Dombrowski" }, + { side: "Commune", ops: 2, name: "Raoul Rigault" }, + { side: "Commune", ops: 2, name: "Karl Marx" }, + { side: "Commune", ops: 2, name: "Blanquists" }, + { side: "Commune", ops: 2, name: "Général Lullier" }, + { side: "Commune", ops: 2, name: "Jules Vallès" }, + { side: "Commune", ops: 4, name: "Charles Delescluze" }, + { side: "Neutral", ops: 3, name: "Conciliation" }, + { side: "Neutral", ops: 3, name: "Georges Clemenceau" }, + { side: "Neutral", ops: 3, name: "Archbishop Georges Darboy" }, + { side: "Neutral", ops: 3, name: "Victor Hugo" }, + { side: "Neutral", ops: 3, name: "Léon Gambetta" }, + { side: "Neutral", ops: 3, name: "Elihu Washburne" }, + { side: "Neutral", ops: 3, name: "Freemason Parade" }, + { side: "Objective", ops: 0, name: "Paris Cannons", region: "Butte Montmartre" }, + { side: "Objective", ops: 0, name: "Aux Barricades!", region: "Buttes-Aux-Cailles" }, + { side: "Objective", ops: 0, name: "Commune's Stronghold", region: "Père Lachaise" }, + { side: "Objective", ops: 0, name: "Fighting in Issy Village", region: "Fort d'Issy" }, + { side: "Objective", ops: 0, name: "Battle of Mont-Valérien", region: "Mont-Valérien" }, + { side: "Objective", ops: 0, name: "Raid on Château de Vincennes", region: "Château de Vincennes" }, + { side: "Objective", ops: 0, name: "Revolution in the Press", region: "Press" }, + { side: "Objective", ops: 0, name: "Pius IX", region: "Catholic Church" }, + { side: "Objective", ops: 0, name: "Socialist International", region: "Social Movements" }, + { side: "Objective", ops: 0, name: "Royalists Dissension", region: "Royalists" }, + { side: "Objective", ops: 0, name: "Rise of Republicanism", region: "Republicans" }, + { side: "Objective", ops: 0, name: "Legitimacy", region: "National Assembly" }, + ], + + space_names: [ + "Red Cube Pool", + "Red Crisis Track Start", + "Red Crisis Track Escalation", + "Red Crisis Track Tension", + "Red Crisis Track Final Crisis", + "Red Bonus Cubes 1", + "Red Bonus Cubes 2", + "Red Bonus Cubes 3", + "Blue Cube Pool", + "Blue Crisis Track Start", + "Blue Crisis Track Escalation", + "Blue Crisis Track Tension", + "Blue Crisis Track Final Crisis", + "Blue Bonus Cubes 1", + "Blue Bonus Cubes 2", + "Blue Bonus Cubes 3", + "Royalists", + "National Assembly", + "Republicans", + "Catholic Church", + "Press", + "Social Movements", + "Mont-Valérien", + "Butte Montmartre", + "Fort d'Issy", + "Butte-Aux-Cailles", + "Père Lachaise", + "Château de Vincennes", + "Prussian Occupied Territory", + "Versailles HQ", + ], +} + +if (typeof module !== 'undefined') + module.exports = data diff --git a/play.html b/play.html new file mode 100644 index 0000000..fce9c2c --- /dev/null +++ b/play.html @@ -0,0 +1,398 @@ +<!DOCTYPE html> +<!-- vim:set nowrap: --> +<html> +<head> +<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1"> +<meta charset="UTF-8"> +<title>RED FLAG OVER PARIS</title> +<link rel="icon" href="favicon.svg"> +<link rel="stylesheet" href="/fonts/fonts.css"> +<link rel="stylesheet" href="/common/play.css"> +<script defer src="/common/play.js"></script> +<script defer src="data.js"></script> +<script defer src="play.js"></script> +<style> + +main { background-color: dimgray; } +header { background-color: gainsboro; } + +body.Commune header.your_turn { background-color: salmon; } +body.Versailles header.your_turn { background-color: skyblue; } +#role_Commune { background-color: salmon; } +#role_Versailles { background-color: skyblue; } + +#log { background-color: ivory; } +#log .h1 { background-color: tan; font-weight: bold; padding-top:2px; padding-bottom:2px; text-align: center; } +#log .h2 { background-color: wheat; padding-top:2px; padding-bottom:2px; text-align: center; } +#log .commune { background-color: lightpink } +#log .versailles { background-color: lightblue } +#log > .i { padding-left: 20px; } +#log .tip { font-style: italic } +#log .tip:hover { cursor: pointer; text-decoration: underline; } + +.role_extra { + float: right; +} + +.action { + cursor: pointer; +} + +#hand { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 20px; + gap: 20px; +} + +.card { + background-size: cover; + background-repeat: no-repeat; + background-color: ivory; + width: 250px; + height: 350px; + border-radius: 16px; + box-shadow: 1px 1px 5px rgba(0,0,0,0.5); +} + +.card_info { + padding: 12px 0; + border-bottom: 1px solid black; + background-color: gray; +} + +.card_info .card { + margin: 0 auto; + width: 125px; + height: 175px; + border-radius: 8px; +} + +#tooltip { + position: fixed; + z-index: 100; + right: 240px; + top: 60px; +} + +/* MAP */ + +#mapwrap { + box-shadow: 0px 0px 15px rgba(0,0,0,0.8); + width: 1500px; + height: 1050px; +} + +#map { + display: block; + width: 1500px; + height: 1050px; + background-image: url(map75.jpg); + background-size: 1500px 1050px; +} + +@media (min-resolution: 97dpi) { + #map { + background-image: url(map150.jpg); + } +} + +.space { + position: absolute; + border: 8px solid transparent; +} + +.space.action { + border-color: white; +} + +.piece { + position: absolute; + pointer-events: none; + background-size: cover; + background-repeat: no-repeat; + filter: drop-shadow(0px 2px 3px #0008); + transition-property: top, left; + transition-duration: 1s; + transition-timing-function: ease; +} + +.piece.action { + filter: + drop-shadow(0 -2px 0 white) + drop-shadow(0 2px 0 white) + drop-shadow(-2px 0 0 white) + drop-shadow(2px 0 0 white) +} + +.card.action { + box-shadow: 0 0 0 3px white; +} + +/* original size (72dpi?) */ +.piece.cube { width: 28px; height: 30px; } +.piece.disc { width: 40px; height: 32px; } +.piece.cylinder { width: 40px; height: 40px; } +.piece.pawn { width: 28px; height: 48px; } + +/* 125% */ +.piece.cube { width: 35px; height: 38px; } +.piece.disc { width: 50px; height: 40px; } +.piece.cylinder { width: 50px; height: 50px; } +.piece.pawn { width: 35px; height: 60px; } + +.piece.cube.red { background-image:url(pieces/red_cube.svg) } +.piece.cube.blue { background-image:url(pieces/blue_cube.svg) } +.piece.disc.red { background-image:url(pieces/red_disc.svg) } +.piece.disc.blue { background-image:url(pieces/blue_disc.svg) } +.piece.cylinder.red { background-image:url(pieces/red_cylinder.svg) } +.piece.cylinder.blue { background-image:url(pieces/blue_cylinder.svg) } +.piece.cylinder.orange { background-image:url(pieces/orange_cylinder.svg) } +.piece.cylinder.purple { background-image:url(pieces/purple_cylinder.svg) } +.piece.pawn { background-image:url(pieces/pawn.svg) } + +#round_marker { top: 965px; } +#round_marker.round1 { left: 623px; } +#round_marker.round2 { left: 709px; } +#round_marker.round3 { left: 805px; } +#round_marker.round4 { left: 897px; } + +#blue_momentum { top: 278px; } +#blue_momentum.m0 { top: 300px; left: 712px; } +#blue_momentum.m1 { left: 630px; } +#blue_momentum.m2 { left: 510px; } +#blue_momentum.m3 { left: 360px; } + +#red_momentum { top: 278px; } +#red_momentum.m0 { top: 273px; left: 742px; } +#red_momentum.m1 { left: 820px; } +#red_momentum.m2 { left: 950px; } +#red_momentum.m3 { left: 1095px; } + +#political_vp { top: 175px; } +#military_vp { top: 225px; } +.vp0 { left: 465px; } +.vp1 { left: 517px; } +.vp2 { left: 570px; } +.vp3 { left: 622px; } +.vp4 { left: 674px; } +.vp5 { left: 726px; } +.vp6 { left: 778px; } +.vp7 { left: 830px; } +.vp8 { left: 882px; } +.vp9 { left: 934px; } +.vp10 { left: 987px; } + +/* CARD ACTION POPUP MENU */ + +#popup { + position: fixed; + user-select: none; + background-color: gainsboro; + 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; + display: none; +} +#popup div { padding: 3pt 8pt; color: gray; display: none; } +#popup div.enabled { color: black; display: block; } +#popup div.enabled:hover { background-color: teal; color: white; } +#popup div.always { display: block; } + +/* CARD IMAGES */ + +.card_strategy_back{background-image:url(cards.1x/card_strategy_back.jpg)} +.card_objective_back{background-image:url(cards.1x/card_objective_back.jpg)} +.card_1{background-image:url(cards.1x/card_1.jpg)} +.card_2{background-image:url(cards.1x/card_2.jpg)} +.card_3{background-image:url(cards.1x/card_3.jpg)} +.card_4{background-image:url(cards.1x/card_4.jpg)} +.card_5{background-image:url(cards.1x/card_5.jpg)} +.card_6{background-image:url(cards.1x/card_6.jpg)} +.card_7{background-image:url(cards.1x/card_7.jpg)} +.card_8{background-image:url(cards.1x/card_8.jpg)} +.card_9{background-image:url(cards.1x/card_9.jpg)} +.card_10{background-image:url(cards.1x/card_10.jpg)} +.card_11{background-image:url(cards.1x/card_11.jpg)} +.card_12{background-image:url(cards.1x/card_12.jpg)} +.card_13{background-image:url(cards.1x/card_13.jpg)} +.card_14{background-image:url(cards.1x/card_14.jpg)} +.card_15{background-image:url(cards.1x/card_15.jpg)} +.card_16{background-image:url(cards.1x/card_16.jpg)} +.card_17{background-image:url(cards.1x/card_17.jpg)} +.card_18{background-image:url(cards.1x/card_18.jpg)} +.card_19{background-image:url(cards.1x/card_19.jpg)} +.card_20{background-image:url(cards.1x/card_20.jpg)} +.card_21{background-image:url(cards.1x/card_21.jpg)} +.card_22{background-image:url(cards.1x/card_22.jpg)} +.card_23{background-image:url(cards.1x/card_23.jpg)} +.card_24{background-image:url(cards.1x/card_24.jpg)} +.card_25{background-image:url(cards.1x/card_25.jpg)} +.card_26{background-image:url(cards.1x/card_26.jpg)} +.card_27{background-image:url(cards.1x/card_27.jpg)} +.card_28{background-image:url(cards.1x/card_28.jpg)} +.card_29{background-image:url(cards.1x/card_29.jpg)} +.card_30{background-image:url(cards.1x/card_30.jpg)} +.card_31{background-image:url(cards.1x/card_31.jpg)} +.card_32{background-image:url(cards.1x/card_32.jpg)} +.card_33{background-image:url(cards.1x/card_33.jpg)} +.card_34{background-image:url(cards.1x/card_34.jpg)} +.card_35{background-image:url(cards.1x/card_35.jpg)} +.card_36{background-image:url(cards.1x/card_36.jpg)} +.card_37{background-image:url(cards.1x/card_37.jpg)} +.card_38{background-image:url(cards.1x/card_38.jpg)} +.card_39{background-image:url(cards.1x/card_39.jpg)} +.card_40{background-image:url(cards.1x/card_40.jpg)} +.card_41{background-image:url(cards.1x/card_41.jpg)} +.card_42{background-image:url(cards.1x/card_42.jpg)} +.card_43{background-image:url(cards.1x/card_43.jpg)} +.card_44{background-image:url(cards.1x/card_44.jpg)} +.card_45{background-image:url(cards.1x/card_45.jpg)} +.card_46{background-image:url(cards.1x/card_46.jpg)} +.card_47{background-image:url(cards.1x/card_47.jpg)} +.card_48{background-image:url(cards.1x/card_48.jpg)} +.card_49{background-image:url(cards.1x/card_49.jpg)} +.card_50{background-image:url(cards.1x/card_50.jpg)} +.card_51{background-image:url(cards.1x/card_51.jpg)} +.card_52{background-image:url(cards.1x/card_52.jpg)} +.card_53{background-image:url(cards.1x/card_53.jpg)} +.card_54{background-image:url(cards.1x/card_54.jpg)} +@media (min-resolution:97dpi) { +.card_strategy_back{background-image:url(cards.2x/card_strategy_back.jpg)} +.card_objective_back{background-image:url(cards.2x/card_objective_back.jpg)} +.card_1{background-image:url(cards.2x/card_1.jpg)} +.card_2{background-image:url(cards.2x/card_2.jpg)} +.card_3{background-image:url(cards.2x/card_3.jpg)} +.card_4{background-image:url(cards.2x/card_4.jpg)} +.card_5{background-image:url(cards.2x/card_5.jpg)} +.card_6{background-image:url(cards.2x/card_6.jpg)} +.card_7{background-image:url(cards.2x/card_7.jpg)} +.card_8{background-image:url(cards.2x/card_8.jpg)} +.card_9{background-image:url(cards.2x/card_9.jpg)} +.card_10{background-image:url(cards.2x/card_10.jpg)} +.card_11{background-image:url(cards.2x/card_11.jpg)} +.card_12{background-image:url(cards.2x/card_12.jpg)} +.card_13{background-image:url(cards.2x/card_13.jpg)} +.card_14{background-image:url(cards.2x/card_14.jpg)} +.card_15{background-image:url(cards.2x/card_15.jpg)} +.card_16{background-image:url(cards.2x/card_16.jpg)} +.card_17{background-image:url(cards.2x/card_17.jpg)} +.card_18{background-image:url(cards.2x/card_18.jpg)} +.card_19{background-image:url(cards.2x/card_19.jpg)} +.card_20{background-image:url(cards.2x/card_20.jpg)} +.card_21{background-image:url(cards.2x/card_21.jpg)} +.card_22{background-image:url(cards.2x/card_22.jpg)} +.card_23{background-image:url(cards.2x/card_23.jpg)} +.card_24{background-image:url(cards.2x/card_24.jpg)} +.card_25{background-image:url(cards.2x/card_25.jpg)} +.card_26{background-image:url(cards.2x/card_26.jpg)} +.card_27{background-image:url(cards.2x/card_27.jpg)} +.card_28{background-image:url(cards.2x/card_28.jpg)} +.card_29{background-image:url(cards.2x/card_29.jpg)} +.card_30{background-image:url(cards.2x/card_30.jpg)} +.card_31{background-image:url(cards.2x/card_31.jpg)} +.card_32{background-image:url(cards.2x/card_32.jpg)} +.card_33{background-image:url(cards.2x/card_33.jpg)} +.card_34{background-image:url(cards.2x/card_34.jpg)} +.card_35{background-image:url(cards.2x/card_35.jpg)} +.card_36{background-image:url(cards.2x/card_36.jpg)} +.card_37{background-image:url(cards.2x/card_37.jpg)} +.card_38{background-image:url(cards.2x/card_38.jpg)} +.card_39{background-image:url(cards.2x/card_39.jpg)} +.card_40{background-image:url(cards.2x/card_40.jpg)} +.card_41{background-image:url(cards.2x/card_41.jpg)} +.card_42{background-image:url(cards.2x/card_42.jpg)} +.card_43{background-image:url(cards.2x/card_43.jpg)} +.card_44{background-image:url(cards.2x/card_44.jpg)} +.card_45{background-image:url(cards.2x/card_45.jpg)} +.card_46{background-image:url(cards.2x/card_46.jpg)} +.card_47{background-image:url(cards.2x/card_47.jpg)} +.card_48{background-image:url(cards.2x/card_48.jpg)} +.card_49{background-image:url(cards.2x/card_49.jpg)} +.card_50{background-image:url(cards.2x/card_50.jpg)} +.card_51{background-image:url(cards.2x/card_51.jpg)} +.card_52{background-image:url(cards.2x/card_52.jpg)} +.card_53{background-image:url(cards.2x/card_53.jpg)} +.card_54{background-image:url(cards.2x/card_54.jpg)} +} + +</style> +</head> +<body> + +<div id="tooltip" class="card hide"></div> + +<div id="popup" onmouseleave="hide_popup_menu()"> +<div id="menu_card_event" class="always" onclick="on_card_event()">Play event</div> +<div id="menu_card_ops" class="always" onclick="on_card_ops()">Operations</div> +<div id="menu_card_use_discarded" class="always" onclick="on_card_use_discarded()">Use discarded event</div> +<div id="menu_card_advance_momentum" class="always" onclick="on_card_advance_momentum()">Advance momentum</div> +</div> + +<header> + <div id="toolbar"> + <div class="menu"> + <div class="menu_title"><img src="/images/cog.svg"></div> + <div class="menu_popup"> + <a class="menu_item" href="info/rules.html" target="_blank">Rules</a> + <a class="menu_item" href="info/playbook.html" target="_blank">Playbook</a> + <a class="menu_item" href="info/pac.html" target="_blank">Player Aids</a> + <a class="menu_item" href="info/cards.html" target="_blank">Cards</a> + <div class="resign menu_separator"></div> + <div class="resign menu_item" onclick="confirm_resign()">Resign</div> + </div> + </div> + <div class="icon_button" onclick="toggle_zoom()"><img src="/images/magnifying-glass.svg"></div> + <div class="icon_button" onclick="toggle_log()"><img src="/images/scroll-quill.svg"></div> + </div> + <div id="prompt"></div> + <div id="actions"></div> +</header> + +<aside> + <div id="roles"> + <div class="role" id="role_Commune"> + <div class="role_name"> + Commune + <div class="role_extra" id="commune_info"></div> + <div class="role_user">-</div> + </div> + </div> + <div class="role" id="role_Versailles"> + <div class="role_name"> + Versailles + <div class="role_extra" id="versailles_info"></div> + <div class="role_user">-</div> + </div> + </div> + <div class="card_info"><div id="active_card" class="card card_strategy_back"></div></div> + </div> + <div id="log"></div> +</aside> + +<main> + +<div id="mapwrap" class="fit"> +<div id="map"> +<div id="spaces"></div> +<div id="pieces"> +<div id="round_marker" class="piece pawn round1"></div> +<div id="political_vp" class="piece cylinder orange vp5"></div> +<div id="military_vp" class="piece cylinder purple vp5"></div> +<div id="red_momentum" class="piece cylinder red m0"></div> +<div id="blue_momentum" class="piece cylinder blue m0"></div> +</div> +</div> +</div> + +<div id="hand"></div> + +</main> + +<footer id="status"></footer> + +</body> @@ -0,0 +1,366 @@ +"use strict" + +const space_count = data.space_names.length + +let layout = [] +let space_layout_cube = [] +let space_layout_disc = [] + +let ui = { + cards: [ null ], + cubes: [], + discs: [], + spaces: [], + red_momentum: document.getElementById("red_momentum"), + blue_momentum: document.getElementById("blue_momentum"), + political_vp: document.getElementById("political_vp"), + military_vp: document.getElementById("military_vp"), + round_marker: document.getElementById("round_marker"), +} + +// :r !python3 tools/genboxes.py +const boxes = { + "Royalists": [318,1711,506,505], + "Republicans": [1960,1712,504,504], + "Catholic Church": [318,3146,505,505], + "Social Movements": [1960,3146,506,505], + "Fort d'Issy": [3374,3172,506,506], + "Butte-Aux-Cailles": [4153,2642,505,505], + "Père Lachaise": [4824,2364,506,505], + "Château de Vincennes": [5369,2599,504,507], + "Press": [1176,2908,447,448], + "National Assembly": [1168,1984,447,447], + "Butte Montmartre": [4208,1842,447,444], + "Mont-Valérien": [2868,2028,447,448], + "Versailles HQ": [2646,3398,404,380], + "Prussian Occupied Territory": [5190,1413,718,346], + "Red Crisis Track Start": [3928,244,361,448], + "Red Crisis Track Escalation": [4289,244,334,448], + "Red Crisis Track Tension": [4623,244,332,448], + "Red Crisis Track Final Crisis": [4955,244,332,448], + "Blue Crisis Track Start": [1728,244,354,446], + "Blue Crisis Track Escalation": [1394,244,334,446], + "Blue Crisis Track Tension": [1061,244,333,446], + "Blue Crisis Track Final Crisis": [728,244,333,446], + "Blue Objective Card": [479,3983,1088,218], + "Red Objective Card": [4454,3984,1088,218], + "Red Cube Pool": [4498,790,791,219], + "Blue Cube Pool": [720,790,791,219], + "Red Bonus Cubes 1": [4289,89,334,155], + "Red Bonus Cubes 2": [4623,89,332,155], + "Red Bonus Cubes 3": [4955,89,332,155], + "Blue Bonus Cubes 1": [1394,89,334,155], + "Blue Bonus Cubes 2": [1061,89,333,155], + "Blue Bonus Cubes 3": [728,89,333,155], +} + +function is_card_action(action, card) { + if (view.actions && view.actions[action] && view.actions[action].includes(card)) + return true + return false +} + +function is_cube_action(i) { + if (view.actions && view.actions.cube && view.actions.cube.includes(i)) + return true + return false +} + +function is_disc_action(i) { + if (view.actions && view.actions.disc && view.actions.disc.includes(i)) + return true + return false +} + +function is_space_action(i) { + if (view.actions && view.actions.space && view.actions.space.includes(i)) + return true + return false +} + +function on_blur(evt) { + document.getElementById("status").textContent = "" +} + +function on_focus_space(evt) { + document.getElementById("status").textContent = evt.target.my_name +} + +function on_click_space(evt) { + if (evt.button === 0) { + if (send_action('space', evt.target.my_space)) + evt.stopPropagation() + } +} + +function on_click_cube(evt) { + if (evt.button === 0) { + if (send_action('cube', evt.target.my_cube)) + evt.stopPropagation() + } +} + +function on_click_disc(evt) { + if (evt.button === 0) { + if (send_action('disc', evt.target.my_disc)) + evt.stopPropagation() + } +} + +function build_user_interface() { + let elt + + for (let c = 1; c <= 41 + 12; ++c) { + elt = ui.cards[c] = document.createElement("div") + elt.className = `card card_${c}` + elt.my_card = c + elt.addEventListener("click", on_click_card) + } + + for (let i = 0; i < 36; ++i) { + elt = ui.cubes[i] = document.createElement("div") + if (i < 18) + elt.className = "piece cube red" + else + elt.className = "piece cube blue" + elt.my_cube = i + elt.addEventListener("mousedown", on_click_cube) + document.getElementById("pieces").appendChild(elt) + } + + for (let i = 0; i < 4; ++i) { + elt = ui.discs[i] = document.createElement("div") + if (i < 2) + elt.className = "piece disc red" + else + elt.className = "piece disc blue" + elt.my_disc = i + elt.addEventListener("mousedown", on_click_disc) + } + + for (let i = 0; i < space_count; ++i) { + let name = data.space_names[i] + let r = boxes[name] + elt = ui.spaces[i] = document.createElement("div") + elt.className = "space" + elt.my_space = i + elt.my_name = name + elt.addEventListener("mousedown", on_click_space) + elt.addEventListener("mouseenter", on_focus_space) + elt.addEventListener("mouseleave", on_blur) + let bw = 8 + let x = Math.round(r[0] / 4) + let y = Math.round(r[1] / 4) + let w = Math.round(r[2] / 4) + let h = Math.round(r[3] / 4) + elt.style.top = y + "px" + elt.style.left = x + "px" + elt.style.width = (w - bw * 2) + "px" + elt.style.height = (h - bw * 2) + "px" + space_layout_cube[i] = { x: x + Math.round(w/2), y: y + Math.round(h*1/2) } + space_layout_disc[i] = { x: x + w, y: y + h } + document.getElementById("spaces").appendChild(elt) + } +} + +function layout_cubes(list, xorig, yorig) { + const dx = 20 + const dy = 11 + if (list.length > 0) { + let ncol = Math.round(Math.sqrt(list.length)) + let nrow = Math.ceil(list.length / ncol) + function place_cube(row, col, e, z) { + let x = xorig - (row * dx - col * dx) - 18 + (nrow-ncol) * 6 + let y = yorig - (row * dy + col * dy) - 28 + (nrow-1) * 8 + e.style.left = x + "px" + e.style.top = y + "px" + e.style.zIndex = z + } + let z = 50 + let i = 0 + for (let row = 0; row < nrow; ++row) + for (let col = 0; col < ncol && i < list.length; ++col) + place_cube(row, col, list[list.length-(++i)], z--) + } +} + +function layout_disc(s, disc) { + if (s > 0) + disc.classList.remove("hide") + else + disc.classList.add("hide") + disc.style.left = (space_layout_disc[s].x - 50 - 12) + "px" + disc.style.top = (space_layout_disc[s].y - 20 - 8) + "px" +} + +function on_focus_card_tip(card_number) { + document.getElementById("tooltip").className = "card card_" + card_number +} + +function on_blur_card_tip() { + document.getElementById("tooltip").classList = "card hide" +} + +function sub_card_name(match, p1, offset, string) { + let c = p1 | 0 + let n = data.cards[c].name + return `<span class="tip" onmouseenter="on_focus_card_tip(${c})" onmouseleave="on_blur_card_tip()">${n}</span>` +} + +function on_log(text) { + let p = document.createElement("div") + + if (text.match(/^>/)) { + text = text.substring(1) + p.className = 'i' + } + + text = text.replace(/&/g, "&") + text = text.replace(/</g, "<") + text = text.replace(/>/g, ">") + text = text.replace(/#(\d+)/g, sub_card_name) + + if (text.match(/^\.h1/)) { + text = text.substring(4) + p.className = 'h1' + } + + if (text.match(/^\.h2/)) { + text = text.substring(4) + if (text === 'Commune') + p.className = 'h2 commune' + else if (text === 'Versailles') + p.className = 'h2 versailles' + else + p.className = 'h2' + } + + p.innerHTML = text + return p +} + +function on_update() { + if (view.active_card) + document.getElementById("active_card").className = `card card_${view.active_card}` + else + document.getElementById("active_card").className = `card card_strategy_back` + + if (view.initiative === "Commune") + document.getElementById("commune_info").textContent = "\u2756" + else + document.getElementById("commune_info").textContent = "" + if (view.initiative === "Versailles") + document.getElementById("versailles_info").textContent = "\u2756" + else + document.getElementById("versailles_info").textContent = "" + + ui.round_marker.className = `piece pawn round${view.round}` + ui.red_momentum.className = `piece cylinder red m${view.red_momentum}` + ui.blue_momentum.className = `piece cylinder blue m${view.blue_momentum}` + ui.military_vp.className = `piece cylinder purple vp${5+view.military_vp}` + ui.political_vp.className = `piece cylinder orange vp${5+view.political_vp}` + + document.getElementById("hand").replaceChildren() + if (view.objective) + document.getElementById("hand").appendChild(ui.cards[view.objective]) + if (view.hand) + for (let c of view.hand) + document.getElementById("hand").appendChild(ui.cards[c]) + + for (let i = 0; i < space_count; ++i) + layout[i] = [] + for (let i = 0; i < 36; ++i) { + layout[view.cubes[i]].push(ui.cubes[i]) + ui.cubes[i].classList.toggle("action", is_cube_action(i)) + } + for (let i = 0; i < space_count; ++i) { + layout_cubes(layout[i], space_layout_cube[i].x, space_layout_cube[i].y) + ui.spaces[i].classList.toggle("action", is_space_action(i)) + } + for (let i = 0; i < 4; ++i) { + layout_disc(view.discs[i], ui.discs[i]) + ui.discs[i].classList.toggle("action", is_cube_action(i)) + } + + for (let i = 1; i < ui.cards.length; ++i) { + ui.cards[i].classList.toggle("action", is_card_action('card', i)) + } + + action_button("commune", "Commune") + action_button("versailles", "Versailles") + action_button("undo", "Undo") +} + +/* CARD ACTION MENU */ + +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() { + if (send_action('card_event', current_popup_card)) + hide_popup_menu() +} + +function on_card_ops() { + if (send_action('card_ops', current_popup_card)) + hide_popup_menu() +} + +function on_card_use_discarded() { + if (send_action('card_use_discarded', current_popup_card)) + hide_popup_menu() +} + +function on_card_advance_momentum() { + if (send_action('card_advance_momentum', current_popup_card)) + hide_popup_menu() +} + +function on_click_card(evt) { + if (evt.button === 0) { + if (view.actions) { + let card = evt.target.my_card + if (is_card_action('card', card)) { + send_action('card', card) + } else { + let menu = [] + if (is_card_action('card_event', card)) + menu.push('card_event') + if (is_card_action('card_ops', card)) + menu.push('card_ops') + if (is_card_action('card_use_discarded', card)) + menu.push('card_use_discarded') + if (is_card_action('card_advance_momentum', card)) + menu.push('card_advance_momentum') + if (menu.length > 0) { + current_popup_card = card + show_popup_menu(evt, menu) + } + } + } + } +} + +build_user_interface() +scroll_with_middle_mouse("main") diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..51d2753 --- /dev/null +++ b/rules.js @@ -0,0 +1,583 @@ +"use strict" + +const data = require("./data") + +const RED_CUBE_POOL = 0 +const RED_CRISIS_TRACK = [1, 2, 3, 4] +const RED_BONUS_CUBES = [ 5, 6, 7 ] +const BLUE_CUBE_POOL = 8 +const BLUE_CRISIS_TRACK = [9, 10, 11, 12] +const BLUE_BONUS_CUBES = [ 13, 14, 15 ] +const S_ROYALISTS = 16 +const S_NATIONAL_ASSEMBLY = 17 +const S_REPUBLICANS = 18 +const S_CATHOLIC_CHURCH = 19 +const S_PRESS = 20 +const S_SOCIAL_MOVEMENTS = 21 +const S_MONT_VALERIEN = 22 +const S_BUTTE_MONTMARTRE = 23 +const S_FORT_D_ISSY = 24 +const S_BUTTE_AUX_CAILLES = 25 +const S_PERE_LACHAISE = 26 +const S_CHATEAU_DE_VINCENNES = 27 +const S_PRUSSIAN_OCCUPIED_TERRITORY = 28 +const S_VERSAILLES_HQ = 29 + +var game, view + +var states = {} + +exports.scenarios = [ "Standard" ] + +exports.roles = [ "Commune", "Versailles" ] + +exports.action = function (state, current, action, arg) { + game = state + if (action in states[game.state]) { + states[game.state][action](arg, current) + } else { + if (action === "undo" && game.undo && game.undo.length > 0) + pop_undo() + else + throw new Error("Invalid action: " + action) + } + return game +} + +exports.resign = function (state, current) { + game = state + if (game.state !== "game_over") { + log_br() + log(`${current} resigned.`) + game.state = "game_over" + game.active = null + game.state = "game_over" + game.result = (current === "Commune" ? "Versailles" : "Commune") + game.victory = current + " resigned." + } + return game +} + +exports.is_checkpoint = function (a, b) { + return a.round !== b.round +} + +exports.view = function(state, current) { + game = state + + view = { + log: game.log, + prompt: null, + actions: null, + + round: game.round, + initiative: game.initiative, + political_vp: game.political_vp, + military_vp: game.military_vp, + + red_hand: game.red_hand.length, + blue_hand: game.blue_hand.length, + red_momentum: game.red_momentum, + blue_momentum: game.blue_momentum, + + discs: game.discs, + cubes: game.cubes, + + active_card: game.active_card, + hand: 0, + objective: 0 + } + + if (current === "Commune") { + view.hand = game.red_hand + view.objective = game.red_objective + } + if (current === "Versailles") { + view.hand = game.blue_hand + view.objective = game.blue_objective + } + + if (game.state === "game_over") { + view.prompt = game.victory + } else if (current === "Observer" || (game.active !== current && game.active !== "Both")) { + if (states[game.state]) { + let inactive = states[game.state].inactive || game.state + view.prompt = `Waiting for ${game.active} to ${inactive}...` + } else { + view.prompt = "Unknown state: " + game.state + } + } else { + view.actions = {} + if (states[game.state]) + states[game.state].prompt(current) + else + view.prompt = "Unknown state: " + game.state + if (view.actions.undo === undefined) { + if (game.undo && game.undo.length > 0) + view.actions.undo = 1 + else + view.actions.undo = 0 + } + } + + return view +} + +exports.setup = function (seed, scenario, options) { + game = { + seed: seed, + log: [], + undo: [], + active: "Both", + state: "choose_objective_card", + + round: 1, + initiative: null, + political_vp: 0, + military_vp: 0, + red_momentum: 0, + blue_momentum: 0, + + strategy_deck: [], + objective_deck: [], + + red_hand: [ 34 ], + red_objective: 0, + blue_hand: [ 17 ], + blue_objective: 0, + + cubes: [ + // red cubes + RED_CRISIS_TRACK[0], + RED_CRISIS_TRACK[0], + RED_CRISIS_TRACK[0], + RED_CRISIS_TRACK[1], + RED_CRISIS_TRACK[1], + RED_CRISIS_TRACK[2], + RED_CRISIS_TRACK[2], + RED_CRISIS_TRACK[3], + RED_CRISIS_TRACK[3], + RED_BONUS_CUBES[0], + RED_BONUS_CUBES[0], + RED_BONUS_CUBES[1], + RED_BONUS_CUBES[1], + RED_BONUS_CUBES[2], + RED_BONUS_CUBES[2], + S_PRESS, + S_SOCIAL_MOVEMENTS, + S_PERE_LACHAISE, + + // blue cubes + BLUE_CRISIS_TRACK[0], + BLUE_CRISIS_TRACK[1], + BLUE_CRISIS_TRACK[1], + BLUE_CRISIS_TRACK[2], + BLUE_CRISIS_TRACK[3], + BLUE_CRISIS_TRACK[3], + BLUE_BONUS_CUBES[0], + BLUE_BONUS_CUBES[1], + BLUE_BONUS_CUBES[2], + BLUE_BONUS_CUBES[2], + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + BLUE_CUBE_POOL, + S_ROYALISTS, + S_PRESS, + ], + + discs: [ 0, 0, 0, 0 ], + + active_card: 0, + count: 0, + } + + log_h1("Red Flag Over Paris") + log_h1("Round 1") + + for (let i = 1; i <= 41; ++i) + if (i !== 17 && i !== 34) + game.strategy_deck.push(i) + + for (let i = 42; i <= 53; ++i) + game.objective_deck.push(i) + + shuffle(game.strategy_deck) + shuffle(game.objective_deck) + + for (let i = 0; i < 4; ++i) { + game.red_hand.push(game.strategy_deck.pop()) + game.blue_hand.push(game.strategy_deck.pop()) + } + + for (let i = 0; i < 2; ++i) { + game.red_hand.push(game.objective_deck.pop()) + game.blue_hand.push(game.objective_deck.pop()) + } + + return game +} + +// === GAME STATES === + +function is_objective_card(c) { + return c >= 42 && c <= 53 +} + +function is_strategy_card(c) { + return !is_objective_card(c) && c !== 17 && c !== 34 +} + +function enemy_player() { + if (game.active === "Commune") + return "Versailles" + return "Commune" +} + +function player_hand(current) { + if (current === "Commune") + return game.red_hand + return game.blue_hand +} + +function can_play_event(c) { + let side = data.cards[c].side + if (side === game.active || side === "Neutral") + return true + return false +} + +function can_advance_momentum() { + if (game.active === "Commune") + return game.red_momentum < 3 + return game.blue_momentum < 3 +} + +states.choose_objective_card = { + inactive: "choose an objective card", + prompt(current) { + view.prompt = "Choose an Objective card." + for (let c of player_hand(current)) { + if (is_objective_card(c)) { + gen_action("card", c) + } + } + }, + card(c, current) { + if (current === "Commune") { + game.red_objective = c + game.red_hand = game.red_hand.filter(c => !is_objective_card(c)) + } else { + game.blue_objective = c + game.blue_hand = game.blue_hand.filter(c => !is_objective_card(c)) + } + if (game.red_objective > 0 && game.blue_objective > 0) + goto_initiative_phase() + else if (game.red_objective > 0) + game.active = "Versailles" + else if (game.blue_objective > 0) + game.active = "Commune" + else + game.active = "Both" + }, +} + +function goto_initiative_phase() { + game.active = "Commune" + game.state = "initiative_phase" +} + +states.initiative_phase = { + inactive: "decide player order", + prompt() { + view.prompt = "Decide player order." + view.actions.commune = 1 + view.actions.versailles = 1 + }, + commune() { + log("Initiative: Commune") + game.initiative = "Commune" + game.active = game.initiative + goto_strategy_phase() + }, + versailles() { + log("Initiative: Versailles") + game.initiative = "Versailles" + game.active = game.initiative + goto_strategy_phase() + }, +} + +function goto_strategy_phase() { + clear_undo() + log_h2(game.active) + game.state = "strategy_phase" +} + +function resume_strategy_phase() { + game.active = enemy_player() + goto_strategy_phase() +} + +states.strategy_phase = { + inactive: "play a card", + prompt() { + view.prompt = "Play a card." + for (let c of player_hand(game.active)) { + console.log(game.active, "hand", c) + if (is_strategy_card(c)) { + if (can_play_event(c)) + gen_action("card_event", c) + gen_action("card_ops", c) + if (game.active_card > 0 && can_play_event(game.active_card)) + gen_action("card_use_discarded", c) + if (can_advance_momentum()) + gen_action("card_advance_momentum", c) + } + } + }, + card_event(c) { + push_undo() + log(`Played #${c} for event.`) + array_remove_item(player_hand(game.active), c) + game.active_card = c + goto_play_event() + }, + card_ops(c) { + push_undo() + log(`Played #${c} for ${data.cards[c].ops} ops.`) + array_remove_item(player_hand(game.active), c) + game.active_card = c + game.count = data.cards[c].ops + game.state = "operations" + }, + card_advance_momentum(c) { + push_undo() + log(`Played #${c} to advance momentum.`) + array_remove_item(player_hand(game.active), c) + game.active_card = c + if (game.active === "Commune") + game.red_momentum += 1 + else + game.blue_momentum += 1 + // TODO: momentum trigger + resume_strategy_phase() + }, + card_use_discarded(c) { + push_undo() + log(`Discarded #${c} to play #${game.active_card}.`) + let old_c = game.active_card + array_remove_item(player_hand(game.active), c) + game.active_card = c + goto_play_event(old_c) + }, +} + +function goto_play_event(c) { + switch (c) { + // TODO + } + resume_strategy_phase() +} + +// === COMMON LIBRARY === + +function gen_action(action, argument) { + if (argument !== undefined) { + if (!(action in view.actions)) + view.actions[action] = [] + set_add(view.actions[action], argument) + } else { + view.actions[action] = 1 + } +} + +function random(range) { + // An MLCG using integer arithmetic with doubles. + // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf + // m = 2**35 − 31 + return (game.seed = game.seed * 200105 % 34359738337) % range +} + +function shuffle(list) { + for (let i = list.length - 1; i > 0; --i) { + let j = random(i + 1) + let tmp = list[j] + list[j] = list[i] + list[i] = tmp + } +} + +// remove item at index (faster than splice) +function array_remove(array, index) { + let n = array.length + for (let i = index + 1; i < n; ++i) + array[i - 1] = array[i] + array.length = n - 1 + return array +} + +// insert item at index (faster than splice) +function array_insert(array, index, item) { + for (let i = array.length; i > index; --i) + array[i] = array[i - 1] + array[index] = item + return array +} + +function array_remove_item(array, item) { + let i = array.indexOf(item) + if (i >= 0) + array_remove(array, i) +} + +function set_clear(set) { + set.length = 0 +} + +function set_has(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return true + } + return false +} + +function set_add(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return set + } + return array_insert(set, a, item) +} + +function set_delete(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return array_remove(set, m) + } + return set +} + +function set_toggle(set, item) { + let a = 0 + let b = set.length - 1 + while (a <= b) { + let m = (a + b) >> 1 + let x = set[m] + if (item < x) + b = m - 1 + else if (item > x) + a = m + 1 + else + return array_remove(set, m) + } + return array_insert(set, a, item) +} + + +// Fast deep copy for objects without cycles +function object_copy(original) { + if (Array.isArray(original)) { + let n = original.length + let copy = new Array(n) + for (let i = 0; i < n; ++i) { + let v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } else { + let copy = {} + for (let i in original) { + let v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } +} + +function clear_undo() { + if (game.undo.length > 0) + game.undo = [] +} + +function push_undo() { + let copy = {} + for (let k in game) { + let v = game[k] + if (k === "undo") + continue + else if (k === "log") + v = v.length + else if (typeof v === "object" && v !== null) + v = object_copy(v) + copy[k] = v + } + game.undo.push(copy) +} + +function pop_undo() { + let save_log = game.log + let save_undo = game.undo + game = save_undo.pop() + save_log.length = game.log + game.log = save_log + game.undo = save_undo +} + +function log(msg) { + game.log.push(msg) +} + +function log_br() { + if (game.log.length > 0 && game.log[game.log.length-1] !== "") + game.log.push("") +} + +function logi(msg) { + game.log.push(">" + msg) +} + +function log_h1(msg) { + log_br() + log(".h1 " + msg) + log_br() +} + +function log_h2(msg) { + log_br() + log(".h2 " + msg) + log_br() +} |