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.

app.js is the only module that touches the browser. It queries every interactive DOM element on startup, attaches keyboard and button event listeners, and owns the race timer that fires the agent’s moves on a setInterval. Every frame it rebuilds the 8×8 grid by calling env.snapshot() and agent.snapshot(), applies CSS classes cell by cell, and syncs seven HUD labels — all without any framework, virtual DOM, or reactive system. The module is loaded as a native ES module so the browser resolves its imports from treasure-maze.js and q-agent.js automatically.

Module Imports

import {
  ACTION_NAMES, DOWN, LEFT, RIGHT, TreasureMaze, UP,
  createRandomMaze, findShortestPath, key
} from "./treasure-maze.js";

import { CompetitiveAgent, DIFFICULTY_SETTINGS } from "./q-agent.js";

DOM Elements

On startup, app.js queries all interactive and display elements with document.querySelector. All IDs are defined in index.html.
VariableSelectorPurpose
board#maze-boardContainer for the 64 cell <div> elements.
statusPill#game-statusStatus badge (Ready, Race Running, You Found the X, etc.).
rewardLabel#reward-labelDisplays env.totalReward to two decimal places.
playerStepsLabel#player-steps-labelDisplays the player’s step count.
agentStepsLabel#agent-steps-labelDisplays the agent’s step count.
difficultyLabel#difficulty-labelDisplays the active difficulty label from DIFFICULTY_SETTINGS.
pathLengthLabel#path-length-labelDisplays BFS shortest-route length (moves, not cells).
difficultySelect#difficulty-select<select> for Easy / Medium / Hard.
difficultyDescription#difficulty-descriptionNarrative text from DIFFICULTY_SETTINGS[difficulty].description.
startButton#start-raceStarts the race (disabled while race is running).
newMazeButton#new-mazeGenerates a fresh randomized maze.
resetButton#reset-raceResets env and agent on the same maze.
trainingLog#training-log<ol> list; newest entries are prepended and the list is capped at 9 items.
playerLeadLabel#player-lead-labelShows relative step lead ("Even", "Player +2", "Agent +3").
agentMoveLabel#agent-move-labelShows agent.lastMove (e.g. "exploited right", "hesitated").

Input Handling

Keyboard (keydown)

The keydown listener on document maps both arrow keys and WASD to the four action integers. Single-character keys are lowercased before lookup to normalise A/a.
const keyMap = {
  ArrowLeft: LEFT,  ArrowUp: UP,  ArrowRight: RIGHT,  ArrowDown: DOWN,
  a:         LEFT,  w:       UP,  d:          RIGHT,  s:         DOWN
};

document.addEventListener("keydown", (event) => {
  const keyName = event.key.length === 1 ? event.key.toLowerCase() : event.key;
  if (keyName in keyMap) {
    event.preventDefault();
    movePlayer(keyMap[keyName]);
  }
});
event.preventDefault() stops the browser from scrolling when the arrow keys are used.

Button Controls ([data-action])

All four directional buttons in the HTML carry a data-action attribute ("up", "down", "left", "right"). A single delegated listener maps them through actionMap:
const actionMap = { left: LEFT, up: UP, right: RIGHT, down: DOWN };

document.querySelectorAll("[data-action]").forEach((button) => {
  button.addEventListener("click", () => movePlayer(actionMap[button.dataset.action]));
});

