Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/treasure-hunt-game/llms.txt

Use this file to discover all available pages before exploring further.

treasure-maze.js is the foundation of the entire game. It owns the rules: what counts as an open cell, how movement deltas are defined, what rewards different actions earn, and how to find the shortest route through any maze. Every other module imports from it — nothing in this file imports from anywhere else. This makes it the ideal place to understand the game’s core data model before reading the agent or UI layers.

Constants

Action Integers

The four movement directions are plain integers so they can be used as object keys, stored in arrays, and compared with === without any string overhead.
export const LEFT  = 0;
export const UP    = 1;
export const RIGHT = 2;
export const DOWN  = 3;

ACTION_NAMES

Maps each action integer to its human-readable string name. Used in log messages and the agent’s lastMove label.
export const ACTION_NAMES = {
  [LEFT]:  "left",
  [UP]:    "up",
  [RIGHT]: "right",
  [DOWN]:  "down"
};

ACTION_DELTAS

Maps each action integer to a [dr, dc] row/column delta. Applying a delta to a current position gives the destination cell.
export const ACTION_DELTAS = {
  [LEFT]:  [0, -1],
  [UP]:    [-1, 0],
  [RIGHT]: [0,  1],
  [DOWN]:  [1,  0]
};

ORIGINAL_MAZE

The 8×8 fallback maze returned by createRandomMaze if all 70 random-generation attempts produce a solution shorter than 11 moves (fewer than 12 path cells). 1 = open cell, 0 = wall.
export const ORIGINAL_MAZE = [
  [1, 0, 1, 1, 1, 1, 1, 1],
  [1, 0, 1, 1, 1, 0, 1, 1],
  [1, 1, 1, 1, 0, 1, 0, 1],
  [1, 1, 1, 0, 1, 1, 1, 1],
  [1, 1, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 0, 1, 0, 0, 0],
  [1, 1, 1, 0, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 1, 1, 1]
];
The player always starts at [0, 0] (top-left) and the treasure is at [7, 7] (bottom-right).

Utility Functions

cloneMaze(maze)

Returns a deep copy of a maze array. Each row is spread into a new array so mutations to the clone never affect the original.
export function cloneMaze(maze) {
  return maze.map((row) => [...row]);
}
Returns: number[][]

key(row, col)

Serializes a grid coordinate to a string key suitable for use in a Map or Set.
export function key(row, col) {
  return `${row},${col}`;
}
Returns: string — format "row,col", e.g. key(3, 5)"3,5"

sameCell(a, b)

Returns true if two [row, col] pairs refer to the same grid cell.
export function sameCell(a, b) {
  return a[0] === b[0] && a[1] === b[1];
}
Returns: boolean

shuffle(values)

Returns a new array with the same elements in a randomized order using the Fisher-Yates algorithm. The original array is not mutated.
export function shuffle(values) {
  const copy = [...values];
  for (let index = copy.length - 1; index > 0; index -= 1) {
    const swapIndex = Math.floor(Math.random() * (index + 1));
    [copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]];
  }
  return copy;
}
Returns: T[]

isInside(row, col, rows = 8, cols = 8)

Checks whether a coordinate falls within the grid bounds. Defaults to 8×8 but accepts any dimensions.
export function isInside(row, col, rows = 8, cols = 8) {
  return row >= 0 && row < rows && col >= 0 && col < cols;
}
Returns: boolean

openNeighborCount(maze, row, col)

Counts how many of a cell’s four orthogonal neighbors are passable (value 1 and inside the grid). Used by the maze generator to enforce corridor integrity.
export function openNeighborCount(maze, row, col) {
  return Object.values(ACTION_DELTAS).reduce((count, [dr, dc]) => {
    const nextRow = row + dr;
    const nextCol = col + dc;
    return count + (
      isInside(nextRow, nextCol, maze.length, maze[0].length) &&
      maze[nextRow][nextCol] === 1 ? 1 : 0
    );
  }, 0);
}
Returns: number (0–4)

