Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Autonomous Raycaster Maze</title>
<style>
body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { border: 1px solid #333; background: #000; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const c = document.getElementById('c');
const ctx = c.getContext('2d');
// --- Configuration ---
const MAP_SIZE = 10;
const CELL_SIZE = 64;
const SCREEN_W = 800;
const SCREEN_H = 600;
c.width = SCREEN_W;
c.height = SCREEN_H;
// --- Game State Variables ---
let maze = [];
let player = { x: 0, y: 0, angle: Math.PI / 2 }; // Start facing positive Y (down)
const MOVEMENT_SPEED = 0.15;
const ANGULAR_SPEED = 0.03;
// --- Maze Generation (Recursive Backtracker) ---
function generateMaze() {
let grid = Array(MAP_SIZE).fill(0).map(() => Array(MAP_SIZE).fill(true)); // True means wall exists
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1]
];
let stack = [];
let start_x = 1;
let start_y = 1;
stack.push({ x: start_x, y: start_y });
grid[start_y][start_x] = false; // Clear starting cell
while (stack.length > 0) {
let current = stack[stack.length - 1];
let neighbors = [];
// Check potential moves (N, S, E, W)
const dirs = [[0, -2], [0, 2], [-2, 0], [2, 0]];
for (const [dx, dy] of dirs) {
let nx = current.x + dx;
let ny = current.y + dy;
if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE &&
!grid[ny][nx] && grid[current.y + Math.floor((dy / 2))][current.x + Math.floor((dx / 2))] === true) {
neighbors.push({ x: nx, y: ny, wall_to_clear: [current.x + dx / 2, current.y + dy / 2] });
}
}
if (neighbors.length > 0) {
let next = neighbors[Math.floor(Math.random() * neighbors.length)];
// Clear the wall between current and next
grid[current.y + Math.floor((next.y - current.y) / 2)][current.x + Math.floor((next.x - current.x) / 2)] = false;
grid[next.y][next.x] = false;
stack.push({ x: next.x, y: next.y });
} else {
stack.pop();
}
}
}
// --- Raycasting Core Logic ---
function castRay(angle) {
const MAX_DIST = 10 * CELL_SIZE;
let x = player.x + Math.cos(angle) * 0.1;
let y = player.y + Math.sin(angle) * 0.1;
let sinA = Math.sin(angle);
let cosA = Math.cos(angle);
const step = 0.005; // Step size for ray increments
let distance = 0;
while (distance < MAX_DIST) {
x += cosA * step;
y += sinA * step;
distance += step;
// Check map collision using integer coordinates of the grid cell edge
const gx = Math.floor(x);
const gy = Math.floor(y);
if (gx < 0 || gx >= MAP_SIZE || gy < 0 || gy >= MAP_SIZE) break;
// Check if the point (gx, gy) is a wall (1)
let currentCellWall = maze[gy][gx];
if (currentCellWall === 1) {
// Collision detected. Distance calculation needs refinement for true hit spot.
// For simplicity and performance in this limited context, we use the distance accumulated so far.
return Math.sqrt((x - player.x)**2 + (y - player.y)**2);
}
}
return MAX_DIST; // Never hits a wall
}
function drawScene() {
ctx.clearRect(0, 0, SCREEN_W, SCREEN_H);
// Draw floor/ceiling (simple ambient color)
ctx.fillStyle = '#444'; ctx.fillRect(0, 0, SCREEN_W, SCREEN_H / 2); // Ceiling
ctx.fillStyle = '#666'; ctx.fillRect(0, SCREEN_H / 2, SCREEN_W, SCREEN_H / 2); // Floor
// Raycasting loop (one ray per vertical screen slice)
const numRays = SCREEN_W;
for (let i = 0; i < numRays; i++) {
const angle = player.angle - Math.PI / 2 + (i * (Math.PI / numRays));
// Angle correction: Raycasting works better if the angle is relative to the X-axis
let rayAngle = angle;
// Distance calculation using simple coordinate projection for performance
let distance = castRay(rayAngle);
// Fisheye Correction (Projected Horizontal Distance)
const correctedDistance = distance * Math.cos(Math.abs(player.angle - rayAngle));
if (correctedDistance < 1) continue; // Ignore if too close/invalid
// Calculate wall height and position on screen
const wallHeight = SCREEN_H / (correctedDistance / CELL_SIZE);
const drawStart = Math.max(0, (SCREEN_H - wallHeight) / 2);
const drawEnd = Math.min(SCREEN_H, drawStart + wallHeight);
// Shading (simple inverse function based on distance)
let shadeStrength = 1 - Math.min(1, correctedDistance / (CELL_SIZE * 2));
let colorFactor = Math.floor(shadeStrength * 150);
ctx.fillStyle = `rgb(${colorFactor}, ${colorFactor}, ${colorFactor})`;
ctx.fillRect(i, drawStart, 1, drawEnd - drawStart);
}
// Display Player Info (HUD)
ctx.fillStyle = 'white';
ctx.font = '16px monospace';
ctx.fillText(`Pos: (${Math.floor(player.x)}, ${Math.floor(player.y)})`, 10, 20);
ctx.fillText(`Angle: ${(player.angle * 180 / Math.PI).toFixed(0)}°`, 10, 40);
}
// --- Movement and AI Logic ---
function movePlayer() {
let newX = player.x + Math.cos(player.angle) * MOVEMENT_SPEED;
let newY = player.y + Math.sin(player.angle) * MOVEMENT_SPEED;
// Check collision (using integer grid coordinates for simplicity)
const mapCheckX = Math.floor(newX);
const mapCheckY = Math.floor(newY);
if (mapCheckX >= 1 && mapCheckX < MAP_SIZE - 1 &&
mapCheckY >= 1 && mapCheckY < MAP_SIZE - 1) {
if (maze[Math.floor(newY)][Math.floor(newX)] === 0) {
player.x = newX;
player.y = newY;
}
}
// Autonomous Turning Logic: Try to keep moving forward or turn slightly if blocked
const rayDistance = castRay(player.angle);
if (rayDistance > 4 * CELL_SIZE) { // If clear path ahead
// Simple drift/jiggle random turning tendency
if (Math.random() < 0.01) {
player.angle += (Math.random() - 0.5) * 0.2;
}
} else {
let turned = false;
// Check for potential turn options (neighbors in a circle around current cell)
const directions = [
{ dx: 1, dy: 0, angle_change: Math.PI / 2 },
{ dx: -1, dy: 0, angle_change: -Math.PI / 2 },
{ dx: 0, dy: 1, angle_change: 0 },
{ dx: 0, dy: -1, angle_change: Math.PI }
];
// Prioritize turning to the most open adjacent cell
let bestTurn = null;
let minDistanceToOpenCell = Infinity;
for (const { dx, dy, angle_change } of directions) {
if (maze[Math.floor(player.y + dy)][Math.floor(player.x + dx)] === 0) {
// Calculate rough turning cost/benefit
let potentialAngle = player.angle + angle_change;
// Check if this new direction is better than the current blocked path
if (potentialAngle !== player.angle && Math.random() < 0.1) {
bestTurn = { angle: potentialAngle, cost: 0 }; // Simple acceptance for now
break;
}
}
}
if (bestTurn) {
player.angle = bestTurn.angle;