Difficulty Select (#difficulty-select)

Changing the select triggers a non-logged race reset followed by a difficulty reconfiguration:
difficultySelect.addEventListener("change", () => {
  resetRace(false);
  configureDifficulty();
  log(`Difficulty changed to ${DIFFICULTY_SETTINGS[difficultySelect.value].label}.`);
});
configureDifficulty() reads the new value, updates the description text, and calls agent.configure(...) so the agent immediately adopts the new speed and behavior settings.

Key Functions

render()

Rebuilds the entire #maze-board grid on every call. Clears board.innerHTML, calls env.snapshot({ agent: agent.snapshot() }) to get a single consistent state object, then iterates all 64 cells and applies CSS classes based on what occupies each cell.
function render() {
  board.innerHTML = "";
  const snapshot = env.snapshot({ agent: agent.snapshot() });
  const playerKey = key(snapshot.player[0],      snapshot.player[1]);
  const agentKey  = key(snapshot.agent.position[0], snapshot.agent.position[1]);
  const targetKey = key(snapshot.target[0],      snapshot.target[1]);

  snapshot.maze.forEach((row, rowIndex) => {
    row.forEach((cellValue, colIndex) => {
      const cell    = document.createElement("div");
      const cellKey = key(rowIndex, colIndex);
      cell.className = "cell";
      // ... class application and icon injection
      board.append(cell);
    });
  });

  updateHud();
}

CSS Class Mapping

CSS ClassApplied When
wallcellValue === 0 (blocked cell)
visitedsnapshot.visited.has(cellKey) (player has been here)
agent-trailsnapshot.agent.trail.has(cellKey) (agent has been here)
treasurecellKey === targetKey
playercellKey === playerKey
agentcellKey === agentKey
bothcellKey === playerKey && cellKey === agentKey (player and agent on same cell)
Each cell also receives a <span class="cell-icon"> whose textContent is set to "X" for the treasure, "🤖" for the agent, "●" for the player, or "●🤖" when both share a cell.

updateHud()

Synchronises all seven HUD labels in one call. Called at the end of every render().
function updateHud() {
  const optimalPath = findShortestPath(mazeData.maze, playerStart, target);
  const lead = agent.steps - playerSteps;
  playerStepsLabel.textContent  = String(playerSteps);
  agentStepsLabel.textContent   = String(agent.steps);
  rewardLabel.textContent       = env.totalReward.toFixed(2);
  difficultyLabel.textContent   = DIFFICULTY_SETTINGS[difficultySelect.value].label;
  pathLengthLabel.textContent   = String(Math.max(optimalPath.path.length - 1, 0));
  playerLeadLabel.textContent   = lead === 0 ? "Even"
    : lead > 0 ? `Player +${lead}` : `Agent +${Math.abs(lead)}`;
  agentMoveLabel.textContent    = agent.lastMove;
}

startRace()

Starts the race by scheduling the agent’s first move and then setting up the interval timer. If the race was previously over, resetRace(false) is called first.
function startRace() {
  if (raceOver) resetRace(false);
  raceActive = true;
  startButton.disabled = true;
  setStatus("running");

  stopAgentTimer();
  window.setTimeout(() => {
    if (!raceActive || raceOver) return;
    moveAgentOnce();
    raceTimer = window.setInterval(moveAgentOnce, agent.settings.moveDelay);
  }, agent.settings.startDelay);
}

Race Timer Flow

startRace()

  └─ window.setTimeout(startDelay)

          ├─ moveAgentOnce()          ← first agent move

          └─ window.setInterval(moveDelay)

                  ├─ moveAgentOnce()  ← tick 2
                  ├─ moveAgentOnce()  ← tick 3
                  └─ ...until endRace() calls clearInterval
startDelay and moveDelay come directly from agent.settings, so selecting a harder difficulty makes the first move arrive sooner and subsequent moves arrive faster.

movePlayer(action)

Called by every keyboard and button input handler. Only fires if the race is active and not over.
function movePlayer(action) {
  if (!raceActive || raceOver) return;
  const result = env.act(action);
  playerSteps += result.state.mode === "invalid" ? 0 : 1;
  log(`Player ${ACTION_NAMES[action]}: reward ${result.reward.toFixed(2)}, total ${result.totalReward.toFixed(2)}.`);
  render();
  checkWinner();
}
Invalid moves (hitting a wall) do not increment playerSteps but do apply the -0.75 reward penalty to totalReward inside env.act().

moveAgentOnce()

Called by the race timer on every setInterval tick.
function moveAgentOnce() {
  if (!raceActive || raceOver) return;
  agent.move();
  render();
  checkWinner();
}

checkWinner()

Checks both win conditions after every player move and every agent tick. Calls endRace() with the appropriate status if either side has won.
function checkWinner() {
  if (env.gameStatus() === "win") { endRace("player-win"); return true; }
  if (agent.hasReachedTreasure()) { endRace("agent-win"); return true; }
  return false;
}

endRace(status)

Clears the agent timer, marks the race as inactive, updates the status pill, re-enables the start button, and appends a result line to the game log.
function endRace(status) {
  raceActive = false;
  raceOver   = true;
  stopAgentTimer();
  setStatus(status);
  startButton.disabled = false;

  if (status === "player-win") {
    log(`Player reached the X first in ${playerSteps} moves. Agent moves: ${agent.steps}.`);
  } else if (status === "agent-win") {
    log(`The ${DIFFICULTY_SETTINGS[difficultySelect.value].label.toLowerCase()} agent reached the X first in ${agent.steps} moves.`);
  }

  render();
}

newMaze()

Generates a fresh randomized maze, recreates both env and agent, and resets all counters. Passes a tighter density on Hard to produce corridors with fewer open cells.
function newMaze() {
  stopAgentTimer();
  mazeData = createRandomMaze({ density: difficultySelect.value === "hard" ? 0.5 : 0.56 });
  env   = new TreasureMaze(mazeData.maze, playerStart, target);
  agent = new CompetitiveAgent({ maze: mazeData.maze, difficulty: difficultySelect.value,
                                  start: agentStart, target });
  playerSteps = 0;
  raceActive  = false;
  raceOver    = false;
  startButton.disabled = false;
  setStatus("ready");
  const route = findShortestPath(mazeData.maze, playerStart, target);
  log(`New randomized 8×8 maze loaded. Only valid open cells can be used. Shortest route: ${route.path.length - 1} moves.`);
  render();
}

resetRace(shouldLog?)

Recreates env and agent on the same mazeData so the board layout is preserved. Used by the Reset button and as a pre-step before startRace() when the previous race is over.
function resetRace(shouldLog = true) {
  stopAgentTimer();
  env   = new TreasureMaze(mazeData.maze, playerStart, target);
  agent = new CompetitiveAgent({ maze: mazeData.maze, difficulty: difficultySelect.value,
                                  start: agentStart, target });
  playerSteps = 0;
  raceActive  = false;
  raceOver    = false;
  startButton.disabled = false;
  setStatus("ready");
  if (shouldLog) log("Race reset on the current maze.");
  render();
}
The difficulty <select> change handler calls resetRace(false) to suppress the “Race reset” log line, then immediately calls configureDifficulty() to apply the new settings and print its own log message instead.

Initialization

When the module first loads, the browser executes two setup calls at the bottom of the file:
configureDifficulty(); // sync difficulty description text + agent settings
newMaze();             // generate first board, render initial state
This means the game is fully playable the moment the page finishes loading — no further user action is required before clicking Start Race.

Build docs developers (and LLMs) love