diff options
-rw-r--r-- | about.html | 3 | ||||
-rw-r--r-- | info/charts.html | 54 | ||||
-rw-r--r-- | info/legend.html | 12 | ||||
-rw-r--r-- | map.jpg | bin | 0 -> 674398 bytes | |||
-rw-r--r-- | map.png | bin | 0 -> 4462084 bytes | |||
-rw-r--r-- | play.html | 242 | ||||
-rw-r--r-- | play.js | 84 | ||||
-rw-r--r-- | rules.js | 207 |
8 files changed, 521 insertions, 81 deletions
@@ -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 & 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> Binary files differBinary files differ@@ -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 – + FLN – <span class="role_user"></span> </div> </div> - <div class="role" id="role_Blue"> + <div class="role" id="role_FRA"> <div class="role_name"> - Blue – + FRA – <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> @@ -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") @@ -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) } |