diff options
| author | Botond Hende <nettingman@gmail.com> | 2026-02-18 00:53:31 +0100 |
|---|---|---|
| committer | Botond Hende <nettingman@gmail.com> | 2026-02-18 00:53:31 +0100 |
| commit | 5a866b9b129479de85e892752fd6ede6627cee55 (patch) | |
| tree | 2d616d439232ec112b4750bf118852477819daca | |
| parent | cc6bdc68cb3ed40a6e51f885ea1c7c7a4e5fa6c5 (diff) | |
| -rw-r--r-- | codenames/assets/css/codenames.css | 12 | ||||
| -rw-r--r-- | codenames/assets/js/codenames.js | 249 | ||||
| -rw-r--r-- | codenames/templates/codenames.html.j2 | 79 | ||||
| -rw-r--r-- | codenames/templates/codenames_base.html.j2 | 27 | ||||
| -rw-r--r-- | codenames/templates/codenames_duet.html.j2 | 32 | ||||
| -rw-r--r-- | codenames/templates/codenames_root.html.j2 | 6 | ||||
| -rw-r--r-- | modules/codenames_generate.py | 6 |
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 |
