summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2024-05-30 17:08:53 +0200
committerTor Andersson <tor@ccxvii.net>2024-05-30 21:59:25 +0200
commitae37a6749a32ffe975212ff8658ff8493e2316ec (patch)
treec191f595be46ef81b1423ff77a11b28327a565f0
parentcbb387b143485a332f4af3b6b3aee91ec72f7ffb (diff)
downloadfriedrich-ae37a6749a32ffe975212ff8658ff8493e2316ec.tar.gz
Initial code.
-rw-r--r--info/cards.html43
-rw-r--r--play.css542
-rw-r--r--play.html2
-rw-r--r--play.js709
-rw-r--r--rules.js2649
5 files changed, 3920 insertions, 25 deletions
diff --git a/info/cards.html b/info/cards.html
index 3821b42..ed44f3d 100644
--- a/info/cards.html
+++ b/info/cards.html
@@ -9,31 +9,30 @@ html {
}
img {
- border-radius: 8px;
- border: 1px solid black;
+ border-radius: 16px;
+ border: 2px solid black;
+ box-shadow: 1px 1px 8px #0004;
margin: 10px;
}
</style>
<body>
-<img src="../cards/Fried.fatecard.reverse.100.png">
+<img src="../cards/Friedrich.Fatecard.07.150.png">
+<img src="../cards/Friedrich.Fatecard.08.150.png">
+<img src="../cards/Friedrich.Fatecard.09.150.png">
+<img src="../cards/Friedrich.Fatecard.16.150.png">
+<img src="../cards/Friedrich.Fatecard.17.150.png">
+<img src="../cards/Friedrich.Fatecard.18.150.png">
-<img src="../cards/Fried.fatecard.07.100.png">
-<img src="../cards/Fried.fatecard.08.100.png">
-<img src="../cards/Fried.fatecard.09.100.png">
-<img src="../cards/Fried.fatecard.16.100.png">
-<img src="../cards/Fried.fatecard.17.100.png">
-<img src="../cards/Fried.fatecard.18.100.png">
-
-<img src="../cards/Fried.fatecard.01.100.png">
-<img src="../cards/Fried.fatecard.02.100.png">
-<img src="../cards/Fried.fatecard.03.100.png">
-<img src="../cards/Fried.fatecard.04.100.png">
-<img src="../cards/Fried.fatecard.05.100.png">
-<img src="../cards/Fried.fatecard.06.100.png">
-<img src="../cards/Fried.fatecard.10.100.png">
-<img src="../cards/Fried.fatecard.11.100.png">
-<img src="../cards/Fried.fatecard.12.100.png">
-<img src="../cards/Fried.fatecard.13.100.png">
-<img src="../cards/Fried.fatecard.14.100.png">
-<img src="../cards/Fried.fatecard.15.100.png">
+<img src="../cards/Friedrich.Fatecard.01.150.png">
+<img src="../cards/Friedrich.Fatecard.02.150.png">
+<img src="../cards/Friedrich.Fatecard.03.150.png">
+<img src="../cards/Friedrich.Fatecard.04.150.png">
+<img src="../cards/Friedrich.Fatecard.05.150.png">
+<img src="../cards/Friedrich.Fatecard.06.150.png">
+<img src="../cards/Friedrich.Fatecard.10.150.png">
+<img src="../cards/Friedrich.Fatecard.11.150.png">
+<img src="../cards/Friedrich.Fatecard.12.150.png">
+<img src="../cards/Friedrich.Fatecard.13.150.png">
+<img src="../cards/Friedrich.Fatecard.14.150.png">
+<img src="../cards/Friedrich.Fatecard.15.150.png">
diff --git a/play.css b/play.css
new file mode 100644
index 0000000..49b9876
--- /dev/null
+++ b/play.css
@@ -0,0 +1,542 @@
+/*
+ Friedrich
+*/
+
+:root {
+ --color-prussia: #005478;
+ --color-hanover: #91c9ed;
+ --color-russia: #147d36;
+ --color-sweden: #b2d233;
+ --color-austria: #f5f5f5;
+ --color-imperial: #fbe300;
+ --color-france: #ed1c23;
+}
+
+:root {
+ --color-prussia: hsl(198, 100%, 24%);
+ --color-hanover: hsl(203, 72%, 75%);
+ --color-russia: hsl(139, 72%, 28%);
+ --color-sweden: hsl(72, 64%, 51%);
+ --color-austria: hsl(0, 0%, 96%);
+ --color-imperial: hsl(54, 100%, 49%);
+ --color-france: hsl(358, 85%, 52%);
+
+ --color-light-prussia: hsl(210, 80%, 75%);
+ --color-light-hanover: hsl(200, 75%, 75%);
+ --color-light-russia: hsl(120, 50%, 70%);
+ --color-light-sweden: hsl(80, 60%, 75%);
+ --color-light-austria: hsl(0, 0%, 100%);
+ --color-light-imperial: hsl(55, 85%, 75%);
+ --color-light-france: hsl(0, 70%, 80%);
+
+ --color-dark-prussia: hsl(198, 100%, 24%);
+ --color-dark-hanover: hsl(203, 72%, 75%);
+ --color-dark-russia: hsl(139, 72%, 28%);
+ --color-dark-sweden: hsl(72, 64%, 51%);
+ --color-dark-austria: hsl(0, 0%, 96%);
+ --color-dark-imperial: hsl(54, 100%, 49%);
+ --color-dark-france: hsl(358, 85%, 52%);
+
+ --color-role-prussia: hsl(210, 80%, 75%);
+ --color-role-russia: hsl(120, 50%, 70%);
+ --color-role-austria: hsl(0, 0%, 98%);
+ --color-role-france: hsl(0, 70%, 80%);
+}
+
+#role_Frederick { background-color: var(--color-role-prussia) }
+#role_Elisabeth { background-color: var(--color-role-russia) }
+#role_Maria_Theresa { background-color: var(--color-role-austria) }
+#role_Pompadour { background-color: var(--color-role-france) }
+
+#log { background-color: #f3ebd4 }
+
+#log .h {
+ background-color: tan;
+ border-top: 1px solid black; border-bottom: 1px solid black;
+ margin: 4px 0;
+}
+
+#log .h.prussia { background-color: var(--color-light-prussia); }
+#log .h.hanover { background-color: var(--color-light-hanover); }
+#log .h.russia { background-color: var(--color-light-russia); }
+#log .h.sweden { background-color: var(--color-light-sweden); }
+#log .h.austria { background-color: var(--color-light-austria); }
+#log .h.imperial { background-color: var(--color-light-imperial); }
+#log .h.france { background-color: var(--color-light-france); }
+
+header.your_turn.prussia { background-color: var(--color-light-prussia); }
+header.your_turn.hanover { background-color: var(--color-light-hanover); }
+header.your_turn.russia { background-color: var(--color-light-russia); }
+header.your_turn.sweden { background-color: var(--color-light-sweden); }
+header.your_turn.austria { background-color: var(--color-light-austria); }
+header.your_turn.imperial { background-color: var(--color-light-imperial); }
+header.your_turn.france { background-color: var(--color-light-france); }
+
+
+#mapwrap {
+ width: 2485px;
+ height: 1654px;
+ margin-bottom: 30px;
+}
+
+#map {
+ position: absolute;
+ width: 2485px;
+ height: 1654px;
+ background-size: cover;
+ background-image: url(Fried.Gameboard.075.jpg);
+ background-color: #ece1a9;
+}
+
+body {
+ background-color: slategray;
+}
+
+.turn.marker.T1 { top: 64px; left: 264px; }
+.turn.marker.T2 { top: 64px; left: 416px; }
+.turn.marker.T3 { top: 68px; left: 596px; }
+.turn.marker.T4 { top: 138px; left: 416px; }
+.turn.marker.T5 { top: 138px; left: 264px; }
+
+.piece {
+ position: absolute;
+ transition-property: top, left;
+ transition-duration: 700ms;
+ transition-timing-function: ease;
+ background-repeat: no-repeat;
+ //background-position: center;
+}
+
+TWOD.piece.cylinder {
+ width: 40px;
+ height: 40px;
+ background-size: 40px 40px;
+ border-radius: 50%;
+ border: 1px solid black;
+}
+
+.piece.cylinder {
+ width: 42px;
+ height: 47px;
+}
+
+.piece.cube {
+ width: 31px;
+ height: 39px;
+}
+
+.piece.action {
+ filter:
+ drop-shadow(2px 0px 0px white)
+ drop-shadow(0px 2px 0px white)
+ drop-shadow(0px -2px 0px white)
+ drop-shadow(-2px 0px 0px white);
+}
+
+.piece.selected {
+ filter:
+ drop-shadow(2px 0px 0px orange)
+ drop-shadow(0px 2px 0px orange)
+ drop-shadow(0px -2px 0px orange)
+ drop-shadow(-2px 0px 0px orange);
+}
+
+.number {
+ width: 14px;
+ height: 14px;
+ background-size: 800% 100%;
+ pointer-events: none;
+}
+
+.number { background-image: url(images/numbers.png); }
+.number.prussia { background-image: url(images/numbers_rev.png); }
+.number.russia { background-image: url(images/numbers_rev.png); }
+
+.number.france { background-color: #ff3534; border-radius: 50%; }
+
+.number.n0 { visibility: hidden; }
+.number.n1 { background-position: 0px 0px; }
+.number.n2 { background-position: -14px 0px; }
+.number.n3 { background-position: -28px 0px; }
+.number.n4 { background-position: -42px 0px; }
+.number.n5 { background-position: -56px 0px; }
+.number.n6 { background-position: -70px 0px; }
+.number.n7 { background-position: -84px 0px; }
+.number.n8 { background-position: -98px 0px; }
+
+/*
+.number.prussia { background-color: var(--color-prussia); }
+.number.hanover { background-color: var(--color-hanover); }
+.number.russia { background-color: var(--color-russia); }
+.number.sweden { background-color: var(--color-sweden); }
+.number.austria { background-color: var(--color-austria); }
+.number.imperial { background-color: var(--color-imperial); }
+.number.france { background-color: var(--color-france); }
+*/
+
+#hand_prussia_header { background-color: var(--color-dark-prussia); color: white; }
+#hand_hanover_header { background-color: var(--color-dark-hanover); color: black; }
+#hand_russia_header { background-color: var(--color-dark-russia); color: white; }
+#hand_sweden_header { background-color: var(--color-dark-sweden); color: black; }
+#hand_austria_header { background-color: var(--color-dark-austria); color: black; }
+#hand_imperial_header { background-color: var(--color-dark-imperial); color: black; }
+#hand_france_header { background-color: var(--color-dark-france); color: black; }
+
+.marker {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ background-size: 30px 30px;
+ background-position: center;
+ border: 1px solid black;
+}
+
+.panel {
+ background-color: #444;
+ width: clamp(824px, calc(100% - 30px), 1636px);
+ margin: 12px auto 36px auto;
+ box-shadow: 1px 2px 6px #0004;
+ border: 1px solid #0008;
+}
+
+.panel_header {
+ color: white;
+ font-weight: bold;
+ text-align: center;
+ padding: 3px 1em;
+ border-bottom: 1px solid #0008;
+}
+
+.panel_body {
+ background-color: #555;
+ display: flex;
+ justify-content: start;
+ flex-wrap: wrap;
+ padding: 18px;
+ gap: 18px;
+ min-height: 350px;
+}
+
+.space {
+ position: absolute;
+ box-sizing: border-box;
+ border: 4px solid transparent;
+}
+
+.space.city {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+}
+
+.space.objective {
+ width: 40px;
+ height: 40px;
+}
+
+.space.depot {
+ width: 52px;
+ height: 52px;
+ border-radius: 50%;
+}
+
+.space.action {
+ border-color: white;
+ z-index: 2000;
+}
+
+.space.city.action {
+ background-color: #fff8;
+}
+
+.space.depot.action {
+ background-color: #fff4;
+}
+
+/*
+ TODO: only show colors for enemy powers?
+*/
+.space.objective.action.prussia { background-color: #00547880; }
+.space.objective.action.hanover { background-color: #91c9ed80; }
+.space.objective.action.russia { background-color: #147d3680; }
+.space.objective.action.sweden { background-color: #b2d23380; }
+.space.objective.action.austria { background-color: #f5f5f580; }
+.space.objective.action.imperial { background-color: #fbe30080; }
+.space.objective.action.france { background-color: #ed1c2380; }
+
+.major_road {
+ position: absolute;
+ box-sizing: border-box;
+ background-color: black;
+ width: 20px;
+ height: 6px;
+}
+
+.road {
+ position: absolute;
+ box-sizing: border-box;
+ background-color: dimgray;
+ width: 20px;
+ height: 4px;
+}
+
+.marker.conquest.austria { background-image: url(images/conquest_austria.2x.png) }
+.marker.conquest.france { background-image: url(images/conquest_france.2x.png) }
+.marker.conquest.imperial { background-image: url(images/conquest_imperial.2x.png) }
+.marker.conquest.prussia { background-image: url(images/conquest_prussia.2x.png) }
+.marker.conquest.russia { background-image: url(images/conquest_russia.2x.png) }
+.marker.conquest.sweden { background-image: url(images/conquest_sweden.2x.png) }
+.marker.retroactive.austria { background-image: url(images/retroactive_austria.2x.png) }
+.marker.retroactive.france { background-image: url(images/retroactive_france.2x.png) }
+.marker.retroactive.imperial { background-image: url(images/retroactive_imperial.2x.png) }
+.marker.retroactive.prussia { background-image: url(images/retroactive_prussia.2x.png) }
+.marker.retroactive.russia { background-image: url(images/retroactive_russia.2x.png) }
+.marker.retroactive.sweden { background-image: url(images/retroactive_sweden.2x.png) }
+.marker.turn { background-image: url(images/turn.2x.png) }
+
+.piece.cube.austria { background-image: url(images/cube_austria.svg) }
+.piece.cube.france { background-image: url(images/cube_france.svg) }
+.piece.cube.hanover { background-image: url(images/cube_hanover.svg) }
+.piece.cube.imperial { background-image: url(images/cube_imperial.svg) }
+.piece.cube.prussia { background-image: url(images/cube_prussia.svg) }
+.piece.cube.russia { background-image: url(images/cube_russia.svg) }
+.piece.cube.sweden { background-image: url(images/cube_sweden.svg) }
+
+/* 3D
+*/
+.piece.cylinder.austria_1 { background-image: url(images/cylinder_austria_1.svg) }
+.piece.cylinder.austria_2 { background-image: url(images/cylinder_austria_2.svg) }
+.piece.cylinder.austria_3 { background-image: url(images/cylinder_austria_3.svg) }
+.piece.cylinder.austria_4 { background-image: url(images/cylinder_austria_4.svg) }
+.piece.cylinder.austria_5 { background-image: url(images/cylinder_austria_5.svg) }
+.piece.cylinder.france_1 { background-image: url(images/cylinder_france_1.svg) }
+.piece.cylinder.france_2 { background-image: url(images/cylinder_france_2.svg) }
+.piece.cylinder.france_3 { background-image: url(images/cylinder_france_3.svg) }
+.piece.cylinder.hanover_1 { background-image: url(images/cylinder_hanover_1.svg) }
+.piece.cylinder.hanover_2 { background-image: url(images/cylinder_hanover_2.svg) }
+.piece.cylinder.imperial_1 { background-image: url(images/cylinder_imperial_1.svg) }
+.piece.cylinder.prussia_1 { background-image: url(images/cylinder_prussia_1.svg) }
+.piece.cylinder.prussia_2 { background-image: url(images/cylinder_prussia_2.svg) }
+.piece.cylinder.prussia_3 { background-image: url(images/cylinder_prussia_3.svg) }
+.piece.cylinder.prussia_4 { background-image: url(images/cylinder_prussia_4.svg) }
+.piece.cylinder.prussia_5 { background-image: url(images/cylinder_prussia_5.svg) }
+.piece.cylinder.prussia_6 { background-image: url(images/cylinder_prussia_6.svg) }
+.piece.cylinder.prussia_7 { background-image: url(images/cylinder_prussia_7.svg) }
+.piece.cylinder.prussia_8 { background-image: url(images/cylinder_prussia_8.svg) }
+.piece.cylinder.russia_1 { background-image: url(images/cylinder_russia_1.svg) }
+.piece.cylinder.russia_2 { background-image: url(images/cylinder_russia_2.svg) }
+.piece.cylinder.russia_3 { background-image: url(images/cylinder_russia_3.svg) }
+.piece.cylinder.russia_4 { background-image: url(images/cylinder_russia_4.svg) }
+.piece.cylinder.sweden_1 { background-image: url(images/cylinder_sweden_1.svg) }
+
+.piece.cylinder.austria.oos { background-image: url(images/cylinder_austria_oos.svg) }
+.piece.cylinder.france.oos { background-image: url(images/cylinder_france_oos.svg) }
+.piece.cylinder.hanover.oos { background-image: url(images/cylinder_hanover_oos.svg) }
+.piece.cylinder.imperial.oos { background-image: url(images/cylinder_imperial_oos.svg) }
+.piece.cylinder.prussia.oos { background-image: url(images/cylinder_prussia_oos.svg) }
+.piece.cylinder.russia.oos { background-image: url(images/cylinder_russia_oos.svg) }
+.piece.cylinder.sweden.oos { background-image: url(images/cylinder_sweden_oos.svg) }
+
+/* 2D
+.piece.cylinder.austria_1 { background-image: url(images/A1.png) }
+.piece.cylinder.austria_2 { background-image: url(images/A2.png) }
+.piece.cylinder.austria_3 { background-image: url(images/A3.png) }
+.piece.cylinder.austria_4 { background-image: url(images/A4.png) }
+.piece.cylinder.austria_5 { background-image: url(images/A5.png) }
+.piece.cylinder.france_1 { background-image: url(images/F1.png) }
+.piece.cylinder.france_2 { background-image: url(images/F2.png) }
+.piece.cylinder.france_3 { background-image: url(images/F3.png) }
+.piece.cylinder.hanover_1 { background-image: url(images/H1.png) }
+.piece.cylinder.hanover_2 { background-image: url(images/H2.png) }
+.piece.cylinder.imperial_1 { background-image: url(images/I1.png) }
+.piece.cylinder.prussia_1 { background-image: url(images/P1.png) }
+.piece.cylinder.prussia_2 { background-image: url(images/P2.png) }
+.piece.cylinder.prussia_3 { background-image: url(images/P3.png) }
+.piece.cylinder.prussia_4 { background-image: url(images/P4.png) }
+.piece.cylinder.prussia_5 { background-image: url(images/P5.png) }
+.piece.cylinder.prussia_6 { background-image: url(images/P6.png) }
+.piece.cylinder.prussia_7 { background-image: url(images/P7.png) }
+.piece.cylinder.prussia_8 { background-image: url(images/P8.png) }
+.piece.cylinder.russia_1 { background-image: url(images/R1.png) }
+.piece.cylinder.russia_2 { background-image: url(images/R2.png) }
+.piece.cylinder.russia_3 { background-image: url(images/R3.png) }
+.piece.cylinder.russia_4 { background-image: url(images/R4.png) }
+.piece.cylinder.sweden_1 { background-image: url(images/S1.png) }
+*/
+
+.card {
+ width: 220px;
+ height: 342px;
+ background-size: 220px 342px;
+ background-color: #f3ebd5;
+ border-radius: 8px;
+ border: 1px solid black;
+}
+
+.card.action {
+ box-shadow: 0 0 0 4px white;
+}
+
+/*
+.card.tc.reverse + .card.tc.reverse { margin-left: -200px; }
+*/
+
+.card.tc { margin-right: -180px; }
+.panel_body { padding-right: calc(18px + 180px); }
+
+#clock_of_fate .card.tc { margin-right: 0px; }
+
+.card.tc.S13 { background-image: url(cards/Friedrich.TC.01.075.jpg) }
+.card.tc.S12 { background-image: url(cards/Friedrich.TC.02.075.jpg) }
+.card.tc.S11 { background-image: url(cards/Friedrich.TC.03.075.jpg) }
+.card.tc.S10 { background-image: url(cards/Friedrich.TC.04.075.jpg) }
+.card.tc.S9 { background-image: url(cards/Friedrich.TC.05.075.jpg) }
+.card.tc.S8 { background-image: url(cards/Friedrich.TC.06.075.jpg) }
+.card.tc.S7 { background-image: url(cards/Friedrich.TC.07.075.jpg) }
+.card.tc.S6 { background-image: url(cards/Friedrich.TC.08.075.jpg) }
+.card.tc.S5 { background-image: url(cards/Friedrich.TC.09.075.jpg) }
+.card.tc.S4 { background-image: url(cards/Friedrich.TC.10.075.jpg) }
+.card.tc.S3 { background-image: url(cards/Friedrich.TC.11.075.jpg) }
+.card.tc.S2 { background-image: url(cards/Friedrich.TC.12.075.jpg) }
+.card.tc.C13 { background-image: url(cards/Friedrich.TC.13.075.jpg) }
+.card.tc.C12 { background-image: url(cards/Friedrich.TC.14.075.jpg) }
+.card.tc.C11 { background-image: url(cards/Friedrich.TC.15.075.jpg) }
+.card.tc.C10 { background-image: url(cards/Friedrich.TC.16.075.jpg) }
+.card.tc.C9 { background-image: url(cards/Friedrich.TC.17.075.jpg) }
+.card.tc.C8 { background-image: url(cards/Friedrich.TC.18.075.jpg) }
+.card.tc.C7 { background-image: url(cards/Friedrich.TC.19.075.jpg) }
+.card.tc.C6 { background-image: url(cards/Friedrich.TC.20.075.jpg) }
+.card.tc.C5 { background-image: url(cards/Friedrich.TC.21.075.jpg) }
+.card.tc.C4 { background-image: url(cards/Friedrich.TC.22.075.jpg) }
+.card.tc.C3 { background-image: url(cards/Friedrich.TC.23.075.jpg) }
+.card.tc.C2 { background-image: url(cards/Friedrich.TC.24.075.jpg) }
+.card.tc.H13 { background-image: url(cards/Friedrich.TC.25.075.jpg) }
+.card.tc.H12 { background-image: url(cards/Friedrich.TC.26.075.jpg) }
+.card.tc.H11 { background-image: url(cards/Friedrich.TC.27.075.jpg) }
+.card.tc.H10 { background-image: url(cards/Friedrich.TC.28.075.jpg) }
+.card.tc.H9 { background-image: url(cards/Friedrich.TC.29.075.jpg) }
+.card.tc.H8 { background-image: url(cards/Friedrich.TC.30.075.jpg) }
+.card.tc.H7 { background-image: url(cards/Friedrich.TC.31.075.jpg) }
+.card.tc.H6 { background-image: url(cards/Friedrich.TC.32.075.jpg) }
+.card.tc.H5 { background-image: url(cards/Friedrich.TC.33.075.jpg) }
+.card.tc.H4 { background-image: url(cards/Friedrich.TC.34.075.jpg) }
+.card.tc.H3 { background-image: url(cards/Friedrich.TC.35.075.jpg) }
+.card.tc.H2 { background-image: url(cards/Friedrich.TC.36.075.jpg) }
+.card.tc.D13 { background-image: url(cards/Friedrich.TC.37.075.jpg) }
+.card.tc.D12 { background-image: url(cards/Friedrich.TC.38.075.jpg) }
+.card.tc.D11 { background-image: url(cards/Friedrich.TC.39.075.jpg) }
+.card.tc.D10 { background-image: url(cards/Friedrich.TC.40.075.jpg) }
+.card.tc.D9 { background-image: url(cards/Friedrich.TC.41.075.jpg) }
+.card.tc.D8 { background-image: url(cards/Friedrich.TC.42.075.jpg) }
+.card.tc.D7 { background-image: url(cards/Friedrich.TC.43.075.jpg) }
+.card.tc.D6 { background-image: url(cards/Friedrich.TC.44.075.jpg) }
+.card.tc.D5 { background-image: url(cards/Friedrich.TC.45.075.jpg) }
+.card.tc.D4 { background-image: url(cards/Friedrich.TC.46.075.jpg) }
+.card.tc.D3 { background-image: url(cards/Friedrich.TC.47.075.jpg) }
+.card.tc.D2 { background-image: url(cards/Friedrich.TC.48.075.jpg) }
+.card.tc.R { background-image: url(cards/Friedrich.TC.49.075.jpg) }
+.card.tc.reverse.deck_1, body.shift .card.tc.deck_1 { background-image: url(cards/Friedrich.TC.reverse.deck_1.075.jpg) }
+.card.tc.reverse.deck_2, body.shift .card.tc.deck_2 { background-image: url(cards/Friedrich.TC.reverse.deck_2.075.jpg) }
+.card.tc.reverse.deck_3, body.shift .card.tc.deck_3 { background-image: url(cards/Friedrich.TC.reverse.deck_3.075.jpg) }
+.card.tc.reverse.deck_4, body.shift .card.tc.deck_4 { background-image: url(cards/Friedrich.TC.reverse.deck_4.075.jpg) }
+.card.tc.reverse.deck_5, body.shift .card.tc.deck_5 { background-image: url(cards/Friedrich.TC.reverse.deck_5.075.jpg) }
+
+.card.fate.c1 { background-image: url(cards/Friedrich.Fatecard.01.075.jpg) }
+.card.fate.c2 { background-image: url(cards/Friedrich.Fatecard.02.075.jpg) }
+.card.fate.c3 { background-image: url(cards/Friedrich.Fatecard.03.075.jpg) }
+.card.fate.c4 { background-image: url(cards/Friedrich.Fatecard.04.075.jpg) }
+.card.fate.c5 { background-image: url(cards/Friedrich.Fatecard.05.075.jpg) }
+.card.fate.c6 { background-image: url(cards/Friedrich.Fatecard.06.075.jpg) }
+.card.fate.c7 { background-image: url(cards/Friedrich.Fatecard.10.075.jpg) }
+.card.fate.c8 { background-image: url(cards/Friedrich.Fatecard.11.075.jpg) }
+.card.fate.c9 { background-image: url(cards/Friedrich.Fatecard.12.075.jpg) }
+.card.fate.c10 { background-image: url(cards/Friedrich.Fatecard.13.075.jpg) }
+.card.fate.c11 { background-image: url(cards/Friedrich.Fatecard.14.075.jpg) }
+.card.fate.c12 { background-image: url(cards/Friedrich.Fatecard.15.075.jpg) }
+.card.fate.c13 { background-image: url(cards/Friedrich.Fatecard.07.075.jpg) }
+.card.fate.c14 { background-image: url(cards/Friedrich.Fatecard.08.075.jpg) }
+.card.fate.c15 { background-image: url(cards/Friedrich.Fatecard.09.075.jpg) }
+.card.fate.c16 { background-image: url(cards/Friedrich.Fatecard.16.075.jpg) }
+.card.fate.c17 { background-image: url(cards/Friedrich.Fatecard.17.075.jpg) }
+.card.fate.c18 { background-image: url(cards/Friedrich.Fatecard.18.075.jpg) }
+.card.fate.reverse { background-image: url(cards/Friedrich.Fatecard.reverse.075.jpg) }
+
+@media (min-resolution: 97dpi) {
+ #map { background-image: url(Fried.Gameboard.150.jpg);
+
+ .card.tc.S13 { background-image: url(cards/Friedrich.TC.01.150.jpg) }
+ .card.tc.S12 { background-image: url(cards/Friedrich.TC.02.150.jpg) }
+ .card.tc.S11 { background-image: url(cards/Friedrich.TC.03.150.jpg) }
+ .card.tc.S10 { background-image: url(cards/Friedrich.TC.04.150.jpg) }
+ .card.tc.S9 { background-image: url(cards/Friedrich.TC.05.150.jpg) }
+ .card.tc.S8 { background-image: url(cards/Friedrich.TC.06.150.jpg) }
+ .card.tc.S7 { background-image: url(cards/Friedrich.TC.07.150.jpg) }
+ .card.tc.S6 { background-image: url(cards/Friedrich.TC.08.150.jpg) }
+ .card.tc.S5 { background-image: url(cards/Friedrich.TC.09.150.jpg) }
+ .card.tc.S4 { background-image: url(cards/Friedrich.TC.10.150.jpg) }
+ .card.tc.S3 { background-image: url(cards/Friedrich.TC.11.150.jpg) }
+ .card.tc.S2 { background-image: url(cards/Friedrich.TC.12.150.jpg) }
+ .card.tc.C13 { background-image: url(cards/Friedrich.TC.13.150.jpg) }
+ .card.tc.C12 { background-image: url(cards/Friedrich.TC.14.150.jpg) }
+ .card.tc.C11 { background-image: url(cards/Friedrich.TC.15.150.jpg) }
+ .card.tc.C10 { background-image: url(cards/Friedrich.TC.16.150.jpg) }
+ .card.tc.C9 { background-image: url(cards/Friedrich.TC.17.150.jpg) }
+ .card.tc.C8 { background-image: url(cards/Friedrich.TC.18.150.jpg) }
+ .card.tc.C7 { background-image: url(cards/Friedrich.TC.19.150.jpg) }
+ .card.tc.C6 { background-image: url(cards/Friedrich.TC.20.150.jpg) }
+ .card.tc.C5 { background-image: url(cards/Friedrich.TC.21.150.jpg) }
+ .card.tc.C4 { background-image: url(cards/Friedrich.TC.22.150.jpg) }
+ .card.tc.C3 { background-image: url(cards/Friedrich.TC.23.150.jpg) }
+ .card.tc.C2 { background-image: url(cards/Friedrich.TC.24.150.jpg) }
+ .card.tc.H13 { background-image: url(cards/Friedrich.TC.25.150.jpg) }
+ .card.tc.H12 { background-image: url(cards/Friedrich.TC.26.150.jpg) }
+ .card.tc.H11 { background-image: url(cards/Friedrich.TC.27.150.jpg) }
+ .card.tc.H10 { background-image: url(cards/Friedrich.TC.28.150.jpg) }
+ .card.tc.H9 { background-image: url(cards/Friedrich.TC.29.150.jpg) }
+ .card.tc.H8 { background-image: url(cards/Friedrich.TC.30.150.jpg) }
+ .card.tc.H7 { background-image: url(cards/Friedrich.TC.31.150.jpg) }
+ .card.tc.H6 { background-image: url(cards/Friedrich.TC.32.150.jpg) }
+ .card.tc.H5 { background-image: url(cards/Friedrich.TC.33.150.jpg) }
+ .card.tc.H4 { background-image: url(cards/Friedrich.TC.34.150.jpg) }
+ .card.tc.H3 { background-image: url(cards/Friedrich.TC.35.150.jpg) }
+ .card.tc.H2 { background-image: url(cards/Friedrich.TC.36.150.jpg) }
+ .card.tc.D13 { background-image: url(cards/Friedrich.TC.37.150.jpg) }
+ .card.tc.D12 { background-image: url(cards/Friedrich.TC.38.150.jpg) }
+ .card.tc.D11 { background-image: url(cards/Friedrich.TC.39.150.jpg) }
+ .card.tc.D10 { background-image: url(cards/Friedrich.TC.40.150.jpg) }
+ .card.tc.D9 { background-image: url(cards/Friedrich.TC.41.150.jpg) }
+ .card.tc.D8 { background-image: url(cards/Friedrich.TC.42.150.jpg) }
+ .card.tc.D7 { background-image: url(cards/Friedrich.TC.43.150.jpg) }
+ .card.tc.D6 { background-image: url(cards/Friedrich.TC.44.150.jpg) }
+ .card.tc.D5 { background-image: url(cards/Friedrich.TC.45.150.jpg) }
+ .card.tc.D4 { background-image: url(cards/Friedrich.TC.46.150.jpg) }
+ .card.tc.D3 { background-image: url(cards/Friedrich.TC.47.150.jpg) }
+ .card.tc.D2 { background-image: url(cards/Friedrich.TC.48.150.jpg) }
+ .card.tc.R { background-image: url(cards/Friedrich.TC.49.150.jpg) }
+ .card.tc.reverse.deck_1, body.shift .card.tc.deck_1 { background-image: url(cards/Friedrich.TC.reverse.deck_1.150.jpg) }
+ .card.tc.reverse.deck_2, body.shift .card.tc.deck_2 { background-image: url(cards/Friedrich.TC.reverse.deck_2.150.jpg) }
+ .card.tc.reverse.deck_3, body.shift .card.tc.deck_3 { background-image: url(cards/Friedrich.TC.reverse.deck_3.150.jpg) }
+ .card.tc.reverse.deck_4, body.shift .card.tc.deck_4 { background-image: url(cards/Friedrich.TC.reverse.deck_4.150.jpg) }
+ .card.tc.reverse.deck_5, body.shift .card.tc.deck_5 { background-image: url(cards/Friedrich.TC.reverse.deck_5.150.jpg) }
+
+ .card.fate.c1 { background-image: url(cards/Friedrich.Fatecard.01.150.jpg) }
+ .card.fate.c2 { background-image: url(cards/Friedrich.Fatecard.02.150.jpg) }
+ .card.fate.c3 { background-image: url(cards/Friedrich.Fatecard.03.150.jpg) }
+ .card.fate.c4 { background-image: url(cards/Friedrich.Fatecard.04.150.jpg) }
+ .card.fate.c5 { background-image: url(cards/Friedrich.Fatecard.05.150.jpg) }
+ .card.fate.c6 { background-image: url(cards/Friedrich.Fatecard.06.150.jpg) }
+ .card.fate.c7 { background-image: url(cards/Friedrich.Fatecard.10.150.jpg) }
+ .card.fate.c8 { background-image: url(cards/Friedrich.Fatecard.11.150.jpg) }
+ .card.fate.c9 { background-image: url(cards/Friedrich.Fatecard.12.150.jpg) }
+ .card.fate.c10 { background-image: url(cards/Friedrich.Fatecard.13.150.jpg) }
+ .card.fate.c11 { background-image: url(cards/Friedrich.Fatecard.14.150.jpg) }
+ .card.fate.c12 { background-image: url(cards/Friedrich.Fatecard.15.150.jpg) }
+ .card.fate.c13 { background-image: url(cards/Friedrich.Fatecard.07.150.jpg) }
+ .card.fate.c14 { background-image: url(cards/Friedrich.Fatecard.08.150.jpg) }
+ .card.fate.c15 { background-image: url(cards/Friedrich.Fatecard.09.150.jpg) }
+ .card.fate.c16 { background-image: url(cards/Friedrich.Fatecard.16.150.jpg) }
+ .card.fate.c17 { background-image: url(cards/Friedrich.Fatecard.17.150.jpg) }
+ .card.fate.c18 { background-image: url(cards/Friedrich.Fatecard.18.150.jpg) }
+ .card.fate.reverse { background-image: url(cards/Friedrich.Fatecard.reverse.150.jpg) }
+
+}
diff --git a/play.html b/play.html
index 7516ef9..a8ae42c 100644
--- a/play.html
+++ b/play.html
@@ -22,7 +22,7 @@
<summary><img src="/images/cog.svg"></summary>
<menu>
<li><a target="_blank" href="/friedrich/info/FriedrichRules.pdf">Rules</a>
- <li><a target="_blank" href="/friedrich/info/cards.html">Cards</a>
+ <li><a target="_blank" href="/friedrich/info/cards.html">Cards of Fate</a>
</menu>
</details>
<button onclick="toggle_pieces()"><img src="/images/earth-africa-europe.svg"></button>
diff --git a/play.js b/play.js
new file mode 100644
index 0000000..fe7c12a
--- /dev/null
+++ b/play.js
@@ -0,0 +1,709 @@
+"use strict"
+
+function toggle_pieces() {
+ document.getElementById("pieces").classList.toggle("hide")
+}
+
+/* DATA */
+
+const P_PRUSSIA = 0
+const P_HANOVER = 1
+const P_RUSSIA = 2
+const P_SWEDEN = 3
+const P_AUSTRIA = 4
+const P_IMPERIAL = 5
+const P_FRANCE = 6
+
+const cities = data.cities
+const last_city = cities.name.length - 1
+
+const ELIMINATED = data.cities.name.length
+const REMOVED = ELIMINATED + 1
+const ELIMINATED_TRAIN_X = 1065
+const ELIMINATED_TRAIN_Y = 200
+const ELIMINATED_GENERAL_X = 1040
+const ELIMINATED_GENERAL_Y = 150
+const ELIMINATED_GENERAL_DX = 50
+
+const all_objectives = []
+set_add_all(all_objectives, data.type.objective1_austria)
+set_add_all(all_objectives, data.type.objective2_austria)
+set_add_all(all_objectives, data.type.objective1_imperial)
+set_add_all(all_objectives, data.type.objective2_imperial)
+set_add_all(all_objectives, data.type.objective1_sweden)
+set_add_all(all_objectives, data.type.objective2_sweden)
+set_add_all(all_objectives, data.type.objective_france)
+set_add_all(all_objectives, data.type.objective_prussia)
+set_add_all(all_objectives, data.type.objective_russia)
+
+const objective1 = [ [], [], [], [], [], [], [] ]
+const objective2 = [ [], [], [], [], [], [], [] ]
+const protect = [ [], [], [], [], [], [], [] ]
+
+for (let s of data.type.objective_prussia) set_add(objective1[P_PRUSSIA], s)
+for (let s of data.type.objective_russia) set_add(objective1[P_RUSSIA], s)
+for (let s of data.type.objective1_sweden) set_add(objective1[P_SWEDEN], s)
+for (let s of data.type.objective2_sweden) set_add(objective2[P_SWEDEN], s)
+for (let s of data.type.objective1_austria) set_add(objective1[P_AUSTRIA], s)
+for (let s of data.type.objective2_austria) set_add(objective2[P_AUSTRIA], s)
+for (let s of data.type.objective1_imperial) set_add(objective1[P_IMPERIAL], s)
+for (let s of data.type.objective2_imperial) set_add(objective2[P_IMPERIAL], s)
+for (let s of data.type.objective_france) set_add(objective1[P_FRANCE], s)
+
+const power_class = [ "prussia", "hanover", "russia", "sweden", "austria", "imperial", "france" ]
+const power_name = [ "Prussia", "Hanover", "Russia", "Sweden", "Austria", "Imperial Army", "France" ]
+
+const cards_of_fate_name = [
+ "No Fate",
+ "Card of Fate 1",
+ "Card of Fate 2",
+ "Card of Fate 3",
+ "Card of Fate 4",
+ "Card of Fate 5",
+ "Card of Fate 6",
+ "Card of Fate 7",
+ "Card of Fate 8",
+ "Card of Fate 9",
+ "Card of Fate 10",
+ "Card of Fate 11",
+ "Card of Fate 12",
+ "Poems",
+ "Lord Bute",
+ "Elisabeth",
+ "Sweden",
+ "India",
+ "America",
+]
+
+const GENERAL_POWER = [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 4, 4, 4, 4, 4, 5, 6, 6, 6 ]
+const TRAIN_POWER = [ 0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6 ]
+
+const all_powers = [ 0, 1, 2, 3, 4, 5, 6 ]
+
+const all_power_generals = [
+ /* P */ [ 0, 1, 2, 3, 4, 5, 6, 7 ],
+ /* H */ [ 8, 9 ],
+ /* R */ [ 10, 11, 12, 13 ],
+ /* S */ [ 14 ],
+ /* A */ [ 15, 16, 17, 18, 19 ],
+ /* I */ [ 20 ],
+ /* F */ [ 21, 22, 23 ],
+]
+
+const all_power_trains = [
+ /* P */ [ 24, 25 ],
+ /* H */ [ 26 ],
+ /* R */ [ 27, 28 ],
+ /* S */ [ 29 ],
+ /* A */ [ 30, 31 ],
+ /* I */ [ 32 ],
+ /* F */ [ 33, 34 ],
+]
+
+const RESERVE = 4
+let suit_class = [ "S", "C", "H", "D", "R" ]
+let suit_name = [ "\u2660", "\u2663", "\u2665", "\u2666", "R" ]
+
+function to_suit(c) {
+ return (c >> 4) & 7
+}
+
+function to_value(c) {
+ return c & 15
+}
+
+/* BUILD UI */
+
+const ui = {
+ header: document.querySelector("header"),
+ spaces_element: document.getElementById("spaces"),
+ pieces_element: document.getElementById("pieces"),
+ markers_element: document.getElementById("markers"),
+ cities: [],
+ action_register: [],
+}
+
+function register_action(target, action, id) {
+ target.my_id = id
+ target.my_action = action
+ target.onmousedown = (evt) => on_click_action(evt, target)
+ ui.action_register.push(target)
+}
+
+function on_click_action(evt, target) {
+ if (evt.button === 0)
+ if (send_action(target.my_action, target.my_id))
+ evt.stopPropagation()
+}
+
+function process_actions() {
+ for (let target of ui.action_register)
+ target.classList.toggle("action", is_action(target.my_action, target.my_id))
+}
+
+function is_action(action, arg) {
+ if (arg === undefined)
+ return !!(view.actions && view.actions[action] === 1)
+ return !!(view.actions && view.actions[action] && set_has(view.actions[action], arg))
+}
+
+function make_road(type, x, y, dx, dy) {
+ let e = document.createElement("div")
+ e.className = type
+ e.style.left = x + "px"
+ e.style.top = y + "px"
+ let a = Math.atan2(dy, dx)
+ let s = (Math.hypot(dx, dy) - 15) / 20
+ e.style.transform =
+ "rotate(" + a + "rad)" +
+ "scale(" + s + ", 1)"
+ // TODO: rotate to align
+ ui.spaces_element.appendChild(e)
+}
+
+function create_piece(action, id, style) {
+ let e = document.createElement("div")
+ e.className = style
+ register_action(e, action, id)
+ return e
+}
+
+function create_marker(style) {
+ let e = document.createElement("div")
+ e.className = style
+ return e
+}
+
+function make_tc_id(n, suit, value) {
+ return (n << 7) | (suit << 4) | value
+}
+
+function make_tc_deck(n) {
+ for (let suit = 0; suit <= 3; ++suit) {
+ for (let value = 2; value <= 13; ++value) {
+ let c = (n << 7) | (suit << 4) | value
+ ui.tc[c] = create_piece("card", c, "card tc " + suit_class[suit] + value)
+ }
+ }
+ for (let value = 2; value <= 3; ++value) {
+ let c = (n << 7) | (4 << 4) | value
+ ui.tc[c] = create_piece("card", c, "card tc R")
+ }
+}
+
+function make_tc_deck_back(n) {
+ let list = []
+ for (let i = 0; i < 50; ++i) {
+ let e = document.createElement("div")
+ e.className = "card tc reverse " + n
+ list.push(e)
+ }
+ return list
+}
+
+function make_fate_card(fc) {
+ let e = document.createElement("div")
+ if (fc === 0)
+ e.className = "card fate reverse"
+ else
+ e.className = "card fate c" + fc
+ return e
+}
+
+function on_init() {
+ ui.pieces = [
+ create_piece("piece", 0, "piece cylinder prussia prussia_1"),
+ create_piece("piece", 1, "piece cylinder prussia prussia_2"),
+ create_piece("piece", 2, "piece cylinder prussia prussia_3"),
+ create_piece("piece", 3, "piece cylinder prussia prussia_4"),
+ create_piece("piece", 4, "piece cylinder prussia prussia_5"),
+ create_piece("piece", 5, "piece cylinder prussia prussia_6"),
+ create_piece("piece", 6, "piece cylinder prussia prussia_7"),
+ create_piece("piece", 7, "piece cylinder prussia prussia_8"),
+ create_piece("piece", 8, "piece cylinder hanover hanover_1"),
+ create_piece("piece", 9, "piece cylinder hanover hanover_2"),
+ create_piece("piece", 10, "piece cylinder russia russia_1"),
+ create_piece("piece", 11, "piece cylinder russia russia_2"),
+ create_piece("piece", 12, "piece cylinder russia russia_3"),
+ create_piece("piece", 13, "piece cylinder russia russia_4"),
+ create_piece("piece", 14, "piece cylinder sweden sweden_1"),
+ create_piece("piece", 15, "piece cylinder austria austria_1"),
+ create_piece("piece", 16, "piece cylinder austria austria_2"),
+ create_piece("piece", 17, "piece cylinder austria austria_3"),
+ create_piece("piece", 18, "piece cylinder austria austria_4"),
+ create_piece("piece", 19, "piece cylinder austria austria_5"),
+ create_piece("piece", 20, "piece cylinder imperial imperial_1"),
+ create_piece("piece", 21, "piece cylinder france france_1"),
+ create_piece("piece", 22, "piece cylinder france france_2"),
+ create_piece("piece", 23, "piece cylinder france france_3"),
+ create_piece("piece", 24, "piece cube prussia"),
+ create_piece("piece", 25, "piece cube prussia"),
+ create_piece("piece", 26, "piece cube hanover"),
+ create_piece("piece", 27, "piece cube russia"),
+ create_piece("piece", 28, "piece cube russia"),
+ create_piece("piece", 29, "piece cube sweden"),
+ create_piece("piece", 30, "piece cube austria"),
+ create_piece("piece", 31, "piece cube austria"),
+ create_piece("piece", 32, "piece cube imperial"),
+ create_piece("piece", 33, "piece cube france"),
+ create_piece("piece", 34, "piece cube france"),
+ ]
+
+ for (let e of ui.pieces)
+ ui.pieces_element.appendChild(e)
+
+ ui.troops = []
+ for (let i = 0; i < 24; ++i)
+ ui.troops[i] = create_marker("hide")
+ for (let e of ui.troops)
+ ui.pieces_element.appendChild(e)
+
+ ui.conquest = []
+ ui.retro = []
+ for (let s of all_objectives) {
+ for (let pow = 0; pow < 7; ++pow) {
+ if (set_has(objective1[pow], s) || set_has(objective2[pow], s)) {
+ ui.conquest[s] = create_conquest("marker conquest " + power_class[pow], s)
+ ui.retro[s] = create_conquest("marker retroactive " + power_class[pow], s)
+ }
+ }
+ }
+
+ ui.turns = [
+ create_marker("marker turn T1"),
+ create_marker("marker turn T2"),
+ create_marker("marker turn T3"),
+ create_marker("marker turn T4"),
+ create_marker("marker turn T5"),
+ ]
+
+ for (let e of ui.turns)
+ ui.pieces_element.appendChild(e)
+
+ ui.hand = [
+ document.getElementById("hand_prussia"),
+ document.getElementById("hand_hanover"),
+ document.getElementById("hand_russia"),
+ document.getElementById("hand_sweden"),
+ document.getElementById("hand_austria"),
+ document.getElementById("hand_imperial"),
+ document.getElementById("hand_france"),
+ ]
+
+ ui.tc = []
+ make_tc_deck(0)
+ make_tc_deck(1)
+ make_tc_deck(2)
+ make_tc_deck(3)
+
+ ui.tc_back = [
+ make_tc_deck_back("deck_1"),
+ make_tc_deck_back("deck_2"),
+ make_tc_deck_back("deck_3"),
+ make_tc_deck_back("deck_4"),
+ ]
+
+ ui.clock_of_fate = document.getElementById("clock_of_fate")
+
+ ui.fate = []
+ for (let fc = 0; fc <= 18; ++fc)
+ ui.fate[fc] = make_fate_card(fc)
+
+ if (0) {
+ for (let a = 0; a <= last_city; ++a) {
+ for (let b of cities.major_roads[a]) {
+ if (a < b) {
+ let dx = cities.x[a] - cities.x[b]
+ let dy = cities.y[a] - cities.y[b]
+ let x = (cities.x[a] + cities.x[b]) / 2
+ let y = (cities.y[a] + cities.y[b]) / 2
+ make_road("major_road", x - 10, y - 3, dx, dy)
+ }
+ }
+ for (let b of cities.roads[a]) {
+ if (a < b) {
+ let dx = cities.x[a] - cities.x[b]
+ let dy = cities.y[a] - cities.y[b]
+ let x = (cities.x[a] + cities.x[b]) / 2
+ let y = (cities.y[a] + cities.y[b]) / 2
+ make_road("road", x - 10, y - 1, dx, dy)
+ }
+ }
+ }
+ }
+
+ for (let a = 0; a <= last_city; ++a) {
+ let e = ui.cities[a] = document.createElement("div")
+ let x = cities.x[a]
+ let y = cities.y[a]
+
+ if (set_has(data.type.depot, a)) {
+ e.className = "space depot"
+ x -= 26
+ y -= 26
+ }
+ else if (set_has(all_objectives, a)) {
+ if (set_has(data.type.objective1_austria, a) || set_has(data.type.objective2_austria, a))
+ e.className = "space objective austria"
+ if (set_has(data.type.objective1_imperial, a) || set_has(data.type.objective2_imperial, a))
+ e.className = "space objective imperial"
+ if (set_has(data.type.objective1_sweden, a) || set_has(data.type.objective2_sweden, a))
+ e.className = "space objective sweden"
+ if (set_has(data.type.objective_france, a))
+ e.className = "space objective france"
+ if (set_has(data.type.objective_prussia, a))
+ e.className = "space objective prussia"
+ if (set_has(data.type.objective_russia, a))
+ e.className = "space objective russia"
+ x -= 20
+ y -= 20
+ }
+ else {
+ e.className = "space city"
+ x -= 18
+ y -= 18
+ }
+
+ register_action(e, "space", a)
+
+ //e.classList.add("hide")
+ e.style.left = x + "px"
+ e.style.top = y + "px"
+ e.title = cities.name[a]
+
+ ui.spaces_element.appendChild(e)
+ }
+
+ update_favicon()
+}
+
+/* UPDATE UI */
+
+function layout_general_offset(g, s) {
+ let n = 0
+ for (let i = g+1; i < 24; ++i)
+ if (view.pos[i] === s)
+ ++n
+ return n
+}
+
+function layout_general_count(g, s) {
+ let n = 0
+ for (let i = 0; i < 24; ++i)
+ if (view.pos[i] === s)
+ ++n
+ return n
+}
+
+function layout_general_offset_elim(g, s) {
+ let n = 0
+ let p = get_cylinder_power(g)
+ for (let i of all_power_generals[p])
+ if (i > g) // && view.pos[i] === s)
+ ++n
+ return n
+}
+
+function layout_train_offset(g, s) {
+ let n = 0
+ for (let i = g+1; i < 35; ++i)
+ if (view.pos[i] === s)
+ ++n
+ return n
+}
+
+function get_cylinder_power(id) {
+ for (let p of all_powers)
+ if (set_has(all_power_generals[p], id))
+ return p
+ return -1
+}
+
+function layout_general(id, s) {
+ let e = ui.pieces[id]
+ let x, y, n
+
+ if (s === REMOVED) {
+ if (e.parentElement === ui.pieces_element)
+ e.remove()
+ return
+ }
+ if (e.parentElement !== ui.pieces_element)
+ ui.pieces_element.appendChild(e)
+
+ if (s === ELIMINATED) {
+ n = layout_general_offset_elim(id)
+ x = ELIMINATED_GENERAL_X + ELIMINATED_GENERAL_DX * get_cylinder_power(id)
+ y = ELIMINATED_GENERAL_Y
+ } else {
+ n = layout_general_offset(id, s)
+ if (layout_general_count(id, s) === 3)
+ n -= 1
+ x = data.cities.x[s]
+ y = data.cities.y[s]
+ }
+
+ let selected = set_has(view.selected, id)
+
+ e.style.left = (x - 21) + "px"
+ e.style.top = (y - 29 - 15 * n) + "px"
+ e.style.zIndex = y + n
+ e.classList.toggle("selected", selected)
+ e.classList.toggle("oos", (view.oos & (1 <<id)) !== 0)
+
+ e = ui.troops[id]
+ // e.style.left = (x + 21 + 1) + "px"
+ // e.style.top = (y - 7 - 14 * n) + "px"
+ e.style.left = (x - 7) + "px"
+ // e.style.top = (y + 7 - 15 * n) + "px"
+ e.style.top = (y + 2 - 15 * n) + "px"
+ e.style.zIndex = y + n + 1
+ e.className = power_class[GENERAL_POWER[id]] + " piece number n" + view.troops[id]
+}
+
+function layout_train(id, s) {
+ let e = ui.pieces[id]
+ let n = layout_train_offset(id, s)
+ let x, y
+
+ if (s === REMOVED) {
+ if (e.parentElement === ui.pieces_element)
+ e.remove()
+ return
+ }
+ if (e.parentElement !== ui.pieces_element)
+ ui.pieces_element.appendChild(e)
+
+ if (s === ELIMINATED) {
+ x = ELIMINATED_TRAIN_X
+ y = ELIMINATED_TRAIN_Y
+ } else {
+ x = data.cities.x[s]
+ y = data.cities.y[s]
+ }
+
+ e.style.left = (x - 14 + n * 33) + "px"
+ e.style.top = (y - 21 - n * 0) + "px"
+ e.classList.toggle("selected", set_has(view.selected, id))
+}
+
+function create_conquest(style, s) {
+ let x = data.cities.x[s]
+ let y = data.cities.y[s]
+ let e = document.createElement("div")
+ e.dataset.id = s
+ e.style.left = (x - 16) + "px"
+ e.style.top = (y - 16) + "px"
+ e.className = style
+ return e
+}
+
+function to_deck(c) {
+ return c >> 7
+}
+
+function to_suit(c) {
+ return (c >> 4) & 7
+}
+
+function to_value(c) {
+ return c & 15
+}
+
+function update_favicon() {
+ let favicon = document.querySelector('link[rel="icon"]')
+ switch (params.role) {
+ case "Frederick": favicon.href = "favicon/favicon_frederick.png"; break
+ case "Elisabeth": favicon.href = "favicon/favicon_elisabeth.png"; break
+ case "Maria Theresa": favicon.href = "favicon/favicon_maria_theresa.png"; break
+ case "Pompadour": favicon.href = "favicon/favicon_pompadour.png"; break
+ }
+}
+
+function on_update() {
+ ui.header.classList.toggle("prussia", view.power === P_PRUSSIA)
+ ui.header.classList.toggle("hanover", view.power === P_HANOVER)
+ ui.header.classList.toggle("russia", view.power === P_RUSSIA)
+ ui.header.classList.toggle("sweden", view.power === P_SWEDEN)
+ ui.header.classList.toggle("austria", view.power === P_AUSTRIA)
+ ui.header.classList.toggle("imperial", view.power === P_IMPERIAL)
+ ui.header.classList.toggle("france", view.power === P_FRANCE)
+
+ for (let g = 0; g <= 23; ++g)
+ layout_general(g, view.pos[g])
+ for (let t = 24; t <= 34; ++t)
+ layout_train(t, view.pos[t])
+
+ let back = [ 0, 0, 0, 0 ]
+
+ for (let i = 0; i < 5; ++i)
+ ui.turns[i].classList.toggle("hide", (typeof view.fate === "object") || (i + 1 < view.fate))
+
+ for (let pow = 0; pow < 7; ++pow) {
+ ui.hand[pow].replaceChildren()
+ for (let c of view.hand[pow]) {
+ if ((c & 15) === 0)
+ ui.hand[pow].append(ui.tc_back[c>>7][back[c>>7]++])
+ else
+ ui.hand[pow].append(ui.tc[c])
+ }
+ }
+
+ ui.clock_of_fate.replaceChildren()
+ ui.clock_of_fate.appendChild(ui.fate[0])
+ if (typeof view.fate === "object")
+ for (let c of view.fate)
+ ui.clock_of_fate.appendChild(ui.fate[c])
+
+ ui.markers_element.replaceChildren()
+ for (let s of view.conquest)
+ ui.markers_element.appendChild(ui.conquest[s])
+ for (let s of view.retro)
+ ui.markers_element.appendChild(ui.retro[s])
+
+ /* troops 1-8, reserve 1-10 with modifiers +1 and +5 */
+ for (let v = 16; v >= 1; --v)
+ action_button_with_argument("value", v, v)
+
+ action_button("take", "Take")
+ action_button("give", "Give")
+ action_button("recruit", "Recruit")
+ action_button("transfer", "Transfer")
+ action_button("detach", "Detach")
+
+ action_button("stop", "Stop")
+ action_button("pass", "Pass")
+ action_button("next", "Next")
+ action_button("done", "Done")
+
+ action_button("end_setup", "End setup")
+ action_button("end_recruit", "End recruit")
+ action_button("end_movement", "End movement")
+ action_button("end_combat", "End combat")
+ action_button("end_supply", "End supply")
+ action_button("end_turn", "End turn")
+
+ action_button("undo", "Undo")
+
+ process_actions()
+}
+
+const piece_name = [
+ "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8",
+ "H1", "H2",
+ "R1", "R2", "R3", "R4",
+ "S1",
+ "A1", "A2", "A3", "A4", "A5",
+ "IA1",
+ "F1", "F2", "F3",
+ "PT1", "PT2", "HT", "RT1", "RT2", "ST", "AT1", "AT2", "IAT", "FT1", "FT2",
+]
+
+function sub_piece(match, p1) {
+ let x = p1 | 0
+ let n = piece_name[x]
+ // TODO: tooltip to highlight piece
+ return n
+}
+
+function sub_space(match, p1) {
+ let x = p1 | 0
+ let n = data.cities.name[x]
+ // TODO: tooltip to highlight location
+ return n
+}
+
+function sub_tc(match, p1) {
+ let x = p1 | 0
+ let suit = to_suit(x)
+ let value = to_value(x)
+ if (suit === RESERVE)
+ return suit_name[suit]
+ return value + suit_name[suit]
+}
+
+function sub_fate(match, p1) {
+ let x = p1 | 0
+ return cards_of_fate_name[x]
+}
+
+function on_log(text) {
+ let p = document.createElement("div")
+
+ if (text.match(/^>/)) {
+ text = text.substring(1)
+ p.className = 'i'
+ }
+
+ text = text.replace(/&/g, "&amp;")
+ text = text.replace(/</g, "&lt;")
+ text = text.replace(/>/g, "&gt;")
+
+ text = text.replace(/S(\d+)/g, sub_space)
+ text = text.replace(/F(\d+)/g, sub_fate)
+ text = text.replace(/C(\d+)/g, sub_tc)
+ text = text.replace(/P(\d+)/g, sub_piece)
+
+ if (text.match(/^# /)) {
+ p.className = "h"
+ text = text.substring(2)
+ }
+ else if (text.match(/^=\d/)) {
+ p.className = "h " + power_class[text[1]]
+ text = power_name[text[1]]
+ }
+
+ p.innerHTML = text
+ return p
+}
+
+on_init()
+
+// === COMMON LIBRARY ===
+
+function array_insert(array, index, item) {
+ for (let i = array.length; i > index; --i)
+ array[i] = array[i - 1]
+ array[index] = item
+}
+
+function set_has(set, item) {
+ if (set === item) return true
+ if (set === undefined) return false
+ if (set === null) return false
+ 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
+ }
+ array_insert(set, a, item)
+}
+
+function set_add_all(set, other) {
+ for (let item of other)
+ set_add(set, item)
+}
+
diff --git a/rules.js b/rules.js
index 0b04622..40c3375 100644
--- a/rules.js
+++ b/rules.js
@@ -1,2 +1,2647 @@
-exports.roles = [ "Frederick", "Elisabeth", "Maria Theresa", "Pompadour" ]
-exports.scenarios = [ "Standard" ]
+"use strict"
+
+const R_FREDERICK = "Frederick"
+const R_ELISABETH = "Elisabeth"
+const R_MARIA_THERESA = "Maria Theresa"
+const R_POMPADOUR = "Pompadour"
+
+const ROLE_NAME_4 = [
+ R_FREDERICK,
+ R_ELISABETH,
+ R_MARIA_THERESA,
+ R_POMPADOUR,
+]
+
+const ROLE_NAME_3 = [
+ R_FREDERICK,
+ R_ELISABETH,
+ R_MARIA_THERESA,
+]
+
+exports.roles = function (_scenario, options) {
+ let n = parseInt(options.players) || 4
+ if (n === 3)
+ return ROLE_NAME_3
+ else
+ return ROLE_NAME_4
+}
+
+exports.scenarios = [
+ "Standard",
+// "Expert",
+// "2P The War in the West",
+// "2P The Austrian Theatre",
+]
+
+/* DATA */
+
+var game
+var view
+var states = {}
+
+const data = require("./data")
+
+function find_city(city) {
+ let n = data.cities.name.length
+ let x = -1
+ for (let c = 0; c < n; ++c) {
+ if (data.cities.name[c] === city) {
+ if (x < 0)
+ x = c
+ else
+ throw "TWO CITIES: " + city
+ }
+ }
+ if (x < 0)
+ throw "CITY NOT FOUND: " + city
+ return x
+}
+
+function find_city_list(names) {
+ let list = []
+ for (let n of names)
+ set_add(list, find_city(n))
+ return list
+}
+
+const P_PRUSSIA = 0
+const P_HANOVER = 1
+const P_RUSSIA = 2
+const P_SWEDEN = 3
+const P_AUSTRIA = 4
+const P_IMPERIAL = 5
+const P_FRANCE = 6
+
+const POWER_NAME = [ "Prussia", "Hanover", "Russia", "Sweden", "Austria", "Imperial Army", "France" ]
+
+const SPADES = 0
+const CLUBS = 1
+const HEARTS = 2
+const DIAMONDS = 3
+const RESERVE = 4
+
+// Strokes of Fate cards
+const FC_POEMS = 13
+const FC_LORD_BUTE = 14
+const FC_ELISABETH = 15
+const FC_SWEDEN = 16
+const FC_INDIA = 17
+const FC_AMERICA = 18
+
+const ELIMINATED = data.cities.name.length
+const REMOVED = ELIMINATED + 1
+
+const max_power_troops = [ 32, 12, 16, 4, 30, 6, 20 ]
+
+const all_powers = [ 0, 1, 2, 3, 4, 5, 6 ]
+
+const all_home_or_depot_cities = [
+ data.country.Prussia,
+ data.country.Hanover,
+ find_city_list([ "Sierpc", "Warszawa" ]),
+ data.country.Sweden,
+ data.country.Austria,
+ set_union(data.country.Empire, data.country.Saxony),
+ find_city_list([ "Hildburghausen" ]),
+ find_city_list([ "Koblenz", "Gemünden" ]),
+]
+
+const all_power_depots = [
+ find_city_list([ "Berlin" ]),
+ find_city_list([ "Stade" ]),
+ find_city_list([ "Sierpc", "Warszawa" ]),
+ find_city_list([ "Stralsund" ]),
+ find_city_list([ "Tabor", "Brünn" ]),
+ find_city_list([ "Hildburghausen" ]),
+ find_city_list([ "Koblenz", "Gemünden" ]),
+]
+
+const MUNSTER_Y = data.cities.y[find_city("Munster")]
+
+const all_power_re_entry_cities = [
+ data.sectors.spades_berlin,
+ data.sectors.diamonds_stade.filter(s => data.cities.y[s] < MUNSTER_Y),
+ data.sectors.spades_warszawa,
+ data.country.Sweden,
+ set_intersect(data.sectors.diamonds_brunn, data.country.Austria),
+ data.sectors.spades_south_of_hildburghausen,
+ data.sectors.hearts_south_of_koblenz,
+]
+
+const all_power_generals = [
+ /* P */ [ 0, 1, 2, 3, 4, 5, 6, 7 ],
+ /* H */ [ 8, 9 ],
+ /* R */ [ 10, 11, 12, 13 ],
+ /* S */ [ 14 ],
+ /* A */ [ 15, 16, 17, 18, 19 ],
+ /* I */ [ 20 ],
+ /* F */ [ 21, 22, 23 ],
+]
+
+const GEN_FRIEDRICH = 0
+const GEN_CUMBERLAND = 9
+
+const all_power_generals_rev = all_power_generals.map(list => list.slice().reverse())
+
+const all_power_trains = [
+ /* P */ [ 24, 25 ],
+ /* H */ [ 26 ],
+ /* R */ [ 27, 28 ],
+ /* S */ [ 29 ],
+ /* A */ [ 30, 31 ],
+ /* I */ [ 32 ],
+ /* F */ [ 33, 34 ],
+]
+
+function is_general(p) {
+ return p < 24
+}
+
+const all_pieces = [ ...all_power_generals.flat(), ...all_power_trains.flat() ]
+const all_generals = [ ...all_power_generals.flat() ]
+
+const all_prussia_trains = [
+ ...all_power_trains[P_PRUSSIA],
+ ...all_power_trains[P_HANOVER],
+]
+
+const all_anti_prussia_trains = [
+ ...all_power_trains[P_RUSSIA],
+ ...all_power_trains[P_SWEDEN],
+ ...all_power_trains[P_AUSTRIA],
+ ...all_power_trains[P_IMPERIAL],
+ ...all_power_trains[P_FRANCE],
+]
+
+const all_friendly_trains = [
+ all_prussia_trains,
+ all_prussia_trains,
+ all_anti_prussia_trains,
+ all_anti_prussia_trains,
+ all_anti_prussia_trains,
+ all_anti_prussia_trains,
+ all_anti_prussia_trains,
+]
+
+const all_enemy_trains = [
+ all_anti_prussia_trains,
+ all_anti_prussia_trains,
+ all_prussia_trains,
+ all_prussia_trains,
+ all_prussia_trains,
+ all_prussia_trains,
+ all_prussia_trains,
+]
+
+const all_prussia_generals = [
+ ...all_power_generals[P_PRUSSIA],
+ ...all_power_generals[P_HANOVER],
+]
+
+const all_anti_prussia_generals = [
+ ...all_power_generals[P_RUSSIA],
+ ...all_power_generals[P_SWEDEN],
+ ...all_power_generals[P_AUSTRIA],
+ ...all_power_generals[P_IMPERIAL],
+ ...all_power_generals[P_FRANCE],
+]
+
+const all_enemy_generals = [
+ all_anti_prussia_generals,
+ all_anti_prussia_generals,
+ all_prussia_generals,
+ all_prussia_generals,
+ all_prussia_generals,
+ all_prussia_generals,
+ all_prussia_generals,
+]
+
+function is_supply_train(p) {
+ return p >= 24
+}
+
+function to_deck(c) {
+ return c >> 7
+}
+
+function to_suit(c) {
+ return (c >> 4) & 7
+}
+
+function to_value(c) {
+ return c & 15
+}
+
+function is_reserve(c) {
+ return to_suit(c) === RESERVE
+}
+
+/* OBJECTIVES */
+
+const all_objectives = []
+set_add_all(all_objectives, data.type.objective1_austria)
+set_add_all(all_objectives, data.type.objective2_austria)
+set_add_all(all_objectives, data.type.objective1_imperial)
+set_add_all(all_objectives, data.type.objective2_imperial)
+set_add_all(all_objectives, data.type.objective1_sweden)
+set_add_all(all_objectives, data.type.objective2_sweden)
+set_add_all(all_objectives, data.type.objective_france)
+set_add_all(all_objectives, data.type.objective_prussia)
+set_add_all(all_objectives, data.type.objective_russia)
+
+const protect_range = []
+for (let s of all_objectives)
+ make_protect_range(protect_range[s] = [], s, s, 3)
+
+function make_protect_range(result, start, here, range) {
+ for (let next of data.cities.adjacent[here]) {
+ if (next !== start)
+ set_add(result, next)
+ if (range > 1)
+ make_protect_range(result, start, next, range - 1)
+ }
+}
+
+const primary_objective = [ [], [], [], [], [], [], [] ]
+const secondary_objective = [ [], [], [], [], [], [], [] ]
+const protect = [ [], [], [], [], [], [], [] ]
+
+for (let s of data.type.objective_prussia) set_add(primary_objective[P_PRUSSIA], s)
+for (let s of data.type.objective_russia) set_add(primary_objective[P_RUSSIA], s)
+for (let s of data.type.objective1_sweden) set_add(primary_objective[P_SWEDEN], s)
+for (let s of data.type.objective2_sweden) set_add(secondary_objective[P_SWEDEN], s)
+for (let s of data.type.objective1_austria) set_add(primary_objective[P_AUSTRIA], s)
+for (let s of data.type.objective2_austria) set_add(secondary_objective[P_AUSTRIA], s)
+for (let s of data.type.objective1_imperial) set_add(primary_objective[P_IMPERIAL], s)
+for (let s of data.type.objective2_imperial) set_add(secondary_objective[P_IMPERIAL], s)
+for (let s of data.type.objective_france) set_add(primary_objective[P_FRANCE], s)
+
+const full_objective = [
+ set_union(primary_objective[0], secondary_objective[0]),
+ set_union(primary_objective[1], secondary_objective[1]),
+ set_union(primary_objective[2], secondary_objective[2]),
+ set_union(primary_objective[3], secondary_objective[3]),
+ set_union(primary_objective[4], secondary_objective[4]),
+ set_union(primary_objective[5], secondary_objective[5]),
+ set_union(primary_objective[6], secondary_objective[6]),
+]
+
+function make_protect(power, country) {
+ for (let s of all_objectives)
+ if (set_has(country, s))
+ set_add(protect[power], s)
+}
+
+make_protect(P_PRUSSIA, data.country.Prussia)
+make_protect(P_PRUSSIA, data.country.Saxony)
+make_protect(P_HANOVER, data.country.Hanover)
+make_protect(P_AUSTRIA, data.country.Austria)
+
+function is_conquest_space(pow, s) {
+ return set_has(full_objective[pow], s)
+}
+
+function is_reconquest_space(pow, s) {
+ return set_has(protect[pow], s)
+}
+
+function is_protected_from_conquest(s) {
+ for (let pow of all_powers) {
+ if (set_has(protect[pow], s)) {
+ let range = protect_range[s]
+ for (let p of all_power_generals[pow])
+ if (set_has(range, game.pos[p]))
+ return true
+ if (pow === P_IMPERIAL) {
+ for (let p of all_power_trains[pow])
+ if (set_has(range, game.pos[p]))
+ return true
+ }
+ }
+ }
+ return false
+}
+
+function is_protected_from_reconquest(s) {
+ for (let pow of all_powers) {
+ if (set_has(full_objective[pow], s)) {
+ let range = protect_range[s]
+ for (let p of all_power_generals[pow])
+ if (set_has(range, game.pos[p]))
+ return true
+ if (pow === P_IMPERIAL) {
+ for (let p of all_power_trains[pow])
+ if (set_has(range, game.pos[p]))
+ return true
+ }
+ }
+ }
+ return false
+}
+
+/* STATE */
+
+function turn_power_draw(pow) {
+ let n = 0
+ switch (pow) {
+ case P_PRUSSIA:
+ n = 7
+ if (set_has(game.fate, FC_LORD_BUTE))
+ n = Math.max(4, n - 2)
+ if (set_has(game.fate, FC_POEMS))
+ n = Math.max(4, n - 2)
+ break
+ case P_HANOVER:
+ n = 2
+ if (set_has(game.fate, FC_INDIA) && set_has(game.fate, FC_AMERICA))
+ n = 1
+ break
+ case P_RUSSIA:
+ n = 4
+ break
+ case P_SWEDEN:
+ n = 1
+ break
+ case P_AUSTRIA:
+ n = 5
+ if (set_has(game.fate, FC_INDIA) || set_has(game.fate, FC_AMERICA))
+ n = 4
+ break
+ case P_IMPERIAL:
+ n = 1
+ break
+ case P_FRANCE:
+ n = 4
+ if (set_has(game.fate, FC_INDIA) || set_has(game.fate, FC_AMERICA))
+ n = 4
+ break
+ }
+ return n
+}
+
+function has_power_dropped_out(pow) {
+ switch (pow) {
+ case P_RUSSIA: return has_russia_dropped_out()
+ case P_SWEDEN: return has_sweden_dropped_out()
+ case P_FRANCE: return has_france_dropped_out()
+ }
+ return false
+}
+
+function has_russia_dropped_out() {
+ return set_has(game.fate, FC_ELISABETH)
+}
+
+function has_sweden_dropped_out() {
+ return set_has(game.fate, FC_SWEDEN)
+}
+
+function has_france_dropped_out() {
+ return set_has(game.fate, FC_INDIA) && set_has(game.fate, FC_AMERICA)
+}
+
+function has_imperial_army_switched_players() {
+ return (has_russia_dropped_out() && has_sweden_dropped_out()) || has_france_dropped_out()
+}
+
+function has_removed_all_pieces(pow) {
+ for (let p of all_power_generals[pow])
+ if (game.pos[p] !== REMOVED)
+ return false
+ for (let p of all_power_trains[pow])
+ if (game.pos[p] !== REMOVED)
+ return false
+ return true
+}
+
+function player_from_power(pow) {
+ let role = null
+ switch (pow) {
+ case P_PRUSSIA:
+ case P_HANOVER:
+ role = R_FREDERICK
+ break
+ case P_RUSSIA:
+ case P_SWEDEN:
+ role = R_ELISABETH
+ break
+ case P_AUSTRIA:
+ role = R_MARIA_THERESA
+ break
+ case P_IMPERIAL:
+ if (has_russia_dropped_out() && has_sweden_dropped_out())
+ role = R_ELISABETH
+ else if (has_france_dropped_out())
+ role = R_POMPADOUR
+ else
+ role = R_MARIA_THERESA
+ break
+ case P_FRANCE:
+ role = R_POMPADOUR
+ break
+ }
+ if (game.scenario === 3 && role === R_POMPADOUR)
+ role = R_ELISABETH
+ return role
+}
+
+function current_player() {
+ return player_from_power(game.power)
+}
+
+function get_top_piece(s) {
+ for (let p of all_pieces)
+ if (game.pos[p] === s)
+ return p
+ return -1
+}
+
+function get_supreme_commander(s) {
+ for (let p of all_generals)
+ if (game.pos[p] === s)
+ return p
+ return -1
+}
+
+function get_stack_power(s) {
+ for (let pow of all_powers)
+ for (let p of all_power_generals[pow])
+ if (game.pos[p] === s)
+ return pow
+ throw "IMPOSSIBLE"
+}
+
+function is_space_suit(s, ranges) {
+ for (let [a, b] of ranges)
+ if (s >= a && s <= b)
+ return true
+ return false
+}
+
+function get_space_suit(s) {
+ if (is_space_suit(s, data.suit.spades))
+ return SPADES
+ if (is_space_suit(s, data.suit.clubs))
+ return CLUBS
+ if (is_space_suit(s, data.suit.hearts))
+ return HEARTS
+ if (is_space_suit(s, data.suit.diamonds))
+ return DIAMONDS
+ throw "IMPOSSIBLE"
+}
+
+function count_eliminated_trains() {
+ let n = 0
+ for (let p of all_power_trains[game.power])
+ if (game.pos[p] === ELIMINATED)
+ ++n
+ return n
+}
+
+function count_used_troops() {
+ let current = 0
+ for (let p of all_power_generals[game.power])
+ current += game.troops[p]
+ return current
+}
+
+function count_unused_generals() {
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.troops[p] === 0)
+ ++n
+ return n
+}
+
+function retire_general(p) {
+ log("P" + p + " retired.")
+
+ // save troops if possible
+ let s = game.pos[p]
+ let n = game.troops[p]
+ game.pos[p] = REMOVED
+ game.troops[p] = 0
+
+ if (s < ELIMINATED) {
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === s) {
+ let x = Math.min(n, 8 - game.troops[p])
+ game.troops[p] += x
+ n -= x
+ }
+ }
+ if (n > 0)
+ log("Lost " + n + " troops.")
+ }
+}
+
+/* SEQUENCE OF PLAY */
+
+const POWER_FROM_ACTION_STEP = [
+ P_PRUSSIA,
+ P_HANOVER,
+ P_RUSSIA,
+ P_SWEDEN,
+ P_AUSTRIA,
+ P_IMPERIAL,
+ P_FRANCE,
+]
+
+function set_active_current_power() {
+ game.power = POWER_FROM_ACTION_STEP[game.step]
+ game.active = current_player()
+}
+
+function goto_start_turn() {
+ game.step = 0
+
+ // Check before drawing a fate card.
+ if (check_victory())
+ return
+
+ if (++game.turn <= 5) {
+ log("# Turn " + game.turn)
+ } else {
+ log("# Card of Fate")
+
+ // remove non-stroke of fate card from last turn
+ for (let i = 1; i <= 12; ++i)
+ set_delete(game.fate, i)
+
+ let fc = game.clock.pop()
+ log("F" + fc)
+
+ set_add(game.fate, fc)
+
+ // Check again in case of eased victory conditions.
+ if (check_victory())
+ return
+
+ if (fc === FC_ELISABETH) {
+ game.hand[P_RUSSIA] = []
+ game.power = P_PRUSSIA
+ game.active = current_player()
+ game.state = "russia_quits_the_game_1"
+ return
+ }
+
+ if (fc === FC_SWEDEN) {
+ game.hand[P_SWEDEN] = []
+ game.power = P_PRUSSIA
+ game.active = current_player()
+ game.state = "sweden_quits_the_game_1"
+ return
+ }
+
+ if ((fc === FC_INDIA && set_has(game.fate, FC_AMERICA)) || (fc === FC_AMERICA && set_has(game.fate, FC_INDIA))) {
+ game.hand[P_FRANCE] = []
+ game.power = P_HANOVER
+ game.active = current_player()
+ game.state = "france_quits_the_game_1"
+ return
+ }
+ }
+
+ resume_start_turn()
+}
+
+function resume_start_turn() {
+
+ // MARIA: politics
+ // MARIA: hussars
+
+ goto_action_stage()
+}
+
+function goto_action_stage() {
+ set_active_current_power()
+
+ if (has_power_dropped_out(game.power)) {
+ end_action_stage()
+ return
+ }
+
+ log("=" + game.power)
+ goto_tactical_cards()
+}
+
+function end_action_stage() {
+ if (++game.step === 7)
+ goto_end_of_turn()
+ else
+ goto_action_stage()
+}
+
+function goto_end_of_turn() {
+ goto_start_turn()
+}
+
+/* VICTORY */
+
+function has_conquered_all_of(list) {
+ for (let s of list)
+ if (!set_has(game.conquest, s))
+ return false
+ return true
+}
+
+function check_power_victory(list, power) {
+ if (has_conquered_all_of(list[power])) {
+ goto_game_over(player_from_power(power), POWER_NAME[power] + " won.")
+ return true
+ }
+ return false
+}
+
+function check_victory() {
+ // Prussian victory
+ if (has_russia_dropped_out() && has_sweden_dropped_out() && has_france_dropped_out()) {
+ goto_game_over(R_FREDERICK, "Prussia won.")
+ return true
+ }
+
+ // Normal victory conditions
+ if (
+ check_power_victory(full_objective, P_RUSSIA) ||
+ check_power_victory(full_objective, P_SWEDEN) ||
+ check_power_victory(full_objective, P_AUSTRIA) ||
+ check_power_victory(full_objective, P_IMPERIAL) ||
+ check_power_victory(full_objective, P_FRANCE)
+ )
+ return true
+
+ // Eased victory conditions
+ if (has_russia_dropped_out()) {
+ if (check_power_victory(primary_objective, P_SWEDEN))
+ return true
+ }
+ if (has_imperial_army_switched_players()) {
+ if (check_power_victory(primary_objective, P_AUSTRIA))
+ return true
+ if (check_power_victory(primary_objective, P_IMPERIAL))
+ return true
+ }
+
+ return false
+}
+
+/* TACTICAL CARDS */
+
+function find_largest_discard(u) {
+ for (let i = 0; i < 4; ++i)
+ if (u[i] <= u[0] && u[i] <= u[1] && u[i] <= u[2] && u[i] <= u[3])
+ return i
+ throw "IMPOSSIBLE"
+}
+
+function next_tactics_deck() {
+ let held = [ 0, 0, 0, 0 ]
+
+ // count cards in hands
+ for (let pow of all_powers)
+ for (let c of game.hand[pow])
+ held[to_deck(c)]++
+
+ // find next unused deck
+ for (let i = 1; i < 4; ++i) {
+ if (held[i] === 0) {
+ game.deck = make_tactics_deck(i)
+ shuffle_bigint(game.deck)
+ return
+ }
+ }
+
+ // find two largest discard piles
+ let a = find_largest_discard(held)
+ log("Deck " + a + ": " + held[a])
+ if (held[a] === 50) {
+ goto_game_over("Draw", "All cards held.")
+ return
+ }
+ held[a] = 100
+
+ let b = find_largest_discard(held)
+ log("Deck " + b + ": " + held[b])
+ if (held[b] === 50) {
+ goto_game_over("Draw", "All cards held.")
+ return
+ }
+
+ log("Shuffled new deck from discards " + (a+1) + " and " + (b+1) + ".")
+
+ game.deck = [
+ make_tactics_discard(a),
+ make_tactics_discard(b)
+ ].flat()
+
+ shuffle_bigint(game.deck)
+}
+
+function draw_next_tc() {
+ if (game.deck.length === 0)
+ next_tactics_deck()
+ return game.deck.pop()
+}
+
+function goto_tactical_cards() {
+ let pow = game.power
+ let n = turn_power_draw(pow)
+
+ log("Draw " + n + " TC.")
+
+ for (let i = 0; i < n; ++i)
+ set_add(game.hand[pow], draw_next_tc())
+
+ // MARIA: supply is before movement
+
+ goto_movement()
+}
+
+/* TRANSFER TROOPS */
+
+function count_stacked_take() {
+ let n = 0
+ for (let p of game.selected)
+ n += 8 - game.troops[p]
+ return n
+}
+
+function count_unstacked_take() {
+ let here = game.pos[game.selected[0]]
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === here && !set_has(game.selected, p))
+ n += 8 - game.troops[p]
+ return n
+}
+
+function count_stacked_give() {
+ let n = 0
+ for (let p of game.selected)
+ n += game.troops[p] - 1
+ return n
+}
+
+function count_unstacked_give() {
+ let here = game.pos[game.selected[0]]
+ let n = 0
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === here && !set_has(game.selected, p))
+ n += game.troops[p] - 1
+ return n
+}
+
+function take_troops(total) {
+ let here = game.pos[game.selected[0]]
+
+ let n = total
+ for (let p of game.selected) {
+ let x = Math.max(0, Math.min(n, 8 - game.troops[p]))
+ game.troops[p] += x
+ n -= x
+ }
+
+ n = total
+ for (let p of all_power_generals_rev[game.power]) {
+ if (game.pos[p] === here && !set_has(game.selected, p)) {
+ let x = Math.max(0, Math.min(n, game.troops[p] - 1))
+ game.troops[p] -= x
+ n -= x
+ }
+ }
+}
+
+function give_troops(total) {
+ let here = game.pos[game.selected[0]]
+
+ let n = total
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === here && !set_has(game.selected, p)) {
+ let x = Math.max(0, Math.min(n, 8 - game.troops[p]))
+ game.troops[p] += x
+ n -= x
+ }
+ }
+
+ n = total
+ for (let p of game.selected) {
+ let x = Math.max(0, Math.min(n, game.troops[p] - 1))
+ game.troops[p] -= x
+ n -= x
+ }
+}
+
+/* MOVEMENT */
+
+function goto_movement() {
+ game.state = "movement"
+ set_clear(game.moved)
+}
+
+function is_supreme_commander(p) {
+ let s = game.pos[p]
+ for (let other of all_generals)
+ if (game.pos[other] === s)
+ return other === p
+ return false
+}
+
+states.movement = {
+ prompt() {
+ prompt("Move your generals and supply trains.")
+
+ let pow = game.power
+
+ for (let p of all_power_generals[pow])
+ if (!set_has(game.moved, p) && is_supreme_commander(p) && game.pos[p] < ELIMINATED)
+ gen_action_piece(p)
+
+ for (let p of all_power_trains[pow])
+ if (!set_has(game.moved, p) && game.pos[p] < ELIMINATED)
+ gen_action_piece(p)
+
+ view.actions.end_movement = 1
+ },
+ piece(p) {
+ push_undo()
+
+ game.selected = [ p ]
+ let here = game.pos[p]
+ for (let other of all_power_generals[game.power])
+ if (other > p && game.pos[other] === here)
+ game.selected.push(other)
+
+ game.count = 0
+ game.major = 1
+ if (is_supply_train(p))
+ game.state = "move_supply_train"
+ else
+ game.state = "move_general"
+ },
+ end_movement() {
+ push_undo()
+ goto_recruit()
+ },
+
+}
+
+function has_any_piece(to) {
+ for (let s of game.pos)
+ if (s === to)
+ return true
+ return false
+}
+
+function has_friendly_supply_train(to) {
+ for (let p of all_friendly_trains[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_enemy_piece(to) {
+ for (let p of all_enemy_generals[game.power])
+ if (game.pos[p] === to)
+ return true
+ for (let p of all_enemy_trains[game.power])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function has_any_other_general(to) {
+ for (let other of all_powers)
+ if (other !== game.power)
+ for (let p of all_power_generals[other])
+ if (game.pos[p] === to)
+ return true
+ return false
+}
+
+function count_pieces(to) {
+ let n = 0
+ for (let s of game.pos)
+ if (s === to)
+ ++n
+ return n
+}
+
+function can_move_general_to(to) {
+ if (has_friendly_supply_train(to))
+ return false
+ if (has_any_other_general(to))
+ return false
+ if (game.selected.length + count_pieces(to) > 3)
+ return false
+ return true
+}
+
+states.move_supply_train = {
+ prompt() {
+ prompt("Move supply train.")
+ view.selected = game.selected
+
+ let who = game.selected[0]
+ let here = game.pos[who]
+
+ if (game.count < 2 + game.major)
+ for (let next of data.cities.major_roads[here])
+ if (!has_any_piece(next))
+ gen_action_space(next)
+ if (game.count < 2)
+ for (let next of data.cities.roads[here])
+ if (!has_any_piece(next))
+ gen_action_space(next)
+
+ if (game.count > 0) {
+ gen_action_piece(who)
+ view.actions.stop = 1
+ }
+ },
+ piece(_) {
+ this.stop()
+ },
+ stop() {
+ game.state = "movement"
+ },
+ space(to) {
+ let who = game.selected[0]
+ let here = game.pos[who]
+
+ log("P" + who + " to S" + to)
+
+ if (!set_has(data.cities.major_roads[here], to))
+ game.major = 0
+ set_add(game.moved, who)
+ game.pos[who] = to
+
+ if (++game.count === 2 + game.major)
+ game.state = "movement"
+ },
+}
+
+states.move_general = {
+ prompt() {
+ prompt("Move general.")
+ view.selected = game.selected
+
+ let who = game.selected[0]
+ let here = game.pos[who]
+
+ if (game.count === 0) {
+ if (game.selected.length > 1)
+ view.actions.detach = 1
+ else
+ view.actions.detach = 0
+
+ let s_take = count_stacked_take()
+ let s_give = count_stacked_give()
+ let u_take = count_unstacked_take()
+ let u_give = count_unstacked_give()
+
+ if (s_take > 0 && u_give > 0)
+ view.actions.take = 1
+ if (s_give > 0 && u_take > 0)
+ view.actions.give = 1
+ } else {
+ gen_action_piece(who)
+ view.actions.stop = 1
+ }
+
+ if (game.count < 3 + game.major)
+ for (let next of data.cities.major_roads[here])
+ if (can_move_general_to(next))
+ gen_action_space_or_piece(next)
+
+ if (game.count < 3)
+ for (let next of data.cities.roads[here])
+ if (can_move_general_to(next))
+ gen_action_space_or_piece(next)
+ },
+ take() {
+ game.state = "move_take"
+ },
+ give() {
+ game.state = "move_give"
+ },
+ detach() {
+ game.state = "move_detach"
+ },
+ piece(p) {
+ if (p === game.selected[0])
+ this.stop()
+ else
+ this.space(game.pos[p])
+ },
+ stop() {
+ for (let p of game.selected)
+ set_add(game.moved, p)
+ game.state = "movement"
+ },
+ space(to) {
+ let pow = game.power
+ let who = game.selected[0]
+ let from = game.pos[who]
+
+ log("P" + who + " to S" + to)
+
+ if (!set_has(data.cities.major_roads[from], to))
+ game.major = 0
+
+ // uniting stacks (flag all as moved)
+ for (let p of game.selected) {
+ set_add(game.moved, p)
+ game.pos[p] = to
+ }
+
+ // uniting stacks (turn all oos if one is oos)
+ let oos = false
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === to && is_out_of_supply(p))
+ oos = true
+ if (oos)
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] === to)
+ set_out_of_supply(p)
+
+ if (is_conquest_space(pow, from) && !set_has(game.conquest, from)) {
+ if (is_protected_from_conquest(from)) {
+ set_add(game.retro, from)
+ } else {
+ log("Conquered S" + from)
+ set_add(game.conquest, from)
+ }
+ }
+
+ if (is_reconquest_space(pow, from) && set_has(game.conquest, from)) {
+ if (is_protected_from_reconquest(from)) {
+ set_add(game.retro, from)
+ } else {
+ log("Reconquered S" + from)
+ set_delete(game.conquest, from)
+ }
+ }
+
+ for (let p of all_enemy_trains[pow]) {
+ if (game.pos[p] === to) {
+ log("Eliminate P" + p)
+ game.pos[p] = ELIMINATED
+ game.state = "movement"
+ return
+ }
+ }
+
+ for (let p of all_power_generals[pow]) {
+ if (game.pos[p] === to && !set_has(game.selected, p)) {
+ set_add(game.moved, p)
+ game.state = "movement"
+ return
+ }
+ }
+
+ if (++game.count === 3 + game.major) {
+ game.state = "movement"
+ }
+ },
+}
+
+states.move_detach = {
+ prompt() {
+ prompt("Detach general.")
+ for (let p of game.selected)
+ gen_action_piece(p)
+ },
+ piece(p) {
+ set_delete(game.selected, p)
+ game.state = "move_general"
+ },
+}
+
+states.move_take = {
+ prompt() {
+ prompt("Take troops from detached generals.")
+ let take = count_stacked_take()
+ let give = count_unstacked_give()
+ let n = Math.min(take, give)
+ view.actions.value = []
+ for (let i = 1; i <= n; ++i)
+ view.actions.value.push(i)
+ },
+ value(v) {
+ take_troops(v)
+ game.state = "move_general"
+ },
+}
+
+states.move_give = {
+ prompt() {
+ prompt("Give troops to detached generals.")
+ let take = count_unstacked_take()
+ let give = count_stacked_give()
+ let n = Math.min(take, give)
+ view.actions.value = []
+ for (let i = 1; i <= n; ++i)
+ view.actions.value.push(i)
+ },
+ value(v) {
+ give_troops(v)
+ game.state = "move_general"
+ },
+}
+
+/* RECRUITMENT */
+
+function troop_cost() {
+ if (game.re_enter !== undefined)
+ return 8
+ return 6
+}
+
+function has_available_depot() {
+ for (let s of all_power_depots[game.power])
+ if (!has_enemy_piece(s))
+ return true
+ return false
+}
+
+function goto_recruit() {
+ push_undo()
+ game.count = 0
+
+ // TODO: reveal too much if we skip recruitment phase?
+ if (count_eliminated_trains() === 0 && count_used_troops() === max_power_troops[game.power]) {
+ end_recruit()
+ return
+ }
+
+ // if all depots have enemy pieces, choose ONE city in XXX sector and COST is 8
+ if (has_available_depot())
+ game.state = "recruit"
+ else
+ game.state = "re_enter_choose_city"
+}
+
+states.re_enter_choose_city = {
+ prompt() {
+ prompt("Choose city to re-enter troops.")
+ for (let s of all_power_re_entry_cities[game.power])
+ if (!has_enemy_piece(s))
+ gen_action_space(s)
+ },
+ space(s) {
+ push_undo()
+ game.re_enter = s
+ game.state = "recruit"
+ },
+}
+
+states.recruit = {
+ prompt() {
+ let cost = troop_cost()
+ let buy_amount = (game.count / cost) | 0
+ let n_troops = count_used_troops()
+ let av_troops = max_power_troops[game.power] - n_troops
+ let av_trains = count_eliminated_trains()
+
+ if (av_trains === 0 && av_troops === 0)
+ prompt(`Nothing to recruit. ${n_troops}/${max_power_troops[game.power]} troops.`)
+ else
+ prompt(`Recruit supply trains and/or troops. ${n_troops}/${max_power_troops[game.power]} troops. ${game.count} points.`)
+
+ if (buy_amount < av_troops + av_trains) {
+ for (let c of game.hand[game.power])
+ gen_action_card(c)
+ }
+
+ if (game.count >= cost) {
+ if (av_troops > 0)
+ for (let p of all_power_generals[game.power])
+ if (game.troops[p] < 8)
+ gen_action_piece(p)
+ if (av_trains > 0)
+ for (let p of all_power_trains[game.power])
+ if (game.pos[p] === ELIMINATED)
+ gen_action_piece(p)
+ }
+
+ // don't force buying a T
+ if (buy_amount === 0 || av_troops === 0)
+ view.actions.end_recruit = 1
+ },
+ card(c) {
+ push_undo()
+ log("Recruit with C" + c)
+ array_remove_item(game.hand[game.power], c)
+ game.count += is_reserve(c) ? 10 : to_value(c)
+ },
+ piece(p) {
+ push_undo()
+ game.count -= troop_cost()
+ if (game.pos[p] === ELIMINATED) {
+ game.selected = [ p ]
+ game.state = "re_enter"
+ } else {
+ game.troops[p]++
+ }
+ },
+ end_recruit() {
+ push_undo()
+ end_recruit()
+ },
+}
+
+function end_recruit() {
+ delete game.re_enter
+ goto_combat()
+}
+
+function can_re_enter_general(s) {
+ return can_move_general_to(s)
+}
+
+function can_re_enter_supply_train(s) {
+ return !has_any_piece(s)
+}
+
+states.re_enter = {
+ prompt() {
+ prompt("Re-enter piece.")
+ let p = game.selected[0]
+ let can_re_enter_at = is_general(p) ? can_re_enter_general : can_re_enter_supply_train
+
+ if (game.re_enter !== undefined) {
+ if (can_re_enter_at(game.re_enter))
+ gen_action_space(game.re_enter)
+ } else {
+ for (let s of all_power_depots[game.power])
+ if (can_re_enter_at(s))
+ gen_action_space(s)
+ }
+ },
+ space(s) {
+ let p = game.selected[0]
+ log("Re-entered P" + p + " at S" + s + ".")
+ game.pos[p] = s
+ if (set_has(all_power_generals[game.power], p))
+ game.troops[p] = 1
+ game.selected = null
+ game.state = "recruit"
+ },
+}
+
+/* COMBAT */
+
+function goto_combat() {
+ set_clear(game.moved)
+
+ let from = []
+ let to = []
+
+ for (let p of all_power_generals[game.power])
+ if (game.pos[p] < ELIMINATED)
+ set_add(from, game.pos[p])
+
+ for (let p of all_enemy_generals[game.power])
+ if (game.pos[p] < ELIMINATED)
+ set_add(to, game.pos[p])
+
+ game.combat = []
+ for (let a of from) {
+ for (let b of to) {
+ if (set_has(data.cities.adjacent[a], b)) {
+ game.combat.push(a)
+ game.combat.push(b)
+ }
+ }
+ }
+
+ if (game.combat.length > 0)
+ game.state = "combat"
+ else
+ goto_retroactive_conquest()
+}
+
+states.combat = {
+ prompt() {
+ prompt("Combat!")
+ for (let i = 0; i < game.combat.length; i += 2)
+ gen_action_supreme_commander(game.combat[i])
+ },
+ piece(p) {
+ push_undo()
+ game.attacker = game.pos[p]
+ game.state = "combat_target"
+ },
+}
+
+states.combat_target = {
+ prompt() {
+ prompt("Choose enemy stack to fight.")
+ for (let i = 0; i < game.combat.length; i += 2)
+ if (game.combat[i] === game.attacker)
+ gen_action_supreme_commander(game.combat[i+1])
+ },
+ piece(p) {
+ push_undo()
+
+ game.defender = game.pos[p]
+
+ for (let i = 0; i < game.combat.length; i += 2) {
+ if (game.combat[i] === game.attacker && game.combat[i+1] === game.defender) {
+ array_remove_pair(game.combat, i)
+ break
+ }
+ }
+
+ goto_combat_play()
+ },
+}
+
+function set_active_attacker() {
+ game.power = get_stack_power(game.attacker)
+ game.active = current_player()
+}
+
+function set_active_defender() {
+ game.power = get_stack_power(game.defender)
+ game.active = current_player()
+}
+
+function goto_combat_play() {
+ let a_troops = 0
+ let d_troops = 0
+
+ for (let p of all_generals) {
+ if (game.pos[p] === game.attacker)
+ a_troops += game.troops[p]
+ if (game.pos[p] === game.defender)
+ d_troops += game.troops[p]
+ }
+
+ log_br()
+
+ let a = get_supreme_commander(game.attacker)
+ let d = get_supreme_commander(game.defender)
+ log(`P${a} (${a_troops}) at S${game.attacker}`)
+ log(`P${d} (${d_troops}) at S${game.defender}`)
+
+ game.count = a_troops - d_troops
+
+ if (game.count <= 0) {
+ set_active_attacker()
+ game.state = "combat_attack"
+ } else {
+ set_active_defender()
+ game.state = "combat_defend"
+ }
+}
+
+function resume_combat_attack() {
+ if (game.count >= 0) {
+ set_active_defender()
+ game.state = "combat_defend"
+ } else {
+ game.state = "combat_attack"
+ }
+}
+
+function resume_combat_defend() {
+ if (game.count <= 0) {
+ set_active_attacker()
+ game.state = "combat_attack"
+ } else {
+ game.state = "combat_defend"
+ }
+}
+
+function gen_play_card(suit) {
+ let has_suit = false
+ for (let c of game.hand[game.power]) {
+ let c_suit = to_suit(c)
+ if (c_suit === suit) {
+ has_suit = true
+ gen_action_card(c)
+ }
+ else if (c_suit === RESERVE)
+ gen_action_card(c)
+ }
+ return has_suit
+}
+
+states.combat_attack = {
+ prompt() {
+ prompt("Attack: " + game.count)
+ view.selected = [
+ get_supreme_commander(game.attacker),
+ get_supreme_commander(game.defender)
+ ]
+ let has_suit = gen_play_card(get_space_suit(game.attacker))
+ if (game.count === 0 && has_suit)
+ view.actions.pass = 0
+ else
+ view.actions.pass = 1
+ },
+ card(c) {
+ array_remove_item(game.hand[game.power], c)
+ let c_suit = to_suit(c)
+ if (c_suit === RESERVE) {
+ game.state = "combat_attack_reserve"
+ } else {
+ game.count += to_value(c)
+ log(POWER_NAME[game.power] + " C" + c + " = " + (game.count))
+ resume_combat_attack()
+ }
+ },
+ pass() {
+ resolve_combat()
+ },
+}
+
+states.combat_defend = {
+ prompt() {
+ prompt("Defend: " + (-game.count))
+ view.selected = [
+ get_supreme_commander(game.attacker),
+ get_supreme_commander(game.defender)
+ ]
+ let has_suit = gen_play_card(get_space_suit(game.defender))
+ if (game.count === 0 && has_suit)
+ view.actions.pass = 0
+ else
+ view.actions.pass = 1
+ },
+ card(c) {
+ array_remove_item(game.hand[game.power], c)
+ let c_suit = to_suit(c)
+ if (c_suit === RESERVE) {
+ game.state = "combat_defend_reserve"
+ } else {
+ game.count -= to_value(c)
+ log(POWER_NAME[game.power] + " C" + c + " = " + (game.count))
+ resume_combat_defend()
+ }
+ },
+ pass() {
+ resolve_combat()
+ },
+}
+
+states.combat_attack_reserve = {
+ prompt() {
+ prompt("Attack: Choose value. " + game.count)
+ view.selected = [
+ get_supreme_commander(game.attacker),
+ get_supreme_commander(game.defender)
+ ]
+ view.actions.value = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
+ },
+ value(v) {
+ log(POWER_NAME[game.power] + " reserve " + v)
+ game.count += v
+ resume_combat_attack()
+ },
+}
+
+states.combat_defend_reserve = {
+ prompt() {
+ prompt("Defend: Choose value." + (-game.count))
+ view.selected = [
+ get_supreme_commander(game.attacker),
+ get_supreme_commander(game.defender)
+ ]
+ view.actions.value = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
+ },
+ value(v) {
+ log(POWER_NAME[game.power] + " reserve " + v)
+ game.count -= v
+ resume_combat_defend()
+ },
+}
+
+function select_stack(s) {
+ let list = []
+ for (let p of all_generals)
+ if (game.pos[p] === s)
+ list.push(p)
+ return list
+}
+
+function resolve_combat() {
+ if (game.count === 0) {
+ log("Tie.")
+ next_combat()
+ } else if (game.count > 0) {
+ set_active_attacker()
+ game.selected = select_stack(game.defender)
+ goto_retreat()
+ } else {
+ set_active_defender()
+ game.selected = select_stack(game.attacker)
+ goto_retreat()
+ }
+}
+
+function next_combat() {
+ log_br()
+ set_active_current_power()
+ game.count = 0
+ delete game.attacker
+ delete game.defender
+ if (game.combat.length > 0)
+ game.state = "combat"
+ else
+ game.state = "combat_done"
+}
+
+states.combat_done = {
+ prompt() {
+ prompt("Combat done.")
+ view.actions.end_combat = 1
+ },
+ end_combat() {
+ goto_retroactive_conquest()
+ },
+}
+
+/* RETREAT */
+
+function get_winner() {
+ return game.count > 0 ? game.attacker : game.defender
+}
+
+function get_loser() {
+ return game.count < 0 ? game.attacker : game.defender
+}
+
+function goto_retreat() {
+ let hits = Math.abs(game.count)
+
+ let winner = get_winner()
+ let loser = get_loser()
+
+ // no more fighting for the loser
+ for (let i = game.combat.length - 2; i >= 0; i -= 2)
+ if (game.combat[i] === loser || game.combat[i+1] === loser)
+ array_remove_pair(game.combat, i)
+
+ log("P" + get_supreme_commander(loser) + " lost " + hits + " troops.")
+
+ // apply hits
+ for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) {
+ let p = game.selected[i]
+ while (game.troops[p] > 1 && hits > 0) {
+ --game.troops[p]
+ --hits
+ }
+ }
+
+ for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) {
+ let p = game.selected[i]
+ while (game.troops[p] > 0 && hits > 0) {
+ --game.troops[p]
+ --hits
+ }
+ }
+
+ // remove eliminated generals
+ for (let i = game.selected.length - 1; i >= 0 && hits > 0; --i) {
+ let p = game.selected[i]
+ if (game.troops[p] === 0) {
+ log("P" + p + " eliminated.")
+ game.pos[p] = ELIMINATED
+ array_remove(game.selected, i)
+ }
+ }
+
+ if (game.selected.length > 0) {
+ game.retreat = search_retreat(loser, winner, Math.abs(game.count))
+ game.state = "retreat"
+ } else {
+ next_combat()
+ }
+}
+
+// search distances from winner within retreat range
+function search_retreat_distance(from, range) {
+ let seen = [ from, 0 ]
+ let queue = [ from << 4 ]
+ while (queue.length > 0) {
+ let item = queue.shift()
+ let here = item >> 4
+ let dist = (item & 15) + 1
+ for (let next of data.cities.adjacent[here]) {
+ if (map_has(seen, next))
+ continue
+ if (dist <= range) {
+ map_set(seen, next, dist)
+ queue.push((next << 4) | dist)
+ }
+ }
+ }
+ return seen
+}
+
+// search all possible retreat paths of given length
+function search_retreat_possible_dfs(result, seen, here, range) {
+ for (let next of data.cities.adjacent[here]) {
+ if (seen.includes(next))
+ continue
+ if (has_any_piece(next))
+ continue
+ if (range === 1) {
+ set_add(result, next)
+ } else {
+ seen.push(next)
+ search_retreat_possible_dfs(result, seen, next, range - 1)
+ seen.pop()
+ }
+ }
+}
+
+function search_retreat_possible(from, range) {
+ let result = []
+ search_retreat_possible_dfs(result, [from], from, range)
+ return result
+}
+
+function search_retreat(loser, winner, range) {
+ let distance = search_retreat_distance(winner, range + 1)
+ let possible = search_retreat_possible(loser, range)
+
+ let max = 0
+ for (let s of possible) {
+ let d = map_get(distance, s, -1)
+ if (d > max)
+ max = d
+ }
+
+ let result = []
+ for (let s of possible)
+ if (map_get(distance, s, -1) === max)
+ result.push(s)
+ return result
+}
+
+states.retreat = {
+ prompt() {
+ prompt("Retreat loser " + Math.abs(game.count))
+ view.selected = game.selected
+ if (game.retreat.length === 0) {
+ prompt("Eliminate loser.")
+ gen_action_piece(game.selected[0])
+ } else {
+ for (let to of game.retreat)
+ gen_action_space(to)
+ }
+ },
+ space(to) {
+ push_undo()
+ log("Retreated to S" + to + ".")
+ for (let p of game.selected) {
+ game.pos[p] = to
+ }
+ delete game.retreat
+ game.state = "retreat_done"
+ // next_combat()
+ },
+ piece(_) {
+ push_undo()
+ log("Eliminated.")
+ for (let p of game.selected)
+ game.pos[p] = ELIMINATED
+ delete game.retreat
+ game.state = "retreat_done"
+ // next_combat()
+ },
+}
+
+states.retreat_done = {
+ prompt() {
+ prompt("Retreat done.")
+ view.actions.next = 1
+ },
+ next() {
+ next_combat()
+ },
+}
+
+/* RETRO-ACTIVE CONQUEST */
+
+function goto_retroactive_conquest() {
+ delete game.combat
+
+ for (let s of game.retro) {
+ if (is_conquest_space(game.power, s)) {
+ if (!is_protected_from_conquest(s)) {
+ log("Conquered S" + s)
+ set_add(game.conquest, s)
+ }
+ }
+ if (is_reconquest_space(game.power, s)) {
+ if (!is_protected_from_reconquest(s)) {
+ log("Reconquered S" + s)
+ set_delete(game.conquest, s)
+ }
+ }
+ }
+
+ set_clear(game.retro)
+
+ // MARIA: supply is before movement
+
+ goto_supply()
+}
+
+/* SUPPLY */
+
+function search_supply_bfs(from, range) {
+ let seen = [ from ]
+ let queue = [ from << 4 ]
+ while (queue.length > 0) {
+ let item = queue.shift()
+ let here = item >> 4
+ let dist = (item & 15) + 1
+ for (let next of data.cities.adjacent[here]) {
+ if (set_has(seen, next))
+ continue
+ if (has_enemy_piece(next))
+ continue
+ set_add(seen, next)
+ if (dist < range)
+ queue.push((next << 4) | dist)
+ }
+ }
+ return seen
+}
+
+function search_supply(range) {
+ for (let p of all_power_trains[game.power]) {
+ let here = game.pos[p]
+ if (here >= ELIMINATED)
+ continue
+ if (!game.supply)
+ game.supply = search_supply_bfs(here, range)
+ else
+ set_add_all(game.supply, search_supply_bfs(here, range))
+ }
+}
+
+function is_out_of_supply(p) {
+ return (game.oos & (1 << p)) !== 0
+}
+
+function set_out_of_supply(p) {
+ return game.oos |= (1 << p)
+}
+
+function set_in_supply(p) {
+ return game.oos &= ~(1 << p)
+}
+
+function has_supply_line(p) {
+ if (!game.supply)
+ throw "SUPPLY NOT INITIALIZED"
+ let s = game.pos[p]
+ if (set_has(all_home_or_depot_cities[game.power], s))
+ return true
+ if (game.supply && set_has(game.supply, s))
+ return true
+ return false
+}
+
+function should_flip_generals() {
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (set_has(game.moved, p))
+ continue
+ if (is_out_of_supply(p) || !has_supply_line(p))
+ return true
+ }
+ return false
+}
+
+function goto_supply() {
+ set_clear(game.moved)
+ search_supply(6)
+ if (should_flip_generals())
+ game.state = "supply"
+ else
+ end_supply()
+}
+
+states.supply = {
+ prompt() {
+ prompt("Supply")
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] >= ELIMINATED)
+ continue
+ if (set_has(game.moved, p))
+ continue
+ if (is_out_of_supply(p) || !has_supply_line(p))
+ gen_action_supreme_commander(game.pos[p])
+ }
+ },
+ piece(x) {
+ let s = game.pos[x]
+ for (let p of all_power_generals[game.power]) {
+ if (game.pos[p] === s) {
+ set_add(game.moved, p)
+ if (is_out_of_supply(p)) {
+ set_in_supply(p)
+ if (!has_supply_line(p)) {
+ log("P" + p + " eliminated.")
+ game.pos[p] = ELIMINATED
+ } else {
+ log("P" + p + " in supply.")
+ }
+ } else {
+ log("P" + p + " out of supply.")
+ set_out_of_supply(p)
+ }
+ }
+ }
+ if (!should_flip_generals())
+ game.state = "supply_done"
+ },
+}
+
+states.supply_done = {
+ prompt() {
+ prompt("Supply done.")
+ view.actions.end_supply = 1
+ },
+ end_supply() {
+ end_supply()
+ },
+}
+
+function end_supply() {
+ set_clear(game.moved)
+ delete game.supply
+ end_action_stage()
+}
+
+/* CARDS OF FATE */
+
+states.russia_quits_the_game_1 = {
+ prompt() {
+ prompt("Russia quits the game. Remove all Russian pieces.")
+ for (let p of all_power_generals[P_RUSSIA])
+ gen_action_piece(p)
+ for (let p of all_power_trains[P_RUSSIA])
+ gen_action_piece(p)
+ },
+ piece(p) {
+ game.pos[p] = REMOVED
+ if (is_general(p))
+ game.troops[p] = 0
+ if (has_removed_all_pieces(P_RUSSIA))
+ game.state = "russia_quits_the_game_2"
+ },
+}
+
+states.russia_quits_the_game_2 = {
+ prompt() {
+ prompt("Russia quits the game. Retire one Prussian general.")
+ for (let p of all_power_generals[game.power])
+ if (p !== GEN_FRIEDRICH && game.pos[p] < ELIMINATED)
+ gen_action_piece(p)
+ },
+ piece(p) {
+ push_undo()
+ retire_general(p)
+ game.state = "russia_quits_the_game_3"
+ },
+}
+
+states.russia_quits_the_game_3 = {
+ prompt() {
+ prompt("Russia quits the game.")
+ view.actions.done = 1
+ },
+ done() {
+ resume_start_turn()
+ },
+}
+
+states.sweden_quits_the_game_1 = {
+ prompt() {
+ prompt("Sweden quits the game. Remove all Swedish pieces.")
+ for (let p of all_power_generals[P_SWEDEN])
+ gen_action_piece(p)
+ for (let p of all_power_trains[P_SWEDEN])
+ gen_action_piece(p)
+ },
+ piece(p) {
+ game.pos[p] = REMOVED
+ if (is_general(p))
+ game.troops[p] = 0
+ if (has_removed_all_pieces(P_SWEDEN))
+ game.state = "sweden_quits_the_game_2"
+ },
+}
+
+states.sweden_quits_the_game_2 = {
+ prompt() {
+ prompt("Sweden quits the game. Retire one Prussian general.")
+ for (let p of all_power_generals[game.power])
+ if (p !== GEN_FRIEDRICH && game.pos[p] < ELIMINATED)
+ gen_action_piece(p)
+ },
+ piece(p) {
+ push_undo()
+ retire_general(p)
+ game.state = "sweden_quits_the_game_3"
+ },
+}
+
+states.sweden_quits_the_game_3 = {
+ prompt() {
+ prompt("Sweden quits the game.")
+ view.actions.done = 1
+ },
+ done() {
+ resume_start_turn()
+ },
+}
+
+states.france_quits_the_game_1 = {
+ prompt() {
+ prompt("France quits the game. Remove all French pieces.")
+ for (let p of all_power_generals[P_FRANCE])
+ gen_action_piece(p)
+ for (let p of all_power_trains[P_FRANCE])
+ gen_action_piece(p)
+ },
+ piece(p) {
+ game.pos[p] = REMOVED
+ if (is_general(p))
+ game.troops[p] = 0
+ if (has_removed_all_pieces(P_FRANCE))
+ game.state = "france_quits_the_game_2"
+ },
+}
+
+states.france_quits_the_game_2 = {
+ prompt() {
+ prompt("France quits the game. Retire Cumberland.")
+ gen_action_piece(GEN_CUMBERLAND)
+ },
+ piece(p) {
+ retire_general(p)
+ resume_start_turn()
+ },
+}
+
+/* SETUP */
+
+const POWER_FROM_SETUP_STEP_4 = [
+ P_PRUSSIA,
+ P_HANOVER,
+ P_RUSSIA,
+ P_SWEDEN,
+ P_AUSTRIA,
+ P_IMPERIAL,
+ P_FRANCE,
+]
+
+const POWER_FROM_SETUP_STEP_3 = [
+ P_PRUSSIA,
+ P_HANOVER,
+ P_RUSSIA,
+ P_SWEDEN,
+ P_FRANCE,
+ P_AUSTRIA,
+ P_IMPERIAL,
+]
+
+function set_active_setup_power() {
+ if (game.scenario === 3)
+ game.power = POWER_FROM_SETUP_STEP_3[game.step]
+ else
+ game.power = POWER_FROM_SETUP_STEP_4[game.step]
+ game.active = current_player()
+}
+
+const SETUP_POSITION = [
+ // P
+ find_city("Oschatz"),
+ find_city("Oschatz"),
+ find_city("Berlin"),
+ find_city("Strehlen"),
+ find_city("Strehlen"),
+ find_city("Brandenburg"),
+ find_city("Arnswalde"),
+ find_city("Mohrungen"),
+
+ // H
+ find_city("Stade"),
+ find_city("Alfeld"),
+
+ // R
+ find_city("Bydgoszcz"),
+ find_city("Bydgoszcz"),
+ find_city("Łomża"),
+ find_city("Sierpc"),
+
+ // S
+ find_city("Stralsund"),
+
+ // A
+ find_city("Brünn"),
+ find_city("Melnik"),
+ find_city("Melnik"),
+ find_city("Olmütz"),
+ find_city("Tabor"),
+
+ // IA
+ find_city("Hildburghausen"),
+
+ // F
+ find_city("Iserlohn"),
+ find_city("Fulda"),
+ find_city("Iserlohn"),
+
+ // Supply Train
+ find_city("Grünberg"),
+ find_city("Jüterbog"),
+
+ find_city("Gifhorn"),
+
+ find_city("Toruń"),
+ find_city("Warszawa"),
+
+ find_city("Wismar"),
+
+ find_city("Beraun"),
+ find_city("Pardubitz"),
+
+ find_city("Erlangen"),
+
+ find_city("Gemünden"),
+ find_city("Koblenz"),
+]
+
+const SETUP_TROOPS = [
+ /* P (32) */ 0, 0, 0, 0, 0, 0, 0, 0,
+ /* H (02) */ 0, 0,
+ /* R (06) */ 0, 0, 0, 0,
+ /* S (4) */ 4,
+ /* A (30) */ 0, 0, 0, 0, 0,
+ /* IA (6) */ 6,
+ /* F (20) */ 0, 0, 0,
+]
+
+function make_fate_deck() {
+ let deck = []
+ for (let i = 1; i <= 18; ++i)
+ deck.push(i)
+ shuffle_bigint(deck)
+ return deck
+}
+
+function make_seeded_fate_deck() {
+ let deck = []
+
+ for (let i = 1; i <= 18; ++i) {
+ if (i === FC_ELISABETH || i === FC_POEMS || i === FC_AMERICA)
+ continue
+ deck.push(i)
+ }
+ shuffle_bigint(deck)
+
+ let aside = []
+ for (let i = 0; i < 4; ++i)
+ aside.push(deck.pop())
+
+ deck.push(FC_ELISABETH)
+ deck.push(FC_POEMS)
+ deck.push(FC_AMERICA)
+ shuffle_bigint(deck)
+
+ for (let i = 0; i < 4; ++i)
+ deck.push(aside.pop())
+
+ return deck
+}
+
+function make_tactics_deck(n) {
+ let deck = []
+ for (let suit = 0; suit <= 3; ++suit)
+ for (let value = 2; value <= 13; ++value)
+ deck.push((n << 7) | (suit << 4) | value)
+ deck.push((n << 7) | (RESERVE << 4) | 2)
+ deck.push((n << 7) | (RESERVE << 4) | 3)
+ return deck
+}
+
+function make_tactics_discard(n) {
+ return make_tactics_deck(n).filter(c => {
+ for (let pow of all_powers)
+ if (set_has(game.hand[pow], c))
+ return false
+ return true
+ })
+}
+
+exports.setup = function (seed, scenario, options) {
+ game = {
+ seed: seed,
+ undo: [],
+ log: [],
+
+ scenario: 4,
+ state: "setup",
+ active: "Frederick",
+ power: P_PRUSSIA,
+
+ turn: 5,
+ step: 0,
+ clock: null,
+ fate: [],
+ deck: null,
+ hand: [ [], [], [], [], [], [], [] ],
+
+ pos: SETUP_POSITION.slice(),
+ oos: 0,
+ troops: SETUP_TROOPS.slice(),
+ conquest: [],
+
+ moved: [],
+ retro: [],
+
+ selected: [],
+ count: 0,
+ }
+
+ game.scenario = parseInt(options.players) || 4
+
+ if (options.seeded)
+ game.clock = make_seeded_fate_deck()
+ else
+ game.clock = make_fate_deck()
+
+ game.deck = make_tactics_deck(0)
+
+ shuffle_bigint(game.deck)
+
+ log("# " + scenario)
+
+ return game
+}
+
+states.setup = {
+ prompt() {
+ prompt("Setup troops: " + count_used_troops() + " / " + max_power_troops[game.power])
+ let done = true
+ for (let p of all_power_generals[game.power]) {
+ if (game.troops[p] === 0) {
+ gen_action_piece(p)
+ done = false
+ }
+ }
+ if (done)
+ view.actions.end_setup = 1
+ },
+ piece(p) {
+ push_undo()
+ game.selected = select_stack(game.pos[p])
+ game.state = "setup_general"
+ },
+ end_setup() {
+ clear_undo()
+ end_setup()
+ },
+}
+
+states.setup_general = {
+ prompt() {
+ prompt("Setup troops.")
+ view.selected = game.selected
+
+ let n_selected = game.selected.length
+ let n_other = count_unused_generals() - game.selected.length
+ let n_troops = max_power_troops[game.power] - count_used_troops()
+
+ // leave at least 1 for each remaining general
+ let take_max = Math.min(8 * n_selected, n_troops - n_other)
+
+ // leave no more than 8 for each remaining general
+ let take_min = Math.max(1 * n_selected, n_troops - n_other * 8)
+
+ view.actions.value = []
+ for (let i = take_min; i <= take_max; ++i)
+ view.actions.value.push(i)
+ },
+ value(v) {
+ let save = game.selected.length - 1
+ for (let p of game.selected) {
+ let n = Math.min(8, v - save)
+ game.troops[p] = n
+ v -= n
+ --save
+ }
+ game.selected = null
+ game.state = "setup"
+ },
+}
+
+function end_setup() {
+ if (++game.step === 7) {
+ goto_start_turn()
+ } else {
+ set_active_setup_power()
+ if (count_unused_generals() === 0)
+ end_setup()
+ }
+}
+
+/* VIEW */
+
+function mask_troops(player) {
+ let view_troops = []
+ for (let pow of all_powers) {
+ if (player_from_power(pow) === player) {
+ for (let p of all_power_generals[pow])
+ view_troops.push(game.troops[p])
+ } else {
+ for (let p of all_power_generals[pow]) {
+ let s = game.pos[p]
+ if (game.attacker === s || game.defender === s)
+ view_troops.push(game.troops[p])
+ else
+ view_troops.push(0)
+ }
+ }
+ }
+ return view_troops
+}
+
+function mask_hand(player) {
+ let view_hand = []
+ for (let pow of all_powers) {
+ if (player_from_power(pow) === player)
+ view_hand[pow] = game.hand[pow]
+ else
+ view_hand[pow] = game.hand[pow].map(c => c & ~127)
+ }
+ return view_hand
+}
+
+exports.view = function (state, player) {
+ game = state
+ view = {
+ prompt: null,
+ actions: null,
+ log: game.log,
+
+ fate: game.turn <= 5 ? game.turn : game.fate,
+ pos: game.pos,
+ oos: game.oos,
+ conquest: game.conquest,
+ troops: mask_troops(player),
+ hand: mask_hand(player),
+
+ power: game.power,
+ retro: game.retro,
+ }
+
+ if (game.state === "game_over") {
+ view.prompt = game.victory
+ } else if (game.active !== player) {
+ let inactive = states[game.state].inactive || game.state
+ view.prompt = `Waiting for ${POWER_NAME[game.power]} to ${inactive}.`
+ } else {
+ view.actions = {}
+ if (states[game.state])
+ states[game.state].prompt()
+ 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
+}
+
+/* COMMON FRAMEWORK */
+
+function goto_game_over(result, victory) {
+ game.active = "None"
+ game.state = "game_over"
+ game.result = result
+ game.victory = victory
+ log("# Game Over")
+ log(game.victory)
+ return true
+}
+
+function prompt(str) {
+ view.prompt = POWER_NAME[game.power] + ": " + str
+}
+
+exports.action = function (state, _player, action, arg) {
+ game = state
+ let S = states[game.state]
+ if (S && action in S) {
+ S[action](arg)
+ } else {
+ if (action === "undo" && game.undo && game.undo.length > 0)
+ pop_undo()
+ else
+ throw new Error("Invalid action: " + action)
+ }
+ return game
+}
+
+function gen_action(action, argument) {
+ if (view.actions[action] === undefined)
+ view.actions[action] = [ argument ]
+ else
+ set_add(view.actions[action], argument)
+}
+
+function gen_action_piece(p) {
+ gen_action("piece", p)
+}
+
+function gen_action_space(s) {
+ gen_action("space", s)
+}
+
+function gen_action_supreme_commander(s) {
+ let p = get_supreme_commander(s)
+ if (p >= 0)
+ gen_action_piece(p)
+}
+
+function gen_action_space_or_piece(s) {
+ let p = get_top_piece(s)
+ if (p >= 0)
+ gen_action_piece(p)
+ else
+ gen_action_space(s)
+}
+
+function gen_action_card(c) {
+ gen_action("card", c)
+}
+
+function log(msg) {
+ game.log.push(msg)
+}
+
+function log_br() {
+ if (game.log.length > 0 && game.log[game.log.length - 1] !== "")
+ game.log.push("")
+}
+
+/* COMMON LIBRARY */
+
+function clear_undo() {
+ game.undo.length = 0
+}
+
+function push_undo() {
+ if (game.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() {
+ if (game.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 random_bigint(range) {
+ // Largest MLCG that will fit its state in a double.
+ // Uses BigInt for arithmetic, so is an order of magnitude slower.
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ // m = 2**53 - 111
+ return (game.seed = Number(BigInt(game.seed) * 5667072534355537n % 9007199254740881n)) % range
+}
+
+function shuffle_bigint(list) {
+ // Fisher-Yates shuffle
+ for (let i = list.length - 1; i > 0; --i) {
+ let j = random_bigint(i + 1)
+ let tmp = list[j]
+ list[j] = list[i]
+ list[i] = tmp
+ }
+}
+
+// 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
+ }
+}
+
+// Array remove and insert (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
+}
+
+function array_remove_item(array, item) {
+ let n = array.length
+ for (let i = 0; i < n; ++i)
+ if (array[i] === item)
+ return array_remove(array, i)
+}
+
+function array_insert(array, index, item) {
+ for (let i = array.length; i > index; --i)
+ array[i] = array[i - 1]
+ array[index] = item
+}
+
+function array_remove_pair(array, index) {
+ let n = array.length
+ for (let i = index + 2; i < n; ++i)
+ array[i - 2] = array[i]
+ array.length = n - 2
+}
+
+function array_insert_pair(array, index, key, value) {
+ for (let i = array.length; i > index; i -= 2) {
+ array[i] = array[i-2]
+ array[i+1] = array[i-1]
+ }
+ array[index] = key
+ array[index+1] = value
+}
+
+// Set as plain sorted array
+
+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
+ }
+ 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 {
+ array_remove(set, m)
+ return
+ }
+ }
+}
+
+function set_add_all(set, other) {
+ for (let item of other)
+ set_add(set, item)
+}
+
+function set_union(one, two) {
+ let set = []
+ for (let item of one)
+ set_add(set, item)
+ for (let item of two)
+ set_add(set, item)
+ return set
+}
+
+function set_intersect(one, two) {
+ let set = []
+ for (let item of one)
+ if (set_has(two, item))
+ set_add(set, item)
+ return set
+}
+
+// Map as plain sorted array of key/value pairs
+
+function map_has(map, key) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return true
+ }
+ return false
+}
+
+function map_get(map, key, missing) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else
+ return map[(m<<1)+1]
+ }
+ return missing
+}
+
+function map_set(map, key, value) {
+ let a = 0
+ let b = (map.length >> 1) - 1
+ while (a <= b) {
+ let m = (a + b) >> 1
+ let x = map[m<<1]
+ if (key < x)
+ b = m - 1
+ else if (key > x)
+ a = m + 1
+ else {
+ map[(m<<1)+1] = value
+ return
+ }
+ }
+ array_insert_pair(map, a<<1, key, value)
+}