findShortestPath(maze, start, target)

Finds the shortest route between two cells using breadth-first search. The function is used by both createRandomMaze (to validate solution length) and CompetitiveAgent (to compute BFS policy moves every turn).
maze
number[][]
required
The 8×8 grid. 1 = open cell, 0 = wall.
start
[number, number]
default:"[0, 0]"
The [row, col] starting cell.
target
[number, number]
default:"[maze.length - 1, maze[0].length - 1]"
The [row, col] destination cell. Defaults to the bottom-right corner.
Returns: { path: [row, col][], actions: number[] }
  • path — ordered list of [row, col] cells from start to target (inclusive). Empty array if no path exists.
  • actions — ordered list of action integers (LEFT / UP / RIGHT / DOWN) to follow the path. Length is path.length - 1.
If no route exists between start and target, both arrays are empty: { path: [], actions: [] }. Algorithm: Standard BFS. The function expands cells level by level, tracks predecessors in a Map, and reconstructs the path by walking backwards from target once it is dequeued.
// Find the shortest path from [0,0] to [7,7] on the original maze
import { findShortestPath, ORIGINAL_MAZE, ACTION_NAMES } from "./treasure-maze.js";

const { path, actions } = findShortestPath(ORIGINAL_MAZE, [0, 0], [7, 7]);

console.log(`Shortest route: ${actions.length} moves`);
// → "Shortest route: 14 moves"

console.log(actions.map((a) => ACTION_NAMES[a]).join(" → "));
// → "down → down → right → down → ..."

createRandomMaze({ rows, cols, density })

Procedurally generates a solvable 8×8 maze. The generator carves a main path first (guaranteeing connectivity), then adds dead-end branches to reach the target cell density.
rows
number
default:"8"
Number of rows in the output grid.
cols
number
default:"8"
Number of columns in the output grid.
density
number
default:"0.55"
Target proportion of open cells (0–1). Higher values produce more open mazes with more branching dead ends. app.js passes 0.56 for easy/medium and 0.5 for hard.
Returns: { maze: number[][], solution: [row, col][] }
  • maze — the generated 8×8 grid.
  • solution — the BFS shortest path from [0, 0] to [rows-1, cols-1].
The function attempts up to 70 generation passes. It rejects any result whose BFS solution path contains fewer than 12 cells (fewer than 11 moves), ensuring every board is genuinely challenging. If all attempts fail (rare), it falls back to ORIGINAL_MAZE.
import { createRandomMaze } from "./treasure-maze.js";

// Default density (easy/medium)
const { maze, solution } = createRandomMaze();
console.log(`Solution length: ${solution.length - 1} moves`);

// Hard mode: fewer open cells, tighter corridors
const hard = createRandomMaze({ density: 0.5 });

TreasureMaze Class

TreasureMaze is the game environment. It tracks player position, visited cells, cumulative reward, and game status. The act() method is the main interaction point — the player (or agent) passes an action integer and receives a result object describing what happened.

Constructor

new TreasureMaze(maze?, player?, target?)
maze
number[][]
default:"ORIGINAL_MAZE"
The 8×8 grid to use. Cloned on construction so the original is never mutated.
player
[number, number]
default:"[0, 0]"
Starting position. Must sit on an open cell (1), otherwise throws.
target
[number, number]
default:"[maze.length - 1, maze[0].length - 1]"
The treasure cell. Must be open, otherwise throws.

Properties

PropertyTypeDescription
baseMazenumber[][]Cloned copy of the original maze. Never changes after construction.
rowsnumberRow count (always 8 for default mazes).
colsnumberColumn count (always 8 for default mazes).
target[number, number]The treasure cell coordinates.
freeCells[number, number][]All open cells that are not the target.
player[number, number]Current player position (mirrored from state).
state{ row, col, mode }Full current state. mode is "start", "valid", "invalid", or "blocked".
visitedSet<string>Keys of cells the player has already occupied.
totalRewardnumberRunning sum of all rewards received this episode.
minRewardnumberLose threshold: -0.5 * rows * cols (= -32 on 8×8).

