Skip to content

Commit

Permalink
maze: Implement generation and decoding of tokens, woot
Browse files Browse the repository at this point in the history
  • Loading branch information
Rosuav committed Jun 27, 2024
1 parent b8513a8 commit 56924ee
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 13 deletions.
2 changes: 1 addition & 1 deletion maze.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h1>Maze Generator</h1>
<label>Cols: <input type=number id=cols value=25></label>
<button>Generate</button>
<button id=watch type=button>Generate and watch</button>
<button id=draw type=button hidden>Draw</button> <!-- Hidden by default, available if you mess around -->
<button id=draw type=button hidden>Draw</button> <!-- Hidden by default, available if you call draw() in console -->
</form></p>
<div id=display></div>
</main>
Expand Down
112 changes: 100 additions & 12 deletions maze.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ function generate(fast) {
clearInterval(interval); //Cancel any currently-running generation
const maze = initialize(DOM("#rows").value, DOM("#cols").value);
victory = false;
start = +new Date;
if (fast) return improve_maze(maze, [], 1);
render(maze);
//Attempt to generate the maze in 20 seconds, regardless of size.
Expand All @@ -103,23 +104,65 @@ function generate(fast) {
}
on("submit", "#generate", e => {e.preventDefault(); generate(1);});
on("click", "#watch", e => {e.preventDefault(); generate(0);});

let drawing = null;
on("click", "#draw", e => {
e.preventDefault();
clearInterval(interval); interval = 0;
if (drawing) {
//Generate a rendering token.
const bitlength = 32 - Math.clz32(Math.max(rendered_maze.length, rendered_maze[0].length));
const factor = 1n << BigInt(bitlength);
const arr = [rendered_maze.length, rendered_maze[0].length];
drawing.forEach(d => arr.push(d[0], d[1]));
console.log(arr);
let token = 0n;
arr.forEach(n => token = (token * factor) + BigInt(n));
console.log(bitlength, token);
if (drawing && drawing.length) {
//Generate a rendering token. This is a mess, would be a lot easier in other languages, but
//it's more important for the token to be compact than for it to be parsed quickly.
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"; //Base64-ish alphabet

//For small mazes (up to 64x64), we can store the dimensions and entry column in one 64-bit
//value each. For larger mazes, we need two or even three such tokens. Four is the utter limit.
const n = Math.max(rendered_maze.length, rendered_maze[0].length);
const width = n <= 64 ? 1 : n <= 4096 ? 2 : n <= 262144 ? 3 : 4;
//Note that we store the dimensions minus one, allowing a 64x64 maze to be stored in one byte
//per value. However, it's not worth compacting further than that; we could pack three 4-bit
//values into two tokens, allowing a 16x16 maze to be stored in one less byte. Big whoop.
const arr = [rendered_maze.length - 1, rendered_maze[0].length - 1, drawing[0][1]];
let token = "";
arr.forEach(n => {
for (let i = 1; i < width; ++i) {
token += alphabet[n & 63];
n >>= 6;
}
token += alphabet[n];
});
let lastrow = 0, lastcol = drawing[0][1];
function direction(n) {
const [r1, c1] = drawing[n - 1];
const [r2, c2] = drawing[n];
if (r2 > r1) return "00";
if (r2 < r1) return "01";
if (c2 > c1) return "10";
if (c2 < c1) return "11";
}
//The bulk of the token consists of three directions per byte value. First, render the loose
//0-2 at the start. (We've already rendered the first entry by storing the actual column.)
const loose = (drawing.length - 1) % 3;
if (loose) {
let bits = "";
for (let i = 1; i < loose + 1; ++i) bits += direction(i);
token += alphabet[Number("0b" + bits)];
}
//Now the rest of it, which will be a multiple of three.
for (let i = loose + 1; i < drawing.length; i += 3) {
let bits = "";
for (let j = i; j < i + 3; ++j) bits += direction(j);
token += alphabet[Number("0b" + bits)];
}
//Right. That's all the directions (packed three to a byte). There are two last nuggets that
//you'll need to decode that: the bit length for the initial three values, and the number of
//directions in the first byte. We've capped the bit length at 4 and there can only be 0-2 in
//the first byte, so we actually have a bunch of spare bits here if they're needed.
token = alphabet[width * 4 + loose] + token;
const url = new URL(location); url.hash = token;
console.log(url.toString());
start = +new Date;
improve_maze(rendered_maze, drawing, 1);
//TODO: If the last cell of drawing was on the bottom row, make it the exit.
//TODO: Render the token in base 64 and show it to the user (or at least the console)
drawing = null;
return;
}
Expand All @@ -128,7 +171,52 @@ on("click", "#draw", e => {
render(maze);
drawing = [];
});
generate(1);

function decode_token(token) {
//Inverse of the above token generation.
if (token.length < 3) return;
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
const decode = {};
for (let i = 0; i < alphabet.length; ++i) decode[alphabet[i]] = ("000000" + i.toString(2)).slice(-6);
//The first character gives us our size width and the number of loose directions.
const n = alphabet.indexOf(token[0]);
const width = n >> 2, loose = n & 3;
//We then combine 'width' byte values (six bits each) to get our three starting
//values: rows, cols, and entry point.
const arr = [];
let tok = 1;
for (let i = 0; i < 3; ++i) {
let bits = "";
for (let i = 0; i < width; ++i) bits += decode[token[tok++]];
arr.push(Number("0b" + bits));
}
const maze = initialize(arr[0] + 1, arr[1] + 1);
maze[0][arr[2]] = "wl wr wb ";
victory = false;
const drawing = [[0, arr[2]]];
let bits = loose ? decode[token[tok++]].slice(loose * -2) : "";
while (tok < token.length) bits += decode[token[tok++]];
while (bits !== "") {
const cur = bits.slice(0, 2); bits = bits.slice(2);
let [dr, dc] = drawing[drawing.length - 1];
let r = dr, c = dc, dir, back;
switch (cur) {
case "00": r++; dir = "b"; back = "a"; break;
case "01": r--; dir = "a"; back = "b"; break;
case "10": c++; dir = "r"; back = "l"; break;
case "11": c--; dir = "l"; back = "r"; break;
}
drawing.push([r, c]);
maze[r][c] = "wa wb wl wr ".replace("w" + back + " ", "");
maze[dr][dc] = maze[dr][dc].replace("w" + dir + " ", "");
}
start = +new Date;
improve_maze(maze, drawing, 1);
//TODO: If the last cell of drawing was on the bottom row, make it the exit.
}
if (location.hash.length > 3) decode_token(location.hash.slice(1));
else generate(1);
window.draw = () => DOM("#draw").hidden = false;

let lastmark = null;
on("mousedown", ".grid div", e => mark(+e.match.dataset.r, +e.match.dataset.c));
Expand Down

0 comments on commit 56924ee

Please sign in to comment.