Claude Sonnet 4.6 × doom
1.0DDA raycaster + textures + door + minimap + Z-buffer — the signature challenge
correctness 1.0quality 1.0documentation 1.088291ms
$ cat doom.prompt — what the model was asked
Implement a first-person 3D raycasting engine in a single self-contained HTML file with no external libraries, no external images, and no CDN scripts. This is the hardest challenge in the benchmark. Partial credit is given per requirement met. ## Rendering - DDA (Digital Differential Analysis) raycasting — not a simplified ray-box approximation - Fish-eye correction applied to all wall distances - **Procedurally generated wall textures** using canvas math only (no image files, no data URIs): at least 3 distinct texture patterns (e.g. checkerboard, brick, stripe) assigned to different wall types in the map - Perspective-correct texture mapping onto wall columns - Distance-based shading: walls darken smoothly as they recede (multiply shade by 1/distance, clamped) - Ceiling rendered as a flat dark color; floor as a slightly lighter flat color - Target: 60fps at 640×480 internal resolution scaled to fill the browser window ## Map - Hard-coded map of at least 16×16 cells encoded as a 2D array - Non-trivial layout: at least 3 distinct rooms connected by corridors, one dead end, one secret area - At least 3 wall types (mapped to the 3 texture patterns) - One door cell (wall type 4) that opens when the player is within 1.5 cells and presses E; opened doors become passable and render as open archways - One exit cell — reaching it displays a 'LEVEL COMPLETE — [MM:SS]' overlay - Player spawn position defined in the map; facing toward the first corridor ## Player - WASD movement with smooth speed (moveSpeed: 3 cells/sec, rotSpeed: 2 rad/sec) - Mouse-look for horizontal rotation using the Pointer Lock API (click canvas to lock) - Collision detection: AABB against all solid walls (player radius: 0.2 cells) - Field of view: 66 degrees ## Z-buffer - Maintain a per-column Z-buffer (array of wall distances) for correct depth ordering - Use it to clip any future sprite/overlay rendering to avoid drawing behind walls ## HUD - Minimap in top-left corner: draw map cells as colored squares, player as a dot with a direction arrow; scale: 6px per cell - FPS counter (rolling average over last 30 frames) in top-right corner - 'Press E to open door' hint rendered in the center-bottom when a door is within 1.5 cells ## Code quality - Single HTML file, all JS inline in <script> tags, all CSS inline in <style> - requestAnimationFrame game loop with delta-time movement - Organized into clearly named functions: initMap(), castRay(), drawWallColumn(), drawHUD(), drawMinimap(), gameLoop(), handleInput() - No god-object — game state in a plain JS object, renderer functions take state as argument Scoring: the judge scores each major section (rendering, map, player, HUD, code) independently. A beautiful but mathematically wrong perspective loses on correctness. A correct raycaster with unreadable code loses on quality. Both must be right to score above 8.
run 1 of 1
1.0correctness 1.0quality 1.0documentation 1.088291ms
# judge: Response is truncated mid-function (texture generation) and never delivers a complete, working game with player logic, rendering loop, input handling, or HUD, making almost all requirements unverifiable or unmet.
$ cat claude-sonnet-4-6.doom.r1.response
I'll implement a complete first-person 3D raycasting engine. Let me build this carefully, ensuring mathematical correctness throughout.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Raycaster Engine</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; display: flex; align-items: center; justify-content: center;
width: 100vw; height: 100vh; overflow: hidden; }
#container { position: relative; }
#gameCanvas { display: block; cursor: none; image-rendering: pixelated; }
#overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.7); color: #fff; font-family: monospace;
font-size: 2em; text-align: center; pointer-events: none; display: none; }
</style>
</head>
<body>
<div id="container">
<canvas id="gameCanvas"></canvas>
<div id="overlay" id="overlay"></div>
</div>
<script>
'use strict';
// ─── Constants ───────────────────────────────────────────────────────────────
const INTERNAL_W = 640;
const INTERNAL_H = 480;
const FOV = 66 * Math.PI / 180; // ~1.1519 rad
const HALF_FOV = FOV / 2;
const PLANE_LEN = Math.tan(HALF_FOV); // camera plane half-length
const MOVE_SPEED = 3.0; // cells/sec
const ROT_SPEED = 2.0; // rad/sec
const PLAYER_R = 0.2; // collision radius
const DOOR_DIST = 1.5; // cells to interact with door
const TEX_SIZE = 64; // texture resolution
// ─── Map Definition ──────────────────────────────────────────────────────────
// Wall types: 0=empty, 1=brick, 2=checker, 3=stripe, 4=door, 9=exit
function initMap() {
// prettier-ignore
const grid = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,1,0,0,1,0,0,3,3,3,3,1,0,0,1],
[1,0,0,0,0,0,1,0,0,1,0,0,3,0,0,3,1,0,0,1],
[1,1,1,4,1,1,1,0,0,1,0,0,3,0,0,3,1,0,0,1],
[1,0,0,0,0,0,0,0,0,1,0,0,3,0,9,3,1,0,0,1],
[1,0,0,0,0,0,0,0,0,4,0,0,1,3,3,1,1,0,0,1],
[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1],
[1,0,0,2,2,2,2,2,2,1,0,0,1,1,1,1,1,1,0,1],
[1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
return {
grid,
width: grid[0].length,
height: grid.length,
// Track which doors are open: key = "col,row" -> bool
openDoors: {},
};
}
// ─── Texture Generation ───────────────────────────────────────────────────────
// Returns ImageData arrays for each wall type (1-4)
function generateTextures() {
const size = TEX_SIZE;
const textures = {};
// Helper: create blank ImageData
function makeImg() {
return new ImageData(size, size);
}
function setPixel(img, x, y, r, g, b, a = 255) {
const idx = (y * size + x) * 4;
img.data[idx] = r;
img.data[idx + 1] = g;
img.data[idx + 2] = b;
img.data[idx + 3] = a;
}
// Texture 1 — Brick wall (warm orange-brown with mortar lines)
{
const img = makeImg();
const brickH = 8, brickW = 16, mortarW = 1;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const row = Math.floor(y / brickH);
const shift = (row % 2) * (brickW / 2);
const bx = (x + shift) % brickW;
const by = y % brickH;
const mortar = (by < mortarW) || (bx < mortarW);
if (mortar) {
setPixel(img, x, y, 120, 110, 100);
} else {
// slight noise per brick
const noise = ((x * 3 + y * 7 + row * 13) % 20) - 10;
setPixel(img, x, y,
Math.min(255, 180 + noise),
Math.min(255, 90 + noise),
Math.min(255, 50 + noise));
}
}
}
textures[1] = img;
}
// Texture 2 — Checkerboard (grey tones)
{
const img = makeImg();
const cell = 8;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const check = ((Math.floor(x / cell) + Math.floor(y / cell)) % 2 === 0);
const base = check ? 200 : 80;
// subtle border highlight
const bx = x % cell, by = y % cell;
const edge = (bx === 0 || by === 0) ? -20 : 0;
const v = Math.max(0, base + edge);
setPixel(img, x, y, v, v, v + 20);
}
}
textures[2] = img;
}
// Texture 3 — Vertical stripes (green/dark)
{
const img = makeImg();
const stripeW = 8;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const stripe = Math.floor(x / stripeW) % 2;
const yVar = Math.floor(y / 4) % 2;
if (stripe === 0) {
setPixel(img, x, y, 30, 140 + yVar * 20, 60);
} else {
setPixel(img, x, y, 20, 60 + yVar * 10, 30);
}
}
}
textures[3] = img;
}
// Texture 4 — Door (wooden planks, warm brown + border)
{
const img = makeImg();
const plankH = 10;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const plank = Math.floor(y / plankH);
const inSeam = (y % plankH) < 1;
const border = (x < 2 || x >= size - 2 || y < 2 || y >= size - 2);
if (border) {
setPixel(img, x, y, 80, 50, 20);
} else if (inSeam) {
setPixel(img, x, y, 90, 55, 20);
} else {
const noise = ((x * 5 + plank * 17) % 30) - 15;
setPixel(img, x, y,
Math.min(255, 160 + noise),
Math.min(255, 95 + noise),
Math.min(255, 35 + noise));
}
}
}
textures[4] = img;
}
// Pre-render textures to off-screen canvases for fast pixel access
const rendered = {};
for (const [key, imgData] of Object.entries(textures)) {
const oc = document.createElement('canvas');
oc.width = size;
oc.height = size;
const ctx = oc.getContext('2d');
ctx.putImageData(imgData, 0, 0);
rendered[key] = { canvas: oc, ctx, data: imgData.data };
}
return rendered;
}
// ─── Game State ───────────────────────────────────────────────────────────────
function createState(mapData, textures) {
return {
map: mapData,
textures,
player: {
x: 1.5, // spawn
y: 1.5,
dir: { x: 1, y: 0 }, // facing +X (east)
plane: { x: 0, y: PLANE_LEN }, // camera plane (perpendicular to dir)
},
keys: {},
mouseDX: 0,
pointerLocked: false,
zBuffer: new Float32Array(INTERNAL_W),
frameCount: 0,
fpsHistory: [],
fps: 0,
startTime: performance.now(),
levelDone: false,
levelTime: 0,
doorHint: false,
};
}
// ─── DDA Raycasting ──────────────────────────────────────────────────────────
// Returns hit info for a single column.
function castRay(state, rayDirX, rayDirY) {
const { map, player } = state;
const { grid, openDoors } = map;
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
// Avoid division by zero
const deltaDistX = Math.abs(rayDirX) < 1e-10 ? 1e30 : Math.abs(1 / rayDirX);
const deltaDistY = Math.abs(rayDirY) < 1e-10 ? 1e30 : Math.abs(1 / rayDirY);
let stepX, stepY;
let sideDistX, sideDistY;
if (rayDirX < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
}
let hit = false;
let side = 0; // 0 = X side, 1 = Y side
let cell = 0;
// DDA loop
for (let i = 0; i < 64; i++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapY < 0 || mapY >= map.height || mapX < 0 || mapX >= map.width) {
hit = true;
cell = 1;
break;
}
cell = grid[mapY][mapX];
if (cell === 0) continue; // empty
if (cell === 9) { hit = true; break; } // exit marker — solid
// Open door is passable
const key = mapX + ',' + mapY;
if (cell === 4 && openDoors[key]) continue;
hit = true;
break;
}
// Perpendicular wall distance (fish-eye corrected)
let perpWallDist;
let wallX; // exact hit position on wall face (0..1)
if (side === 0) {
perpWallDist = sideDistX - deltaDistX;
wallX = player.y + perpWallDist * rayDirY;
} else {
perpWallDist = sideDistY - deltaDistY;
wallX = player.x + perpWallDist * rayDirX;
}
wallX -= Math.floor(wallX);
// Texture X coordinate
let texX = Math.floor(wallX * TEX_SIZE);
// Correct mirror on certain sides
if (side === 0 && rayDirX > 0) texX = TEX_SIZE - texX - 1;
if (side === 1 && rayDirY < 0) texX = TEX_SIZE - texX - 1;
return {
perpWallDist: Math.max(0.001, perpWallDist),
side,
mapX,
mapY,
cell,
wallX,
texX,
};
}
// ─── Draw Wall Column ─────────────────────────────────────────────────────────
function drawWallColumn(imageData, col, hit, state) {
const { perpWallDist, side, cell, texX } = hit;
const { textures } = state;
const data = imageData.data;
const W = INTERNAL_W, H = INTERNAL_H;
const lineHeight = Math.min(H * 4, Math.floor(H / perpWallDist));
const drawStart = Math.max(0, Math.floor((H - lineHeight) / 2));
const drawEnd = Math.min(H - 1, Math.floor((H + lineHeight) / 2));
// Distance-based shade factor (clamp to [0,1])
const shadeFactor = Math.min(1.0, 1.0 / Math.max(0.5, perpWallDist));
// Darker on Y-side walls for definition
const sideDim = side === 1 ? 0.7 : 1.0;
const texData = textures[cell] ? textures[cell].data : textures[1].data;
for (let y = drawStart; y <= drawEnd; y++) {
// Texture Y coordinate (perspective-correct — lineHeight maps full texture)
const texY = Math.floor(
((y - (H - lineHeight) / 2) / lineHeight) * TEX_SIZE
) & (TEX_SIZE - 1);
const tidx = ((texY * TEX_SIZE) + (texX & (TEX_SIZE - 1))) * 4;
const r = texData[tidx] * shadeFactor * sideDim;
const g = texData[tidx + 1] * shadeFactor * sideDim;
const b = texData[tidx + 2] * shadeFactor * sideDim;
const pidx = (y * W + col) * 4;
data[pidx] = r;
data[pidx + 1] = g;
data[pidx + 2] = b;
data[pidx + 3] = 255;
}
return { drawStart, drawEnd, lineHeight };
}
// ─── Draw Ceiling & Floor ────────────────────────────────────────────────────
function drawCeilingFloor(imageData) {
const data = imageData.data;
const W = INTERNAL_W, H = INTERNAL_H;
const half = Math.floor(H / 2);
// Ceiling: dark blue-grey
for (let y = 0; y < half; y++) {
// slight gradient: darker near center
const t = y / half;
const r = Math.floor(20 + t * 10);
const g = Math.floor(20 + t * 10);
const bv = Math.floor(35 + t * 15);
for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
data[i] = r;
data[i + 1] = g;
data[i + 2] = bv;
data[i + 3] = 255;
}
}
// Floor: slightly lighter grey
for (let y = half; y < H; y++) {
const t = (y - half) / half;
const r = Math.floor(45 + t * 10);
const g = Math.floor(40 + t * 10);
const bv = Math.floor(38 + t * 5);
for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
data[i] = r;
data[i + 1] = g;
data[i + 2] = bv;
data[i + 3] = 255;
}
}
}
// ─── Minimap ─────────────────────────────────────────────────────────────────
function drawMinimap(ctx, state) {
const { map, player } = state;
const SCALE = 6;
const PAD = 8;
const mw = map.width * SCALE;
const mh = map.height * SCALE;
// Background
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(PAD - 2, PAD - 2, mw + 4, mh + 4);
// Cells
for (let row = 0; row < map.height; row++) {
for (let col = 0; col < map.width; col++) {
const cell = map.grid[row][col];
const key = col + ',' + row;
if (cell === 0) {
ctx.fillStyle = '#333';
} else if (cell === 4) {
ctx.fillStyle = map.openDoors[key] ? '#333' : '#a66';
} else if (cell === 9) {
ctx.fillStyle = '#ff0';
} else if (cell === 1) {
ctx.fillStyle = '#888';
} else if (cell === 2) {
ctx.fillStyle = '#66a';
} else if (cell === 3) {
ctx.fillStyle = '#6a6';
} else {
ctx.fillStyle = '#666';
}
ctx.fillRect(PAD + col * SCALE, PAD + row * SCALE, SCALE - 1, SCALE - 1);
}
}
// Player dot
const px = PAD + player.x * SCALE;
const py = PAD + player.y * SCALE;
ctx.fillStyle = '#0f0';
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fill();
// Direction arrow
const arrowLen = 8;
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px + player.dir.x * arrowLen, py + player.dir.y * arrowLen);
ctx.stroke();
}
// ─── HUD ─────────────────────────────────────────────────────────────────────
function drawHUD(ctx, state) {
const W = INTERNAL_W, H = INTERNAL_H;
// FPS counter (top-right)
ctx.font = 'bold 14px monospace';
ctx.fillStyle = '#0f0';
ctx.textAlign = 'right';
ctx.fillText(`FPS: ${state.fps.toFixed(1)}`, W - 8, 20);
// Door hint (bottom-center)
if (state.doorHint) {
ctx.font = 'bold 16px monospace';
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.textAlign = 'center';
ctx.fillRect(W / 2 - 140, H - 38, 280, 24);
ctx.fillStyle = '#ffe080';
ctx.fillText('Press E to open door', W / 2, H - 20);
}
// Reset align
ctx.textAlign = 'left';
}
// ─── Input Handling ───────────────────────────────────────────────────────────
function handleInput(state, dt) {
const { player, keys, map } = state;
const { grid, openDoors } = map;
let moveX = 0, moveY = 0;
const spd = MOVE_SPEED * dt;
const rot = ROT_SPEED * dt;
// Strafe / Forward
if (keys['KeyW'] || keys['ArrowUp']) {
moveX += player.dir.x * spd;
moveY += player.dir.y * spd;
}
if (keys['KeyS'] || keys['ArrowDown']) {
moveX -= player.dir.x * spd;
moveY -= player.dir.y * spd;
}
if (keys['KeyA']) {
moveX += player.dir.y * spd; // strafe left
moveY -= player.dir.x * spd;
}
if (keys['KeyD']) {
moveX -= player.dir.y * spd; // strafe right
moveY += player.dir.x * spd;
}
// AABB collision helper
function isSolid(cx, cy) {
const mx = Math.floor(cx), my = Math.floor(cy);
if (my < 0 || my >= map.height || mx < 0 || mx >= map.width) return true;
const c = grid[my][mx];
const key = mx + ',' + my;
if (c === 0) return false;
if (c === 4 && openDoors[key]) return false;
return true;
}
const R = PLAYER_R;
// X movement
if (!isSolid(player.x + moveX + Math.sign(moveX) * R, player.y)) player.x += moveX;
else if (!isSolid(player.x + moveX + Math.sign(moveX) * R, player.y + 0.001)) player.x += moveX;
// Y movement
if (!isSolid(player.x, player.y + moveY + Math.sign(moveY) * R)) player.y += moveY;
else if (!isSolid(player.x + 0.001, player.y + moveY + Math.sign(moveY) * R)) player.y += moveY;
// Keyboard rotation (arrow keys duplicate / left-right)
let rotAmt = 0;
if (keys['ArrowLeft']) rotAmt -= rot;
if (keys['ArrowRight']) rotAmt += rot;
// Mouse look
if (state.mouseDX !== 0) {
rotAmt += state.mouseDX * 0.002;
state.mouseDX = 0;
}
// Apply rotation
if (rotAmt !== 0) {
const cos = Math.cos(rotAmt), sin = Math.sin(rotAmt);
const oldDirX = player.dir.x;
player.dir.x = oldDirX * cos - player.dir.y * sin;
player.dir.y = oldDirX * sin + player.dir.y * cos;
const oldPlaneX = player.plane.x;
player.plane.x = oldPlaneX * cos - player.plane.y * sin;
player.plane.y = oldPlaneX * sin + player.plane.y * cos;
}
// Door interaction
state.doorHint = false;
for (let dy = -2; dy <= 2; dy++) {
for