Methods


key(row, col)

Serializes a grid coordinate to a "row,col" string key. Delegates directly to the module-level key() utility, so instance methods and standalone utilities produce identical output.
row
number
required
Row index (0-based).
col
number
required
Column index (0-based).
Returns: string — e.g. env.key(3, 5)"3,5"

isTarget(row, col)

Returns true if the given cell is the treasure cell.
row
number
required
Row index (0-based).
col
number
required
Column index (0-based).
Returns: boolean

isInside(row, col)

Bounds-checks a coordinate against the environment’s own rows and cols dimensions. Unlike the module-level isInside(), this instance method uses this.rows and this.cols — no extra arguments required.
row
number
required
Row index to check.
col
number
required
Column index to check.
Returns: boolean

canStandOn(row, col)

Returns true if the cell is both within bounds (isInside) and open (baseMaze[row][col] === 1). Used internally by validActions(), updateState(), and the constructor validation.
row
number
required
Row index to check.
col
number
required
Column index to check.
Returns: boolean

reset(player?)

Resets the environment to a fresh episode. Clears visited cells, sets totalReward to 0, and repositions the player.
player
[number, number]
default:"[0, 0]"
Starting position for the new episode.
Returns: number[] — the initial observation from observe().
const env = new TreasureMaze(maze);
const initialObs = env.reset([0, 0]);
// initialObs is a 64-element flat array

validActions(cell?)

Returns the list of action integers that would move to an open cell from the given position.
cell
[number, number] | null
default:"null"
The cell to check from. Defaults to the current state position.
Returns: number[] — subset of [LEFT, UP, RIGHT, DOWN].

updateState(action)

Applies an action to the internal state. Marks the current cell as visited, then moves the player if the action is valid or sets mode to "invalid" or "blocked". Returns: void

getReward()

Reads the current state and returns the scalar reward for the most recent move.
ConditionReward
Player is on target1.0
No valid moves ("blocked")minReward - 1 (≤ -33)
Cell already visited-0.25
Invalid move (hit wall)-0.75
Valid move to new cell-0.04
Returns: number

act(action)

The primary interaction method. Calls updateState, computes the reward, accumulates it into totalReward, and returns a full result object.
action
number
required
One of LEFT (0), UP (1), RIGHT (2), DOWN (3).
Returns:
{
  envState:    number[],   // 64-element flat observation array
  reward:      number,     // reward for this move
  status:      "win" | "lose" | "not_over",
  state:       { row, col, mode },
  totalReward: number      // cumulative reward this episode
}
const env = new TreasureMaze(maze);
env.reset();

const result = env.act(RIGHT); // move right
console.log(result.reward);    // -0.04 (valid new cell)
console.log(result.status);    // "not_over"

gameStatus()

Determines whether the game is still running. Returns: "win" | "lose" | "not_over"
  • "win" — player is on the target cell.
  • "lose"totalReward < minReward (player wasted too many moves).
  • "not_over" — game is still in progress.

observe()

Produces a 64-element flat array snapshot of the current board. Open cells are 1, walls are 0, and the player’s current cell is 0.5. Returns: number[] — length 64 (8 rows × 8 cols).

snapshot(extra?)

Returns a plain object capturing the full environment state. Used by app.js during render() to pass both environment and agent state to the DOM renderer in one call.
extra
object
default:"{}"
Additional properties merged into the snapshot object. app.js passes { agent: agent.snapshot() }.
Returns:
{
  maze:        number[][],        // cloneMaze(baseMaze)
  visited:     Set<string>,       // copy of visited keys
  player:      [number, number],  // [state.row, state.col]
  target:      [number, number],
  status:      "win" | "lose" | "not_over",
  totalReward: number,
  state:       { row, col, mode },
  // ...any extra properties spread in
}

Build docs developers (and LLMs) love