summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--about.html3
-rw-r--r--info/charts.html54
-rw-r--r--info/legend.html12
-rw-r--r--map.jpgbin0 -> 674398 bytes
-rw-r--r--map.pngbin0 -> 4462084 bytes
-rw-r--r--play.html242
-rw-r--r--play.js84
-rw-r--r--rules.js207
8 files changed, 521 insertions, 81 deletions
diff --git a/about.html b/about.html
index 06831ff..75a46fb 100644
--- a/about.html
+++ b/about.html
@@ -10,6 +10,7 @@ Algeria makes the transition from being the 10th department of France to nationa
Game Design by Brian Train.
<ul>
-<li><a href="/algeria/info/algeria_en_v1.2.pdf">Rulebook</a>
+<li><a href="/algeria/info/algeria_en_v1.2.pdf">Rules</a>
+<li><a href="/algeria/info/legend.html">Legend</a>
<li><a href="/algeria/info/charts.html">Charts</a>
</ul>
diff --git a/info/charts.html b/info/charts.html
index 201024b..ccb2618 100644
--- a/info/charts.html
+++ b/info/charts.html
@@ -1,12 +1,64 @@
<!DOCTYPE html>
<title>Algeria Chart</title>
<style>
-body { max-width: 50rem; margin: 2rem; }
+html,body{margin:0;padding:0}
+main { max-width: 600px; margin: 0 auto; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid black; padding: 0 5px; }
th { background-color: gainsboro; }
</style>
<body>
+<main>
<h1>Algeria Charts</h1>
+<h2>Combat Results Table</h2>
+
+<p>
+<table>
+<tr><th>Die Roll<th>1<th>2-4<th>5-8<th>9-15<th>16-24<th>25-36<th>37-50<th>51+
+<tr><td>1<td>-<td>-<td>-<td>-<td>1<td>1<td>1<td>2
+<tr><td>2<td>-<td>-<td>-<td>1<td>1<td>1<td>2<td>2
+<tr><td>3<td>-<td>-<td>1<td>1<td>2<td>2<td>2<td>3
+<tr><td>4<td>-<td>1<td>1<td>2<td>2<td>2<td>3<td>4
+<tr><td>5<td>1<td>1<td>2<td>2<td>2<td>3<td>4<td>4
+<tr><td>6<td>1<td>2<td>2<td>2<td>3<td>4<td>4<td>5
+</table>
+
+<h2>FLN AP Cost &amp; Sources</h2>
+
+<p>
+<table>
+<tr><th>Activity<th style="width: 50px;">APs<th>Units<th>Comments
+<tr><td>Build<td>3 (2)<td>0<td>non-neutralized Front needed; only Cadres or Companies may be build. Only 2 AP to build in Morocco or Tunisia
+<tr><td>Augment<td>3<td>1 Cadre<td>Cadre augments to Front; only one Front per area and not in Remote
+<tr><td>Harass<td>0<td>1 Company<td>roll on Combat Results Table; French fire back at half firepower. May do any number in area per turn but Companies attack singly.
+<tr><td>Propaganda<td>1<td>1 (any unit will do)<td>any FLN unit will do; one per area, not in Remote
+<tr><td>Strike (urban only)<td>5<td>1 Front + Cadres<td>non-neutralized Front needed; each Cadre assisting gives +1 DRM
+<tr><td>Intimidate<td>3<td>1 (any mobile unit)<td>only one area per turn
+<tr><td>Movement<td>0<td>1 (any mobile unit)<td>units attempt movement one at a time
+</table>
+
+<p>
+<table>
+<tr><th>Source<th colspan="3">APs Received
+<tr><td>Areas under FLN Control (-1 AP if area is terrorized)
+<td><strong>Urban:</strong>
+<ul><li>5 if controlled<li>2 if contested but non-neutralized FLN units are present</ul>
+<td><strong>Rural:</strong>
+<ul><li>2 if controlled<li>1 if contested but non-neutralized FLN units are present</ul>
+<td><strong>Remote:</strong>
+<ul><li>0</ul>
+<tr><td>Foreign Governments<td colspan="3">per random event: AP arrive by sea and some may be intercepted by French Navy (11.7)
+<tr><td>FLN PSL<td colspan="3">AP = 10% of current FLN PSL (round fractions down)
+</td></tr>
+</table>
+
+<h2>Mission Success</h2>
+
+<p>
+<table>
+<tr><th>Die Roll<td>-1<td>0<td>1<td>2<td>3<td>4<td>5<td>6<td>7<td>8
+<tr><th>Result<td>0+<td>0+<td>1+<td>1<td>1<td>2<td>2<td>3@<td>4@<td>5@
+</table>
+</main>
</body>
diff --git a/info/legend.html b/info/legend.html
new file mode 100644
index 0000000..d762aeb
--- /dev/null
+++ b/info/legend.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Algeria Legend</title>
+<style>
+html,body{margin:0;padding:0}
+main { max-width: 600px; margin: 0 auto; }
+img{display:block;margin:12px auto;max-width:100%}
+</style>
+<body>
+<main>
+<img src="legend.png" alt="[Algeria Legend]">
+</main>
+</body>
diff --git a/map.jpg b/map.jpg
new file mode 100644
index 0000000..da42e1e
--- /dev/null
+++ b/map.jpg
Binary files differ
diff --git a/map.png b/map.png
new file mode 100644
index 0000000..dbc22ca
--- /dev/null
+++ b/map.png
Binary files differ
diff --git a/play.html b/play.html
index bddb39b..726f03d 100644
--- a/play.html
+++ b/play.html
@@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">
<meta charset="UTF-8">
<title>ALGERIA</title>
-<link rel="icon" href="favicon.png">
<link rel="stylesheet" href="/fonts/fonts.css">
<link rel="stylesheet" href="/common/play.css">
<script defer src="/common/play.js"></script>
@@ -14,19 +13,21 @@
main { background-color: dimgray }
-#role_Red .role_name { background-color: salmon; }
-#role_Blue .role_name { background-color: skyblue; }
+#role_FLN .role_name { background-color: #006633; color: white;}
+#role_FRA .role_name { background-color: #002654; color: white; }
#mapwrap {
- width: 1100px;
- height: 850px;
+ width: 1400px;
+ height: 824px;
box-shadow: 0px 1px 10px #0008;
}
#map {
- width: 1100px;
- height: 850px;
- background-image: url(map100.png);
+ width: 1400px;
+ height: 824px;
+ background-size: 1400px 824px;
+ background-position: center;
+ background-image: url(map.jpg);
}
#map div {
@@ -36,42 +37,96 @@ main { background-color: dimgray }
transition-timing-function: ease;
}
-.token {
- width: 58px;
- height: 64px;
- background-size: 58px 64px;
- filter: drop-shadow(0px 2px 4px #0008);
-}
-
-.tile {
- width: 75px;
- height: 75px;
+.counter {
+ width: 68px;
+ height: 68px;
background-repeat: no-repeat;
- background-size: 50px 50px;
+ background-size: 68px 68px;
background-position: center;
border-width: 2px;
border-style: solid;
box-shadow: 0 0 0 1px #222, 1px 2px 4px #0008;
}
-.token.white { background-image: url(images/token_white.svg) }
-.token.red { background-image: url(images/token_red.svg) }
-.token.blue { background-image: url(images/token_blue.svg) }
+.counter-map {
+ width: 48px;
+ height: 48px;
+ background-size: 48px 48px;
+ border-width: 1px;
+}
+
+.counter-med {
+ width: 35px;
+ height: 35px;
+ background-size: 35px 35px;
+ border-width: 1px;
+}
+
+.counter-mini {
+ width: 24px;
+ height: 24px;
+ background-size: 24px 24px;
+ border-width: 1px;
+}
+
+.selected {
+ border-color: yellow;
+}
+
+.counter.fr_al_5_mob_inf { background-image: url(counters/fr_al_5_mob_inf.png) }
+.counter.fr_al_7_mob_arm { background-image: url(counters/fr_al_7_mob_arm.png) }
+.counter.fr_al_static { background-image: url(counters/fr_al_static.png) }
+.counter.fr_div_7_inf { background-image: url(counters/fr_div_7_inf.png) }
+.counter.fr_div_25 { background-image: url(counters/fr_div_25.png) }
+.counter.fr_elite_9_inf { background-image: url(counters/fr_elite_9_inf.png) }
+.counter.fr_elite_9_mar { background-image: url(counters/fr_elite_9_mar.png) }
+.counter.fr_elite_9_para { background-image: url(counters/fr_elite_9_para.png) }
+.counter.fr_elite_10 { background-image: url(counters/fr_elite_10.png) }
+.counter.fr_reg_6_inf { background-image: url(counters/fr_reg_6_inf.png) }
+
+.counter.fr_al_5_mob_inf.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_al_5_mob_inf.png) }
+.counter.fr_al_7_mob_arm.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_al_7_mob_arm.png) }
+.counter.fr_al_static.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_al_static.png) }
+.counter.fr_div_7_inf.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_div_7_inf.png) }
+.counter.fr_div_25.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_div_25.png) }
+.counter.fr_elite_9_inf.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_elite_9_inf.png) }
+.counter.fr_elite_9_mar.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_elite_9_mar.png) }
+.counter.fr_elite_9_para.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_elite_9_para.png) }
+.counter.fr_elite_10.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_elite_10.png) }
+.counter.fr_reg_6_inf.neutralized { background-image: url(counters/neutralized.png), url(counters/fr_reg_6_inf.png) }
+
+.counter.front { background-image: url(counters/front.png) }
+.counter.cadre { background-image: url(counters/cadre.png) }
+.counter.company { background-image: url(counters/company.png) }
-.tile.white { background-image: url(images/tile_white.png) }
-.tile.red { background-image: url(images/tile_red.png) }
-.tile.blue { background-image: url(images/tile_blue.png) }
-.tile.gold { background-image: url(images/tile_gold.png) }
-.tile.green { background-image: url(images/tile_green.png) }
+.counter.front.neutralized { background-image: url(counters/neutralized.png), url(counters/front.png) }
+.counter.cadre.neutralized { background-image: url(counters/neutralized.png), url(counters/cadre.png) }
+.counter.company.neutralized { background-image: url(counters/neutralized.png), url(counters/company.png) }
-.tile.red { background-color: hsl(0,90%,49%); border-color: hsl(0,90%,59%) hsl(0,90%,39%) hsl(0,90%,39%) hsl(0,90%,59%); }
-.tile.white { background-color: hsl(0,0%,94%); border-color: hsl(0,0%,100%) hsl(0,0%,84%) hsl(0,0%,84%) hsl(0,0%,100%); }
-.tile.blue { background-color: hsl(201,80%,47%); border-color: hsl(201,80%,57%) hsl(201,80%,37%) hsl(201,80%,37%) hsl(201,80%,57%); }
-.tile.gold { background-color: hsl(50,81%,59%); border-color: hsl(50,81%,69%) hsl(50,81%,49%) hsl(50,81%,49%) hsl(50,81%,69%); }
-.tile.green { background-color: hsl(125,21%,33%); border-color: hsl(125,21%,43%) hsl(125,21%,23%) hsl(125,21%,23%) hsl(125,21%,43%); }
+.counter.psl-fln { background-image: url(counters/psl.png) }
+.counter.psl-fra { background-image: url(counters/psl_fr.png) }
+.counter.ap { background-image: url(counters/ap.png) }
+
+.counter.naval-pts { background-image: url(counters/naval.png) }
+.counter.helo-pts-max { background-image: url(counters/helo_max.png) }
+.counter.helo-pts-avail { background-image: url(counters/helo_avail.png) }
+.counter.air-pts-max { background-image: url(counters/air_max.png) }
+.counter.air-pts-avail { background-image: url(counters/air_avail.png) }
+
+.counter.border_zone { background-image: url(counters/border.png)}
+.counter.remote { background-image: url(counters/remote.png)}
+.counter.terror { background-image: url(counters/terror.png)}
+
+.counter.control_fln { background-image: url(counters/control.png)}
+.counter.control_fra { background-image: url(counters/control_fr.png)}
+.counter.control_neut { background-image: url(counters/neut.png)}
+
+.counter.oas_fln { background-image: url(counters/oas_fln.png)}
+.counter.oas_fra { background-image: url(counters/oas_fr.png)}
+.counter.oas { background-image: url(counters/oas.png)}
.panel {
- max-width: 1100px;
+ max-width: 900px;
margin: 36px auto;
background-color: #555;
}
@@ -90,7 +145,29 @@ main { background-color: dimgray }
justify-content: start;
flex-wrap: wrap;
padding: 20px;
- gap: 20px;
+ gap: 14px;
+}
+
+.track {
+ position: relative;
+}
+
+.track div {
+ position: absolute;
+}
+
+#psl_track {
+ width: 868px;
+ height: 821px;
+ background-size: 868px 821px;
+ background-image: url(psl_track.png);
+}
+
+#air_naval_track {
+ width: 813px;
+ height: 112px;
+ background-size: 813px 112px;
+ background-image: url(air-helo_track.png);
}
</style>
@@ -102,8 +179,10 @@ main { background-color: dimgray }
<div class="menu">
<div class="menu_title"><img src="/images/cog.svg"></div>
<div class="menu_popup">
- <a class="menu_item" href="info/rules.html" target="_blank">Rules</a>
+ <a class="menu_item" href="info/algeria_en_v1.2.pdf" target="_blank">Rules</a>
+ <a class="menu_item" href="info/legend.html" target="_blank">Legend</a>
<a class="menu_item" href="info/charts.html" target="_blank">Charts</a>
+
<div class="resign menu_separator"></div>
<div class="resign menu_item" onclick="confirm_resign()">Resign</div>
</div>
@@ -116,15 +195,15 @@ main { background-color: dimgray }
<aside>
<div id="roles">
- <div class="role" id="role_Red">
+ <div class="role" id="role_FLN">
<div class="role_name">
- Red &#x2013;
+ FLN &#x2013;
<span class="role_user"></span>
</div>
</div>
- <div class="role" id="role_Blue">
+ <div class="role" id="role_FRA">
<div class="role_name">
- Blue &#x2013;
+ FRA &#x2013;
<span class="role_user"></span>
</div>
</div>
@@ -137,30 +216,85 @@ main { background-color: dimgray }
<div id="mapwrap" class="">
<div id="map">
-<div id="token_white" class="token white" style="left:90px;top:135px;"></div>
-<div id="token_red1" class="token red" style="left:200px"></div>
-<div id="token_red2" class="token red" style="left:300px"></div>
-<div id="token_red3" class="token red" style="left:400px"></div>
-<div id="token_blue1" class="token blue" style="left:500px"></div>
-<div id="token_blue2" class="token blue" style="left:600px"></div>
-<div id="token_blue3" class="token blue" style="left:700px"></div>
+<div class="counter counter-map fr_div_7_inf selected" style="left:125px;top:187px;"></div>
+<div class="counter counter-map fr_div_7_inf neutralized" style="left:184px;top:187px;"></div>
+
+<div class="counter counter-map oas" style="left:680px;top:85px;"></div>
+<div class="counter counter-map oas_fln" style="left:740px;top:85px;"></div>
+<div class="counter counter-map oas_fra" style="left:800px;top:85px;"></div>
+
+<div class="counter counter-mini control_fln" style="left:200px"></div>
+<div class="counter counter-mini control_fra" style="left:240px"></div>
+<div class="counter counter-mini control_neut" style="left:280px"></div>
+<div class="counter counter-mini terror" style="left:1000px;top:10px;"></div>
+<div class="counter counter-mini remote" style="left:1000px;top:60px;"></div>
+
+<div class="counter counter-med border_zone" style="left:20px;top:530px;"></div>
+<div class="counter counter-med border_zone" style="left:1350px;top:250px;"></div>
+
+</div>
+</div>
+
+<div id="fra_supply_panel" class="panel">
+<div id="fra_supply_header" class="panel_header">French Supply</div>
+<div id="fra_supply" class="panel_body">
+
+<div class="counter fr_div_7_inf"></div>
+<div class="counter fr_div_25"></div>
+<div class="counter fr_reg_6_inf"></div>
+
+<div class="counter fr_elite_9_inf"></div>
+<div class="counter fr_elite_9_mar"></div>
+<div class="counter fr_elite_9_para"></div>
+
+<div class="counter fr_elite_10"></div>
+
+<div class="counter fr_al_5_mob_inf"></div>
+<div class="counter fr_al_7_mob_arm"></div>
+<div class="counter fr_al_static"></div>
</div>
</div>
-<div id="hand_panel" class="panel">
-<div id="hand_header" class="panel_header">Hand</div>
-<div id="hand" class="panel_body">
+<div id="fln_supply_panel" class="panel">
+<div id="fln_supply_header" class="panel_header">FLN Supply</div>
+<div id="fln_supply" class="panel_body">
-<div class="tile red" style="left:198px;top:300px"></div>
-<div class="tile blue" style="left:338px;top:300px"></div>
-<div class="tile green" style="left:478px;top:300px"></div>
-<div class="tile white" style="left:618px;top:300px"></div>
-<div class="tile gold" style="left:758px;top:300px"></div>
+<div class="counter front"></div>
+<div class="counter cadre"></div>
+<div class="counter company"></div>
</div>
</div>
+<div id="psl_panel" class="panel">
+<div id="psl_header" class="panel_header">PSL Track</div>
+<div id="psl" class="panel_body">
+<div id="psl_track" class="track">
+
+<div class="counter ap" style="left:20px;top:15px"></div>
+<div class="counter psl-fln" style="left:20px;top:414px"></div>
+<div class="counter psl-fra" style="left:441px;top:494px"></div>
+
+</div>
+</div>
+</div>
+
+<div id="air_naval_panel" class="panel">
+<div id="air_naval_header" class="panel_header">Air / Naval Track</div>
+<div id="air_naval" class="panel_body">
+<div id="air_naval_track" class="track">
+
+<div class="counter naval-pts" style="left:171px;top:29px"></div>
+<div class="counter helo-pts-max" style="left:338px;top:10px"></div>
+<div class="counter helo-pts-avail" style="left:478px;top:10px"></div>
+<div class="counter air-pts-max" style="left:618px;top:10px"></div>
+<div class="counter air-pts-avail" style="left:758px;top:10px"></div>
+
+</div>
+</div>
+</div>
+
</main>
<footer id="status"></footer>
diff --git a/play.js b/play.js
new file mode 100644
index 0000000..4ebf306
--- /dev/null
+++ b/play.js
@@ -0,0 +1,84 @@
+"use strict"
+
+/* global view, player, send_action, action_button, scroll_with_middle_mouse */
+
+let ui = {
+ board: document.getElementById("map"),
+}
+
+let action_register = []
+
+function register_action(e, action, id) {
+ e.my_action = action
+ e.my_id = id
+ e.onmousedown = on_click_action
+ action_register.push(e)
+}
+
+function on_click_action(evt) {
+ if (evt.button === 0)
+ if (send_action(evt.target.my_action, evt.target.my_id))
+ evt.stopPropagation()
+}
+
+function is_action(action, arg) {
+ if (arg === undefined)
+ return !!(view.actions && view.actions[action] === 1)
+ return !!(view.actions && view.actions[action] && view.actions[action].includes(arg))
+}
+
+function create(t, p, ...c) {
+ let e = document.createElement(t)
+ Object.assign(e, p)
+ e.append(c)
+ if (p.my_action)
+ register_action(e, p.my_action, p.my_id)
+ return e
+}
+
+function create_item(p) {
+ let e = create("div", p)
+ ui.board.appendChild(e)
+ return e
+}
+
+let on_init_once = false
+
+function on_init() {
+ if (on_init_once)
+ return
+ on_init_once = true
+}
+
+function on_update() {
+ on_init()
+
+ for (let e of action_register)
+ e.classList.toggle("action", is_action(e.my_action, e.my_id))
+
+ action_button("roll", "Roll")
+ action_button("done", "Done")
+ action_button("undo", "Undo")
+}
+
+
+function on_log(text) {
+ let p = document.createElement("div")
+ if (text.match(/^\.r /)) {
+ text = text.substring(3)
+ p.className = 'h1 r'
+ }
+ else if (text.match(/^\.b /)) {
+ text = text.substring(3)
+ p.className = 'h1 b'
+ }
+ else if (text.match(/^\.x /)) {
+ text = text.substring(3)
+ p.className = 'h1 x'
+ }
+
+ p.innerHTML = text
+ return p
+}
+
+scroll_with_middle_mouse("main")
diff --git a/rules.js b/rules.js
index 8bc5bf9..dae8237 100644
--- a/rules.js
+++ b/rules.js
@@ -1,22 +1,24 @@
"use strict"
-const RED = "Red"
-const BLUE = "Blue"
+const FLN = "FLN"
+const FRA = "FRA"
-var game, states
+var states = {}
+var game = null
+var view = null
exports.scenarios = [ "Standard" ]
-exports.roles = [ RED, BLUE ]
+exports.roles = [ FLN, FRA ]
-exports.setup = function (seed, scenario, options) {
- game = {
- seed: seed,
- state: null,
- log: [],
- undo: [],
- }
- return game
+function gen_action(action, argument) {
+ if (!(action in view.actions))
+ view.actions[action] = []
+ view.actions[action].push(argument)
+}
+
+function gen_action_token(token) {
+ gen_action("token", token)
}
exports.action = function (state, player, action, arg) {
@@ -24,26 +26,17 @@ exports.action = function (state, player, action, arg) {
let S = states[game.state]
if (action in S)
S[action](arg, player)
+ else if (action === "undo" && game.undo && game.undo.length > 0)
+ pop_undo()
else
throw new Error("Invalid action: " + action)
return game
}
-exports.resign = function (state, player) {
- game = state
- if (game.state !== 'game_over') {
- if (player === RED)
- goto_game_over(BLUE, "Red resigned.")
- if (player === BLUE)
- goto_game_over(RED, "Blue resigned.")
- }
- return game
-}
-
exports.view = function(state, player) {
game = state
- let view = {
+ view = {
log: game.log,
prompt: null,
}
@@ -62,5 +55,169 @@ exports.view = function(state, player) {
view.actions.undo = 0
}
- return view;
+ return view
+}
+
+exports.resign = function (state, player) {
+ game = state
+ if (game.state !== 'game_over') {
+ if (player === FLN)
+ goto_game_over(FRA, "FLN resigned.")
+ if (player === FRA)
+ goto_game_over(FLN, "France resigned.")
+ }
+ return game
+}
+
+function goto_game_over(result, victory) {
+ game.state = "game_over"
+ game.active = "None"
+ game.result = result
+ game.victory = victory
+ log("")
+ log(game.victory)
+ return false
+}
+
+states.game_over = {
+ prompt() {
+ view.prompt = game.victory
+ },
+}
+
+// === PREPARATION ===
+
+exports.setup = function (seed, scenario, options) {
+ game = {
+ seed: seed,
+ state: null,
+ log: [],
+ undo: [],
+ }
+
+ game.active = RED
+ goto_random_event()
+
+ return game
+}
+
+// === FLOW OF PLAY ===
+
+function goto_random_event() {
+ game.state = "random_event"
+}
+
+states.random_event = {
+ inactive: "to roll for a random event",
+ prompt() {
+ view.prompt = "Roll for a random event."
+ gen_action("roll")
+ },
+ roll() {
+ clear_undo()
+ let rnd = 10 * roll_d6() + roll_d6()
+ log("Random event roll " + rnd)
+ // goto_reinforcement_phase()
+ },
+}
+
+function goto_reinforcement_phase() {
+ game.state = "reinforcement"
+}
+
+// === COMMON LIBRARY ===
+
+function log(msg) {
+ game.log.push(msg)
+}
+
+function clear_undo() {
+ game.undo.length = 0
+}
+
+function push_undo() {
+ let copy = {}
+ for (let k in game) {
+ let v = game[k]
+ if (k === "undo")
+ continue
+ else if (k === "log")
+ v = v.length
+ else if (typeof v === "object" && v !== null)
+ v = object_copy(v)
+ copy[k] = v
+ }
+ game.undo.push(copy)
+}
+
+function pop_undo() {
+ let save_log = game.log
+ let save_undo = game.undo
+ game = save_undo.pop()
+ save_log.length = game.log
+ game.log = save_log
+ game.undo = save_undo
+}
+
+function random(range) {
+ // An MLCG using integer arithmetic with doubles.
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ // m = 2**35 − 31
+ return (game.seed = game.seed * 200105 % 34359738337) % range
+}
+
+function shuffle(list) {
+ // Fisher-Yates shuffle
+ for (let i = list.length - 1; i > 0; --i) {
+ let j = random(i + 1)
+ let tmp = list[j]
+ list[j] = list[i]
+ list[i] = tmp
+ }
+}
+
+function roll_d6() {
+ return random(6) + 1;
+}
+
+// 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)
}