<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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.