mager-bench

Claude Sonnet 4.6 × doom

1.0

DDA 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.0
correctness 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