automata/langtons-ant.mjs

/**
 * @module automata
 */

/**
 * Creates an unlimited, dynamically expanding 2D grid using nested Maps.
 * Ideal for sparse matrices or algorithms operating on an infinite plane (e.g., Langton's Ant, Game of Life).
 * 
 * @returns {Object} An object containing `get(x, y)` and `set(x, y, val)` methods.
 * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMYwKYEM4YKoB2ANuCLgCYDiMI5CAvggGYxRgIBEAAgOYDWAV1RgQGGAC8A9ALggiAZw4BuYMHTZchEiIrVaKoA|▶ Try it live in Instacode}
 */
export const createUnlimitedGrid = () => {
  const grid = new Map();

  const getRow = y => {
    let row;
    if (!(row = grid.get(y))) {
      row = new Map();
      grid.set(y, row);
    }
    return row;
  };
  
  const set = (x, y, val) => {
    const row = getRow(y);
    row.set(x, val);
  };
  
  const get = (x, y) => {
    const row = getRow(y);
    return row.get(x) || 0;
  };
  
  return { set, get };
};

/**
 * Initializes a Langton's Ant automaton on a given grid.
 * The ant follows standard rules: turns right on 0, turns left on 1, and flips the cell state.
 * 
 * @param {Object} grid - The grid interface containing `get(x, y)` and `set(x, y, val)` methods.
 * @param {number} startX - The initial X coordinate of the ant.
 * @param {number} startY - The initial Y coordinate of the ant.
 * @param {number} [initialDir=-1] - The initial direction (0: TOP, 1: RIGHT, 2: BOTTOM, 3: LEFT). If -1, a random direction is chosen.
 * @returns {Object} An object containing a `step()` method which advances the simulation by one tick and returns `{x, y, state}` of the modified cell.
 * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMYwKYEM4YDJYB2A5nFIQM4CChiAvggGYxRgIBEAAsQNYCuqMCAwwAXgHo+cEABsK7ANzBg6bLgIkylGnCVA|▶ Try it live in Instacode}
 */
export const createLangtonsAnt = (grid, startX, startY, initialDir = -1) => {
  const [X, Y] = [0, 1];
  const [TOP, RIGHT, BOT, LEFT] = [0, 1, 2, 3];
  const coordMap = new Map([
    [TOP,   [ 0,-1]],
    [RIGHT, [ 1, 0]],
    [BOT,   [ 0, 1]],
    [LEFT,  [-1, 0]],
  ]);
  
  let dir = initialDir !== -1 ? initialDir : Math.floor(Math.random() * 4);
  const pos = [startX, startY];
  
  const step = () => {
    const currentState = grid.get(pos[X], pos[Y]);
    const nextState = currentState === 0 ? 1 : 0;
    
    grid.set(pos[X], pos[Y], nextState);
    
    if (currentState === 0) {
      dir = (dir + 1) % 4; // Turn Right
    } else {
      dir = (dir + 3) % 4; // Turn Left
    }
    
    const move = coordMap.get(dir);
    pos[X] += move[X];
    pos[Y] += move[Y];
    
    return {
      x: pos[X] - move[X],
      y: pos[Y] - move[Y],
      state: nextState
    };
  };
  
  return { step };
};