summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBotond Hende <nettingman@gmail.com>2026-02-18 00:53:31 +0100
committerBotond Hende <nettingman@gmail.com>2026-02-18 00:53:31 +0100
commit5a866b9b129479de85e892752fd6ede6627cee55 (patch)
tree2d616d439232ec112b4750bf118852477819daca
parentcc6bdc68cb3ed40a6e51f885ea1c7c7a4e5fa6c5 (diff)
codenames duet generator functionalHEADmaster
-rw-r--r--codenames/assets/css/codenames.css12
-rw-r--r--codenames/assets/js/codenames.js249
-rw-r--r--codenames/templates/codenames.html.j279
-rw-r--r--codenames/templates/codenames_base.html.j227
-rw-r--r--codenames/templates/codenames_duet.html.j232
-rw-r--r--codenames/templates/codenames_root.html.j26
-rw-r--r--modules/codenames_generate.py6
7 files changed, 294 insertions, 117 deletions
diff --git a/codenames/assets/css/codenames.css b/codenames/assets/css/codenames.css
index db3fb71..30ec0ee 100644
--- a/codenames/assets/css/codenames.css
+++ b/codenames/assets/css/codenames.css
@@ -26,3 +26,15 @@ div.red {
div.blue {
background: blue;
}
+
+.game-mode-0,
+.game-mode-1,
+.game-mode-2 {
+ display: none;
+}
+
+html[data-game-mode="0"] .game-mode-0,
+html[data-game-mode="1"] .game-mode-1,
+html[data-game-mode="2"] .game-mode-2 {
+ display: inherit;
+} \ No newline at end of file
diff --git a/codenames/assets/js/codenames.js b/codenames/assets/js/codenames.js
index c838b48..c4c5f46 100644
--- a/codenames/assets/js/codenames.js
+++ b/codenames/assets/js/codenames.js
@@ -1,11 +1,13 @@
"use strict"
+const GAME_MODE_KEY = "data-game-mode";
+
const Tile = Object.freeze({
- NEUTRAL: 0,
- BLACK: 1,
- GREEN: 2,
- RED: 3,
- BLUE: 4,
+ NEUTRAL: 0,
+ BLACK: 1,
+ GREEN: 2,
+ RED: 3,
+ BLUE: 4,
});
function cyrb128(str) {
@@ -81,6 +83,7 @@ function getPosIndexByValue(array, pos) {
return -1;
}
+// TODO: handle multiple rounds and memoize
function generateDuetGrids(width, height, greenCount, blackCount, commonGreenCount, commonBlackCount, blackGreenCount, seed) {
let seed_array = cyrb128(seed);
let rand = sfc32(seed_array[0], seed_array[1], seed_array[2], seed_array[3]);
@@ -153,59 +156,211 @@ function generateDuetGrids(width, height, greenCount, blackCount, commonGreenCou
return [p1, p2];
}
-function parseAndGenerateDuet()
+function isValidUrl(params) {
+ if (!params.has("gm") || !params.has("w") || !params.has("h") || !params.has("b") || !params.has("s")) {
+ return false;
+ }
+
+ switch (params.get("gm")) {
+ case "0":
+ if (!params.has("rb")) {
+ return false;
+ }
+ break;
+ case "1":
+ if (!params.has("cg") || !params.has("cb") || !params.has("bg")) {
+ return false;
+ }
+ // fallthrough
+ case "2":
+ if (!params.has("g") || !params.has("p")) {
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+function parseUrlAndGenerate()
{
let params = new URLSearchParams(window.location.search);
- if (params.has("w") && params.has("h") && params.has("g") && params.has("b") &&
- params.has("cg") && params.has("cb") && params.has("bg") && params.has("s")) {
- return generateDuetGrids(+params.get("w"), +params.get("h"), +params.get("g"), +params.get("b"),
- +params.get("cg"), +params.get("cb"), +params.get("bg"), params.get("s"));
+ if (!isValidUrl(params))
+ return null;
+
+ switch (params.get("gm")) {
+ case "1":
+ return generateDuetGrids(+params.get("w"), +params.get("h"), +params.get("g"), +params.get("b"),
+ +params.get("cg"), +params.get("cb"), +params.get("bg"), decodeURI(params.get("s")));
}
return null;
}
-function generate_for_current_page() {
- const duetP1 = document.getElementById("duet_p1");
- const duetP2 = document.getElementById("duet_p2");
- if (duetP1 !== null || duetP2 !== null) {
- let grids = parseAndGenerateDuet();
- if (grids == null) {
- return;
- }
+function drawMap(grid, map, increment) {
+ let rowIdx = (increment > 0) ? 0 : map.length - 1;
+ let rowEndIdx = (increment > 0) ? map.length : -1;
- const gridRoot = duetP1;
- let grid = grids[0];
+ while (rowIdx !== rowEndIdx) {
+ let row = map[rowIdx];
+ let rowDiv = document.createElement("div");
+ rowDiv.classList.add("tile_row");
+ grid.appendChild(rowDiv);
- for (let ii = 0; ii < grid.length; ii++) {
- let row = grid[ii];
- let rowDiv = document.createElement("div");
- rowDiv.classList.add("tile_row");
- gridRoot.appendChild(rowDiv);
- for (let jj = 0; jj < row.length; jj++) {
- let tileDiv = document.createElement("div");
- tileDiv.classList.add("tile");
- rowDiv.appendChild(tileDiv);
- switch (grid[ii][jj]) {
- case Tile.NEUTRAL:
- tileDiv.classList.add("neutral");
- break;
- case Tile.BLACK:
- tileDiv.classList.add("black");
- break;
- case Tile.GREEN:
- tileDiv.classList.add("green");
- break;
- case Tile.RED:
- tileDiv.classList.add("red");
- break;
- case Tile.BLUE:
- tileDiv.classList.add("blue");
- break;
- }
+ let columnIdx = (increment > 0) ? 0 : row.length - 1;
+ let columnEndIdx = (increment > 0) ? row.length : -1;
+
+ while (columnIdx !== columnEndIdx) {
+ let tileDiv = document.createElement("div");
+ tileDiv.classList.add("tile");
+ rowDiv.appendChild(tileDiv);
+ switch (map[rowIdx][columnIdx]) {
+ case Tile.NEUTRAL:
+ tileDiv.classList.add("neutral");
+ break;
+ case Tile.BLACK:
+ tileDiv.classList.add("black");
+ break;
+ case Tile.GREEN:
+ tileDiv.classList.add("green");
+ break;
+ case Tile.RED:
+ tileDiv.classList.add("red");
+ break;
+ case Tile.BLUE:
+ tileDiv.classList.add("blue");
+ break;
}
+
+ columnIdx += increment;
+ }
+
+ rowIdx += increment;
+ }
+}
+
+function generateMapsFromUrl() {
+ const grid1 = document.getElementById("grid_p1");
+ const grid2 = document.getElementById("grid_p2");
+
+ if (grid1 != null) {
+ grid1.innerHTML = "";
+ }
+ if (grid2 != null) {
+ grid2.innerHTML = "";
+ }
+
+ let generatedMaps = parseUrlAndGenerate();
+ if (generatedMaps == null) {
+ return;
+ }
+
+ let player = new URLSearchParams(window.location.search).get("p");
+ if (player === "0" || player === "2") {
+ drawMap(grid1, generatedMaps[0], 1);
+ }
+
+ if (player === "1" || player === "2") {
+ drawMap(grid2, generatedMaps[1], -1);
+ }
+}
+
+function getPlayer() {
+ return document.querySelector('input[name="p"]:checked').value;
+}
+
+function getGameIntParam(key) {
+ return parseInt(document.getElementById(key).value) || 0;
+}
+
+// TODO: warn about bad inputs
+function parseInputAndSetUrl() {
+ let params = new URLSearchParams();
+ let gamemode = document.documentElement.getAttribute(GAME_MODE_KEY);
+ params.append("gm", gamemode);
+ params.append("s", encodeURI(document.getElementById("s").value));
+ params.append("w", getGameIntParam("w"));
+ params.append("h", getGameIntParam("h"));
+ params.append("b", getGameIntParam("b"));
+ switch (gamemode) {
+ case "0": // NORMAL
+ params.append("rb", getGameIntParam("rb"));
+ break;
+
+ case "1": // DUET
+ params.append("cg", getGameIntParam("cg"));
+ params.append("cb", getGameIntParam("cb"));
+ params.append("bg", getGameIntParam("bg"));
+ // fallthrough
+
+ case "2": // DUET MAYHEM
+ params.append("g", getGameIntParam("g"));
+ params.append("p", getPlayer());
+ break;
+ }
+
+ history.pushState({}, "", window.location.origin + window.location.pathname + "?" + params.toString());
+ return true;
+}
+
+function onGenerateButton() {
+ if (parseInputAndSetUrl()) {
+ generateMapsFromUrl();
+ }
+}
+
+function onSetGameMode(mode) {
+ document.documentElement.setAttribute(GAME_MODE_KEY, mode)
+ if (mode === 0) {
+ document.getElementById("b").value = 1;
+ }
+ else {
+ document.getElementById("b").value = 3;
+ }
+}
+
+function onRandomSeed() {
+ let result = '';
+ const characters =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ const charactersLength = characters.length;
+ for (let i = 0; i < 10; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+
+ document.getElementById("s").value = result;
+}
+
+function setParamsFromUrl(params) {
+ params.entries().forEach(function (item) {
+ if (item[0] === "s") {
+ document.getElementById("s").value = decodeURI(item[1]);
}
+ else if (item[0] === "gm") {
+ onSetGameMode(parseInt(item[1]));
+ }
+ else if (item[0] === "p") {
+ document.getElementById("p" + item[1]).click();
+ }
+ else {
+ document.getElementById(item[0]).value = parseInt(item[1]);
+ }
+ })
+}
+
+function initPage() {
+ let params = new URLSearchParams(window.location.search);
+ if (isValidUrl(params)) {
+ setParamsFromUrl(params);
+ generateMapsFromUrl();
+ }
+ else {
+ onSetGameMode(0);
+ onRandomSeed();
+ onGenerateButton();
}
}
-generate_for_current_page(); \ No newline at end of file
+initPage(); \ No newline at end of file
diff --git a/codenames/templates/codenames.html.j2 b/codenames/templates/codenames.html.j2
new file mode 100644
index 0000000..0e1e17a
--- /dev/null
+++ b/codenames/templates/codenames.html.j2
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html dir="ltr" lang="en">
+<head>
+ <meta charset="utf-8"/>
+ <title>{{ title }}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
+ <meta property="og:title" content="{{ title }}" />
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content="{{ url }}" />
+ <link rel="stylesheet" href="{{ site.assets_path }}/css/codenames.css" />
+ <link rel="stylesheet" href="{{ site.assets_path_static }}/css/bootstrap-grid.min.css" />
+</head>
+<body>
+<div class="bootstrap-wrapper">
+ <div class="container">
+ <div class="row">
+ <main class="col-md-9">
+ <div id="grid_p1"></div>
+ <div id="grid_p2"></div>
+ <button onclick="onSetGameMode(0)">Normal</button>
+ <button onclick="onSetGameMode(1)">Duet</button>
+ <button onclick="onSetGameMode(2)">Duet Mayhem</button>
+ <br>
+ <label for="s">Seed:</label>
+ <input type="text" name="s" id="s"/>
+ <button onclick="onRandomSeed()">Random</button>
+ <br>
+ <label>Size:</label>
+ <input type="number" name="w" id="w" value="5"/>
+ <span>x</span>
+ <input type="number" name="h" id="h" value="5"/>
+ <br>
+ <span class="game-mode-0">
+ <label for="rb">Red/blue tiles:</label>
+ <input type="number" name="rb" id="rb" value="8"/>
+ <label>(+1 for the other team)</label>
+ <br>
+ </span>
+ <span class="game-mode-1 game-mode-2">
+ <label for="g">Green tiles (per person):</label>
+ <input type="number" name="g" id="g" value="9"/>
+ <br>
+ </span>
+ <span class="game-mode-1">
+ <label for="cg">Shared green tiles:</label>
+ <input type="number" name="cg" id="cg" value="3"/>
+ <br>
+ </span>
+ <label for="b">Black tiles:</label>
+ <input type="number" name="b" id="b" value="3"/>
+ <br>
+ <span class="game-mode-1">
+ <label for="cb">Shared black tiles:</label>
+ <input type="number" name="cb" id="cb" value="1"/>
+ <br>
+ <label for="bg">Black-green tiles:</label>
+ <input type="number" name="bg" id="bg" value="1"/>
+ <br>
+ </span>
+ <!-- TODO: EXTRA INFO WITH CALCULATED VALUES -->
+ <span class="game-mode-1 game-mode-2">
+ <input type="radio" id="p0" name="p" value="0" checked="checked">
+ <label for="p0">Player1</label>
+ <input type="radio" id="p1" name="p" value="1">
+ <label for="p1">Player2</label>
+ <input type="radio" id="p2" name="p" value="2">
+ <label for="p2">Both</label>
+ <br>
+ </span>
+ <button onclick="onGenerateButton()">Generate</button>
+ </main>
+ </div>
+ </div>
+</div>
+</body>
+<footer>
+ <script src="/assets/js/codenames.js"></script>
+</footer>
+</html>
diff --git a/codenames/templates/codenames_base.html.j2 b/codenames/templates/codenames_base.html.j2
deleted file mode 100644
index e6751f5..0000000
--- a/codenames/templates/codenames_base.html.j2
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html dir="ltr" lang="en">
-<head>
- <meta charset="utf-8"/>
- <title>{{ title }}</title>
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
- <meta property="og:title" content="{{ title }}" />
- <meta property="og:type" content="website" />
- <meta property="og:url" content="{{ url }}" />
- <link rel="stylesheet" href="{{ site.assets_path }}/css/codenames.css" />
- <link rel="stylesheet" href="{{ site.assets_path_static }}/css/bootstrap-grid.min.css" />
-</head>
-<body>
-<div class="bootstrap-wrapper">
- <div class="container">
- <div class="row">
- <main class="col-md-9">
- {% block content required %}{% endblock %}
- </main>
- </div>
- </div>
-</div>
-</body>
-<footer>
- <script src="/assets/js/codenames.js"></script>
-</footer>
-</html>
diff --git a/codenames/templates/codenames_duet.html.j2 b/codenames/templates/codenames_duet.html.j2
deleted file mode 100644
index f5cd94b..0000000
--- a/codenames/templates/codenames_duet.html.j2
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "codenames_base.html.j2" %}
-{% block content %}
-<div id="duet_p1"></div>
-<div id="duet_p2"></div>
-<form action="{{ url }}" method="get">
- <label for="s">Seed:</label>
- <input type="text" name="s" id="s" required />
- <button>Random</button>
- <br>
- <label>Size:</label>
- <input type="number" name="w" id="w" value="5" required />
- <span>x</span>
- <input type="number" name="h" id="h" value="5" required />
- <br>
- <label for="g">Green tiles (per person):</label>
- <input type="number" name="g" id="g" value="9" required />
- <br>
- <label for="cg">Shared green tiles:</label>
- <input type="number" name="cg" id="cg" value="3" required />
- <br>
- <label for="b">Black tiles:</label>
- <input type="number" name="b" id="b" value="3" required />
- <br>
- <label for="cb">Shared black tiles:</label>
- <input type="number" name="cb" id="cb" value="1" required />
- <br>
- <label for="bg">Black-green tiles:</label>
- <input type="number" name="bg" id="bg" value="1" required />
- <br>
- <button type="submit">Generate</button>
-</form>
-{% endblock %} \ No newline at end of file
diff --git a/codenames/templates/codenames_root.html.j2 b/codenames/templates/codenames_root.html.j2
deleted file mode 100644
index 0cbbce7..0000000
--- a/codenames/templates/codenames_root.html.j2
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends "codenames_base.html.j2" %}
-{% block content %}
- <a href="/classic.html">Classic/Images</a>
- <br>
- <a href="/duet.html">Duet</a>
-{% endblock %} \ No newline at end of file
diff --git a/modules/codenames_generate.py b/modules/codenames_generate.py
index 55dac9e..de51561 100644
--- a/modules/codenames_generate.py
+++ b/modules/codenames_generate.py
@@ -21,12 +21,8 @@ def generate(jinja_env: jinja2.Environment, output_root_path: str, local: bool):
"codenames_url": Config.CODENAMES_ROOT_URL,
})
- root_template = jinja_env.get_template("codenames_root.html.j2")
+ root_template = jinja_env.get_template("codenames.html.j2")
with open(os.path.join(output_root_path, "index.html"), "w") as f:
f.write(root_template.render({"ctx" : Config.CODENAMES_ROOT_URL, "title": Config.CODENAMES_NAME}))
- duet_template = jinja_env.get_template("codenames_duet.html.j2")
- with open(os.path.join(output_root_path, "duet.html"), "w") as f:
- f.write(duet_template.render({"ctx" : Config.CODENAMES_ROOT_URL + "duet.html", "title": Config.CODENAMES_NAME}))
-
shutil.copytree(Config.CODENAMES_ASSETS_SOURCE_DIR, output_root_path + Config.ASSETS_IMPORT_PATH) \ No newline at end of file