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.

q-agent.js defines the AI opponent that races the player through the maze. It exports two items: a DIFFICULTY_SETTINGS configuration object that controls how fast and how smart the agent plays, and a CompetitiveAgent class that combines BFS pathfinding with probabilistic exploration and hesitation to produce behavior that genuinely varies by difficulty level. The module imports findShortestPath, ACTION_DELTAS, ACTION_NAMES, and key from treasure-maze.js — it has no other dependencies.

DIFFICULTY_SETTINGS

DIFFICULTY_SETTINGS is a plain object keyed by difficulty name. Each entry contains six properties that CompetitiveAgent and app.js read to control timing and decision-making.

Property Reference

PropertyTypeDescription
labelstringDisplay name shown in the HUD.
moveDelaynumber (ms)Interval between agent moves once the race starts.
startDelaynumber (ms)Delay before the agent takes its first move after the race begins.
mistakeRatenumber (0–1)Probability per move that the agent uses chooseExplorationMove instead of choosePolicyMove.
hesitateRatenumber (0–1)Probability per move that the agent skips the turn entirely (returns current snapshot without moving).
descriptionstringNarrative description shown below the difficulty selector in the UI.

Values by Difficulty

PropertyEasyMediumHard
label"Easy""Medium""Hard"
moveDelay1150 ms780 ms470 ms
startDelay1600 ms900 ms350 ms
mistakeRate0.420.180
hesitateRate0.20.080
description"The agent explores more, hesitates sometimes, and gives the player a head start.""The agent usually follows the learned route but still makes occasional exploration choices.""The agent exploits the optimal path aggressively and rarely gives the player breathing room."
On Hard difficulty mistakeRate and hesitateRate are both 0, meaning the agent always follows the BFS-optimal path and never skips a turn. It is effectively unbeatable on a short maze unless the player already knows the route.

CompetitiveAgent Class

CompetitiveAgent is the AI opponent. On every call to move(), it decides whether to hesitate, whether to explore a random open cell, or whether to follow the BFS-shortest path to the treasure. It tracks its own position, visited trail, and step count independently of the TreasureMaze environment.

Constructor

new CompetitiveAgent({ maze, difficulty, start, target })
maze
number[][]
required
The 8×8 grid. Must be the same maze the TreasureMaze environment is using.
difficulty
string
default:"\"medium\""
One of "easy", "medium", or "hard". Controls which DIFFICULTY_SETTINGS entry is loaded.
start
[number, number]
default:"[0, 0]"
The agent’s starting position. Also used as the initial key in trail.
target
[number, number]
default:"[7, 7]"
The treasure cell the agent is racing toward.
The constructor calls configure() internally, so every property listed below is set on construction.

Properties

PropertyTypeDescription
mazenumber[][]The maze grid passed to the constructor.
difficultystringActive difficulty name ("easy", "medium", or "hard").
settingsobjectThe active DIFFICULTY_SETTINGS entry. Falls back to medium for unknown keys.
position[number, number]The agent’s current [row, col].
target[number, number]The treasure cell [row, col].
trailSet<string>All cell keys the agent has visited, including the start.
stepsnumberTotal number of successful moves taken (hesitations don’t count).
lastMovestringHuman-readable description of the most recent action. Initialized to "waiting" on construction; updated to values such as "exploited right", "explored down", "hesitated", or "blocked" after each move() call.
policy{ path, actions }The most recently computed BFS result from choosePolicyMove().

Methods

configure({ maze, difficulty, start, target })

Resets the agent in-place with new parameters. Called by the constructor and by app.js when the difficulty selector changes. Returns this for chaining.
agent.configure({ maze: newMaze, difficulty: "hard", start: [0, 0], target: [7, 7] });
Returns: this

validActions(from?)

Returns all moves the agent can make from a given position — only moves that land on an open cell (1) inside the grid are included.
from
[number, number]
default:"this.position"
The cell to check from.
Returns: Array<{ action: number, row: number, col: number }> — each entry contains the action integer and the destination coordinates.

