Back to Browser Side Quest
SOURCE / PINNED RELEASE

Made of little things.

Browser Side Quest

Release
b9d42f5768e4…
Author-recorded commit
c267690145fa…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/browser-side-quest

Archive hash verified: 6d4b90b8a5f328f7…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/shared/utils.ts
// Port of BrowserQuest server/js/utils.js (MPL-2.0).
import { Orientations, type Orientation } from './gametypes.js';

export const random = (range: number): number => Math.floor(Math.random() * range);

export const randomRange = (min: number, max: number): number => min + Math.random() * (max - min);

export const randomInt = (min: number, max: number): number =>
  min + Math.floor(Math.random() * (max - min + 1));

export const clamp = (min: number, max: number, value: number): number =>
  value < min ? min : value > max ? max : value;

export function randomOrientation(): Orientation {
  switch (random(4)) {
    case 0:
      return Orientations.LEFT;
    case 1:
      return Orientations.RIGHT;
    case 2:
      return Orientations.UP;
    default:
      return Orientations.DOWN;
  }
}

/** Chebyshev distance, as used by the original server. */
export function distanceTo(x: number, y: number, x2: number, y2: number): number {
  const distX = Math.abs(x - x2);
  const distY = Math.abs(y - y2);
  return distX > distY ? distX : distY;
}

const ESCAPES: Record<string, string> = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#39;',
};

/**
 * The original server stripped unsafe tags and HTML-escaped player text.
 * Text is rendered with textContent in this port, so we only strip tags,
 * control characters and surrounding whitespace.
 */
export function sanitize(value: string): string {
  return value
    .replace(/<[^>]*>/g, '')
    .replace(/[\u0000-\u001f\u007f]/g, '')
    .trim();
}

export function escapeHtml(value: string): string {
  return value.replace(/[&<>"']/g, (c) => ESCAPES[c]);
}

/** 1-based tile index to grid position, exactly as in the original maps. */
export function tileIndexToGridPosition(tileNum: number, width: number): { x: number; y: number } {
  const getX = (num: number, w: number): number => {
    if (num === 0) return 0;
    return num % w === 0 ? w - 1 : (num % w) - 1;
  };
  tileNum -= 1;
  return { x: getX(tileNum + 1, width), y: Math.floor(tileNum / width) };
}

export function gridPositionToTileIndex(x: number, y: number, width: number): number {
  return y * width + x + 1;
}