<div class="controls">
<input
id="key"
value="fc94b0c1e5b0987c5843997697ee9fb7"
size="32"
type="text"
pattern="[:0-9a-fA-F]+"
/>
<button id="draw">Draw</button>
<button id="random">Get random key</button>
</div>
<div class="centered">
<div id="display"></div>
</div>
body {
background: #333;
color: white;
font-family: Consolas, Monaco, monospace;
font-size: 24px;
}
* { font: inherit }
#key { display: block; }
#key:invalid { color: red; }
#display {
white-space: pre;
}
.centered {
display: flex;
justify-content: center;
align-items: center;
margin: 1em 0;
}
// Algorithm description & collision analysis
// http://www.dirk-loss.de/sshvis/drunken_bishop.pdf
const WIDTH = 17;
const HEIGHT = 9;
// the symbols representing the number of coins on a cell
const SYMBOLS = ' .o+=*BOX@%&#/^SE';
// the rules of movement given a command between 0 and 3, inclusive
const MOVES = [
{ x: -1, y: -1 }, // ↖
{ x: 1, y: -1 }, // ↗
{ x: -1, y: 1 }, // ↙
{ x: 1, y: 1 } // ↘
];
// ensures the returned value is always min <= x <= max
const clamp = (min, max, x) =>
Math.max(min, Math.min(max, x));
const nextPosition = (position, move) => {
// look up direction to move in the rules lookup
const delta = MOVES[move];
// return a new position by clamping the move to the grid
return {
x: clamp(0, WIDTH-1, position.x + delta.x),
y: clamp(0, HEIGHT-1, position.y + delta.y)
};
};
// we split an integer between 0 and 255 into 2-bit numbers
// by extracting pairs of bits from a number, starting with
// the least significant bit:
//
// a9 = 10 10 10 01 => [01, 10, 10, 10]
// ^ ^ ^ ^ ^ ^ ^ ^
// # 4 3 2 1 1 2 3 4
const splitByteIntoCommand = byte => ([
byte & 3,
(byte >>> 2) & 3,
(byte >>> 4) & 3,
(byte >>> 6) & 3
]);
const parseCommands = (hexString) => {
const commands = [];
// loop over all the characters in the hex string
for (let i = 0; i < hexString.length; i += 2) {
// take a pair of hex characters each time (one byte == 2 chars)
const value = parseInt(hexString.slice(i, i + 2), 16);
// split the byte into 4 double-bit numbers and append them to
// the list of commands
commands.push(...splitByteIntoCommand(value));
}
return commands;
}
const step = (world, position, command) => {
// create a copy of the world state
const newWorld = Array.from(world);
// drop a coin in the current position
newWorld[position.y * WIDTH + position.x] += 1;
// return the new world state and the next position
return [newWorld, nextPosition(position, command)];
}
const simulate = (commands, steps = commands.length) => {
// start in the middle of the grid
const start = { x: 8, y: 4 };
// set the inital position to the starting position
let position = start;
// make the initial world empty
let world = Array(WIDTH * HEIGHT).fill(0);
// loop over the requested number of steps
for (let i = 0; i < steps; i++)
// calculate the next world state and position
[world, position] = step(world, position, commands[i]);
// remember the last position calculated
const end = position;
// set the starting position to 15 (S)
world[start.y * WIDTH + start.x] = 15;
// set the ending position to 16 (E)
world[end.y * WIDTH + end.x] = 16;
return world;
}
const draw = (world, width, height) => {
const drawing = world
.map(cell => SYMBOLS[cell % SYMBOLS.length])
.join('');
const result = ['+' + '-'.repeat(width) + '+'];
for (let i = 0; i < height; i++)
result.push('|' + drawing.slice(i * width, (i + 1) * width) + '|');
result.push('+' + '-'.repeat(width) + '+');
return result.join('\n');
}
const run = (commands, steps = 0) => {
// simlate the world for some steps
const world = simulate(commands, steps);
// draw it
displayDiv.textContent = draw(world, WIDTH, HEIGHT, `${steps} steps`) + `\n${steps} steps`;
// if there are more steps to draw, wait until the next animation frame
// to draw one more step than previously
if (steps < commands.length)
requestAnimationFrame(() => run(commands, steps + 1));
};
const randomHexString = () => {
let key = '';
for (let i = 0; i < 32; i++)
key += Math.floor(Math.random() * 16).toString(16)
return key;
}
// the UI stuff
const displayDiv = document.getElementById('display');
const drawButton = document.getElementById('draw');
const randomButton = document.getElementById('random');
const keyInput = document.getElementById('key');
randomButton.addEventListener('click', () => {
keyInput.value = randomHexString();
});
drawButton.addEventListener('click', () => {
const key = keyInput.value.replace(/[^a-fA-F0-9]/g, '').padStart(32, '0').slice(0, 32);
const commands = parseCommands(key);
run(commands, 0);
});
// draw the initial key animation
run(parseCommands(keyInput.value), 0);
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.