chooseExplorationMove()

Picks a random valid move, preferring unvisited cells. If all neighbors have already been visited, any valid neighbor is eligible. Returns: { action: number, row: number, col: number } | nullnull if there are no valid moves.

choosePolicyMove()

Recomputes the BFS shortest path from the current position to target, then returns the move that takes the agent to the next cell on that path. Returns: { action: number, row: number, col: number } | nullnull if no path exists or the agent is already at the target.
choosePolicyMove calls findShortestPath on every invocation. This keeps the policy fresh after exploration detours instead of following a stale path computed from the starting cell.

move()

The main per-tick method called by app.js on the setInterval timer. Implements a three-level decision flow:
1

Hesitation check

Roll against settings.hesitateRate. If the roll hits, set lastMove = "hesitated" and return the current snapshot without updating position or steps.
2

Exploration vs. exploitation

Roll against settings.mistakeRate. If the roll hits, call chooseExplorationMove(); otherwise call choosePolicyMove().
3

Safe fallback

If the primary choice returns null, fall back first to choosePolicyMove(), then to chooseExplorationMove(). If both return null, the agent is fully blocked — set lastMove = "blocked" and return snapshot.
4

Commit move

Update position, add the destination key to trail, increment steps, and set lastMove to "explored <direction>" or "exploited <direction>".
move() {
  // 1. Hesitation
  if (Math.random() < this.settings.hesitateRate) {
    this.lastMove = "hesitated";
    return this.snapshot();
  }

  // 2. Exploration vs. exploitation
  const useExploration = Math.random() < this.settings.mistakeRate;
  const move = useExploration
    ? this.chooseExplorationMove()
    : this.choosePolicyMove();

  // 3. Safe fallback
  const safeMove = move ?? this.choosePolicyMove() ?? this.chooseExplorationMove();

  if (!safeMove) {
    this.lastMove = "blocked";
    return this.snapshot();
  }

  // 4. Commit
  this.position = [safeMove.row, safeMove.col];
  this.trail.add(key(safeMove.row, safeMove.col));
  this.steps += 1;
  this.lastMove = `${useExploration ? "explored" : "exploited"} ${ACTION_NAMES[safeMove.action]}`;
  return this.snapshot();
}
Returns: snapshot object (see below).

hasReachedTreasure()

Returns true if the agent’s current position matches target. Returns: boolean

optimalPathLength()

Computes the BFS shortest path length from [0, 0] to target on the current maze. Note: this always starts from [0, 0], not from the agent’s current position — it reflects the theoretical minimum number of steps to solve the maze from scratch. Returns: number — total cells in the BFS path (including start and end).

snapshot()

Returns a plain object capturing the agent’s current state. Called at the end of every move() call and by app.js inside render() via env.snapshot({ agent: agent.snapshot() }). Returns:
{
  position:   [number, number],   // current [row, col]
  trail:      Set<string>,        // copy of all visited keys
  steps:      number,             // total moves taken
  lastMove:   string,             // most recent action description
  difficulty: string,             // "easy" | "medium" | "hard"
  settings:   object,             // active DIFFICULTY_SETTINGS entry
  policy:     { path, actions }   // last computed BFS result
}

Usage Example

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

const { maze } = createRandomMaze();

const agent = new CompetitiveAgent({
  maze,
  difficulty: "medium",
  start: [0, 0],
  target: [7, 7]
});

// Tick the agent once per interval
const timer = setInterval(() => {
  const snap = agent.move();
  console.log(`Step ${snap.steps}: ${snap.lastMove} → [${snap.position}]`);

  if (agent.hasReachedTreasure()) {
    clearInterval(timer);
    console.log(`Agent reached the X in ${agent.steps} steps.`);
  }
}, DIFFICULTY_SETTINGS.medium.moveDelay);

Build docs developers (and LLMs) love