mager-bench

Claude Haiku 4.5 × doom

0.7

DDA raycaster + textures + door + minimap + Z-buffer — the signature challenge

correctness 0.0quality 1.0documentation 1.050595ms
$ 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

0.7
correctness 0.0quality 1.0documentation 1.050595ms

# judge: Response is truncated mid-function (texture generation incomplete) leaving no working game loop, rendering, input handling, or HUD—almost none of the requirements can be verified as implemented.

$ cat claude-haiku-4-5.doom.r1.response
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Raycasting Engine</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100vw;
            height: 100vh;
            background: #000;
            font-family: 'Courier New', monospace;
            overflow: hidden;
        }
        #gameContainer {
            position: relative;
            background: #000;
            cursor: crosshair;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
            image-rendering: pixelated;
            image-rendering: crisp-edges;
        }
        #crosshair {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 20px;
            height: 20px;
            border: 2px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="gameContainer">
        <canvas id="gameCanvas"></canvas>
        <div id="crosshair"></div>
    </div>

    <script>
        // ============================================================================
        // GAME STATE
        // ============================================================================
        const gameState = {
            // Map
            map: null,
            mapWidth: 0,
            mapHeight: 0,
            doors: {},
            
            // Player
            player: {
                x: 0,
                y: 0,
                angle: 0,
                radius: 0.2,
                moveSpeed: 3,
                rotSpeed: 2,
                fov: 66 * Math.PI / 180
            },
            
            // Camera
            screenWidth: 640,
            screenHeight: 480,
            zBuffer: [],
            
            // Input
            keys: {},
            mouseActive: false,
            lastMouseX: 0,
            
            // Timing
            lastFrameTime: Date.now(),
            frameCount: 0,
            frameTime: [],
            fps: 0,
            levelStartTime: 0,
            levelComplete: false,
            levelCompleteTime: 0,
            
            // Textures
            textures: {},
            
            // Game constants
            WALL_EMPTY: 0,
            WALL_BRICK: 1,
            WALL_CHECK: 2,
            WALL_STRIPE: 3,
            WALL_DOOR: 4,
            WALL_EXIT: 5
        };

        // ============================================================================
        // MAP GENERATION & INITIALIZATION
        // ============================================================================
        function initMap() {
            // 20x20 map with 3 rooms, corridors, secret area, and door
            const map = [
                [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,0,0,2,2,0,0,0,0,0,0,0,0,0,1],
                [1,0,0,0,0,0,0,0,2,2,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,0,0,0,0,0,0,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,3,3,3,3,3,0,0,1],
                [1,0,0,0,0,0,0,0,0,0,0,0,3,3,3,3,3,0,0,1],
                [1,0,0,0,0,0,0,0,0,0,0,0,3,3,3,3,3,0,0,1],
                [1,1,1,1,1,4,1,1,1,1,1,1,3,3,3,3,3,0,0,1],
                [1,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
                [1,2,2,2,0,0,0,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,0,0,0,0,0,0,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,0,0,0,0,0,0,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,0,0,0,0,0,0,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,5,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]
            ];

            gameState.map = map;
            gameState.mapWidth = map[0].length;
            gameState.mapHeight = map.length;
            
            // Initialize doors
            gameState.doors = {
                '5,8': false  // Door at position (5,8)
            };
            
            // Set player start position and angle
            gameState.player.x = 2.5;
            gameState.player.y = 1.5;
            gameState.player.angle = 0;
            
            // Initialize z-buffer
            gameState.zBuffer = new Array(gameState.screenWidth).fill(Infinity);
            
            // Generate textures
            generateTextures();
            
            // Start timer
            gameState.levelStartTime = Date.now();
        }

        // ============================================================================
        // TEXTURE GENERATION
        // ============================================================================
        function generateTextures() {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            
            // Texture size for sampling
            const texSize = 64;
            
            // Brick texture (type 1)
            const brickCanvas = document.createElement('canvas');
            brickCanvas.width = texSize;
            brickCanvas.height = texSize;
            const brickCtx = brickCanvas.getContext('2d');
            drawBrickTexture(brickCtx, texSize);
            gameState.textures[1] = brickCanvas;
            
            // Checkerboard texture (type 2)
            const checkCanvas = document.createElement('canvas');
            checkCanvas.width = texSize;
            checkCanvas.height = texSize;
            const checkCtx = checkCanvas.getContext('2d');
            drawCheckerboardTexture(checkCtx, texSize);
            gameState.textures[2] = checkCanvas;
            
            // Stripe texture (type 3)
            const stripeCanvas = document.createElement('canvas');
            stripeCanvas.width = texSize;
            stripeCanvas.height = texSize;
            const stripeCtx = stripeCanvas.getContext('2d');
            drawStripeTexture(stripeCtx, texSize);
            gameState.textures[3] = stripeCanvas;
        }

        function drawBrickTexture(ctx, size) {
            const brickW = size / 8;
            const brickH = size / 4;
            
            ctx.fillStyle = '#8B4513';
            ctx.fillRect(0, 0, size, size);
            
            for (let y = 0; y < size; y += brickH) {
                const offsetX = (y / brickH) % 2 === 1 ? brickW / 2 : 0;
                for (let x = offsetX; x < size; x += brickW) {
                    ctx.strokeStyle = '#654321';
                    ctx.lineWidth = 2;
                    ctx.strokeRect(x, y, brickW - 1, brickH - 1);
                    
                    // Add shading to brick
                    const gradient = ctx.createLinearGradient(x, y, x, y + brickH);
                    gradient.addColorStop(0, 'rgba(255,255,255,0.1)');
                    gradient.addColorStop(1, 'rgba(0,0,0,0.2)');
                    ctx.fillStyle = gradient;
                    ctx.fillRect(x, y, brickW - 1, brickH - 1);
                }
            }
        }

        function drawCheckerboardTexture(ctx, size) {
            const squares = 8;
            const squareSize = size / squares;
            
            for (let y = 0; y < squares; y++) {
                for (let x = 0; x < squares; x++) {
                    if ((x + y) % 2 === 0) {
                        ctx.fillStyle = '#C0C0C0';
                    } else {
                        ctx.fillStyle = '#808080';
                    }
                    ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
                }
            }
            
            // Add grid lines
            ctx.strokeStyle = '#404040';
            ctx.lineWidth = 1;
            for (let i = 0; i <= squares; i++) {
                ctx.beginPath();
                ctx.moveTo(i * squareSize, 0);
                ctx.lineTo(i * squareSize, size);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(0, i * squareSize);
                ctx.lineTo(size, i * squareSize);
                ctx.stroke();
            }
        }

        function drawStripeTexture(ctx, size) {
            const stripeWidth = size / 16;
            
            ctx.fillStyle = '#4169E1';
            ctx.fillRect(0, 0, size, size);
            
            ctx.fillStyle = '#87CEEB';
            for (let x = 0; x < size; x += stripeWidth * 2) {
                ctx.fillRect(x, 0, stripeWidth, size);
            }
            
            // Add horizontal gradient
            const gradient = ctx.createLinearGradient(0, 0, 0, size);
            gradient.addColorStop(0, 'rgba(255,255,255,0.2)');
            gradient.addColorStop(0.5, 'rgba(0,0,0,0)');
            gradient.addColorStop(1, 'rgba(0,0,0,0.2)');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, size, size);
        }

        // ============================================================================
        // RAYCASTING ENGINE
        // ============================================================================
        function castRay(state, angle) {
            const x = state.player.x;
            const y = state.player.y;
            const map = state.map;
            
            const dx = Math.cos(angle);
            const dy = Math.sin(angle);
            
            // DDA algorithm
            let rayX = x;
            let rayY = y;
            const stepSize = 0.01;  // Small step size for accuracy
            let distance = 0;
            let hitWallType = 0;
            let wallX = 0;  // Position on wall (0-1) for texture mapping
            let isVertical = false;
            
            // Max distance to cast
            const maxDist = 100;
            
            while (distance < maxDist) {
                rayX += dx * stepSize;
                rayY += dy * stepSize;
                distance += stepSize;
                
                const gridX = Math.floor(rayX);
                const gridY = Math.floor(rayY);
                
                // Check bounds
                if (gridX < 0 || gridX >= map[0].length || gridY < 0 || gridY >= map.length) {
                    break;
                }
                
                const cellType = map[gridY][gridX];
                
                // Check if we hit a wall
                if (cellType > 0 && cellType !== state.WALL_EXIT) {
                    // Check if door is open
                    if (cellType === state.WALL_DOOR) {
                        const doorKey = gridX + ',' + gridY;
                        if (!state.doors[doorKey]) {
                            hitWallType = cellType;
                            // Determine which side we hit
                            isVertical = Math.abs((rayX % 1) - 0.5) > Math.abs((rayY % 1) - 0.5);
                            wallX = isVertical ? (rayY % 1) : (rayX % 1);
                            break;
                        }
                    } else {
                        hitWallType = cellType;
                        isVertical = Math.abs((rayX % 1) - 0.5) > Math.abs((rayY % 1) - 0.5);
                        wallX = isVertical ? (rayY % 1) : (rayX % 1);
                        break;
                    }
                }
            }
            
            // Apply fish-eye correction
            const angleDiff = angle - state.player.angle;
            const correctedDist = distance * Math.cos(angleDiff);
            
            return {
                distance: correctedDist,
                wallType: hitWallType,
                wallX: wallX,
                isVertical: isVertical
            };
        }

        function drawWallColumn(ctx, state, col, rayData) {
            const screenWidth = state.screenWidth;
            const screenHeight = state.screenHeight;
            
            if (rayData.distance <= 0 || rayData.wallType === 0) {
                return;
            }
            
            // Update z-buffer
            state.zBuffer[col] = rayData.distance;
            
            // Calculate wall height with perspective
            const wallHeight = Math.min(screenHeight, (screenHeight / rayData.distance) * 0.5);
            const startY = (screenHeight - wallHeight) / 2;
            
            // Distance-based shading
            const shade = Math.min(1.0, 1.0 / rayData.distance);
            
            // Get texture
            const texCanvas = state.textures[rayData.wallType] || state.textures[1];
            const texCtx = texCanvas.getContext('2d');
            const texSize = texCanvas.width;
            
            // Sample texture column
            const texX = Math.floor(rayData.wallX * texSize) % texSize;
            
            // Draw wall column with perspective-correct texture mapping
            for (let y = 0; y < wallHeight; y++) {
                const screenY = Math.floor(startY + y);
                if (screenY < 0 || screenY >= screenHeight) continue;
                
                // Perspective-correct texture V coordinate
                const texY = Math.floor((y / wallHeight) * texSize) % texSize;
                
                // Sample from texture
                const imageData = texCtx.getImageData(texX, texY, 1, 1);
                let r = imageData.data[0];
                let g = imageData.data[1];
                let b = imageData.data[2];
                
                // Apply distance shading
                r = Math.floor(r * shade);
                g = Math.floor(g * shade);
                b = Math.floor(b * shade);
                
                ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
                ctx.fillRect(col, screenY, 1, 1);
            }
        }

        // ============================================================================
        // COLLISION DETECTION
        // ============================================================================
        function checkCollision(state, newX, newY) {
            const radius = state.player.radius;
            const map = state.map;
            
            // Check against map boundaries and walls
            const minGridX = Math.floor(newX - radius);
            const maxGridX = Math.floor(newX + radius);
            const minGridY = Math.floor(newY - radius);
            const maxGridY = Math.floor(newY + radius);
            
            for (let gy = minGridY; gy <= maxGridY; gy++) {
                for (let gx = minGridX; gx <= maxGridX; gx++) {
                    if (gx < 0 || gx >= map[0].length || gy < 0 || gy >= map.length) {
                        return false;
                    }
                    
                    const cellType = map[gy][gx];
                    
                    if (cellType > 0 && cellType !== state.WALL_EXIT) {
                        if (cellType === state.WALL_DOOR) {
                            const doorKey = gx + ',' + gy;
                            if (!state.doors[doorKey]) {
                                return false;
                            }
                        } else {
                            return false;
                        }
                    }
                }
            }
            
            return true;
        }

        // ============================================================================
        // INPUT HANDLING
        // ============================================================================
        function handleInput(state, deltaTime) {
            const canvas = document.getElementById('gameCanvas');
            
            // Keyboard input
            const moveDir = { x: 0, y: 0 };
            if (state.keys['w'] || state.keys['W']) {
                moveDir.x += Math.cos(state.player.angle);
                moveDir.y += Math.sin(state.player.angle);
            }
            if (state.keys['s'] || state.keys['S']) {
                moveDir.x -= Math.cos(state.player.angle);
                moveDir.y -= Math.sin(state.player.angle);
            }
            if (state.keys['a'] || state.keys['A']) {
                moveDir.x += Math.cos(state.player.angle - Math.PI / 2);
                moveDir.y += Math.sin(state.player.angle - Math.PI / 2);
            }
            if (state.keys['d'] || state.keys['D']) {
                moveDir.x += Math.cos(state.player.angle + Math.PI / 2);
                moveDir.y += Math.sin(state.player.angle + Math.PI / 2);
            }
            
            // Normalize and apply movement
            const moveMag = Math.sqrt(moveDir.x * moveDir.x + moveDir.y * moveDir.y);
            if (moveMag > 0) {
                const moveAmount = state.player.moveSpeed * deltaTime;
                const newX = state.player.x + (moveDir.x / moveMag) * moveAmount;
                const newY = state.player.y + (moveDir.y / moveMag) * moveAmount;
                
                if (checkCollision(state, newX, newY)) {
                    state.player.x = newX;
                    state.player.y = newY;
                }
            }
            
            // Door interaction
            if (state.keys['e'] || state.keys['E']) {
                checkDoorInteraction(state);
                state.keys['e'] = false;
                state.keys['E'] = false;
            }
            
            // Check for level complete
            checkLevelComplete(state);
        }

        function checkDoorInteraction(state) {
            for (const doorKey in state.doors) {
                const [gx, gy] = doorKey.split(',').map(Number);
                const dx = state.player.x - gx - 0.5;
                const dy = state.player.y - gy - 0.5;
                const dist = Math.sqrt(dx * dx + dy * dy);
                
                if (dist < 1.5) {
                    state.doors[doorKey] = true;
                }
            }
        }

        function checkLevelComplete(state) {
            const map = state.map;
            const gridX = Math.floor(state.player.x);
            const gridY = Math.floor(state.player.y);
            
            if (gridX >= 0 && gridX < map[0].length && gridY >= 0 && gridY < map.length) {
                if (map[gridY][gridX] === state.WALL_EXIT) {
                    if (!state.levelComplete) {
                        state.levelComplete = true;
                        state.levelCompleteTime = Date.now();
                    }
                }
            }
        }

        // ============================================================================
        // RENDERING
        // ============================================================================
        function drawWalls(ctx, state) {
            const numRays = state.screenWidth;
            const halfFOV = state.player.fov / 2;
            
            for (let col = 0; col < numRays; col++) {
                const rayAngle = state.player.angle - halfFOV + (col / numRays) * state.player.fov;
                const rayData = castRay(state, rayAngle);
                drawWallColumn(ctx, state, col, rayData);
            }
        }

        function drawFloorCeiling(ctx, state) {
            const screenWidth = state.screenWidth;
            const screenHeight = state.screenHeight;
            const midY = screenHeight / 2;
            
            // Ceiling
            ctx.fillStyle = '#1a1a1a';
            ctx.fillRect(0, 0, screenWidth, midY);
            
            // Floor
            ctx.fillStyle = '#2a2a2a';
            ctx.fillRect(0, midY, screenWidth, screenHeight);
        }

        function drawMinimap(ctx, state) {
            const cellSize = 6;
            const mapX = 10;
            const mapY = 10;
            
            // Draw map cells
            for (let y = 0; y < state.mapHeight; y++) {
                for (let x = 0; x < state.mapWidth; x++) {
                    const cellType = state.map[y][x];
                    
                    let color = '#000000';
                    if (cellType === 0) {
                        color = '#1a1a1a';
                    } else if (cellType === state.WALL_BRICK) {
                        color = '#8B4513';
                    } else if (cellType === state.WALL_CHECK) {
                        color = '#C0C0C0';
                    } else if (cellType === state.WALL_STRIPE) {
                        color = '#4169E1';
                    } else if (cellType === state.WALL_DOOR) {
                        color = state.doors[x + ',' + y] ? '#1a1a1a' : '#FF6600';
                    } else if (cellType === state.WALL_EXIT) {
                        color = '#00FF00';
                    }
                    
                    ctx.fillStyle = color;
                    ctx.fillRect(mapX + x * cellSize, mapY + y * cellSize, cellSize - 1, cellSize - 1);
                }
            }
            
            // Draw player
            const playerMapX = mapX + state.player.x * cellSize;
            const playerMapY = mapY + state.player.y * cellSize;
            
            ctx.fillStyle = '#FFFF00';
            ctx.beginPath();
            ctx.arc(playerMapX, playerMapY, 2, 0, Math.PI * 2);
            ctx.fill();
            
            // Draw direction arrow
            const arrowLen = 4;
            const arrowX = playerMapX + Math.cos(state.player.angle) * arrowLen;
            const arrowY = playerMapY + Math.sin(state.player.angle) * arrowLen;
            ctx.strokeStyle = '#FFFF00';
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.moveTo(playerMapX, playerMapY);
            ctx.lineTo(arrowX, arrowY);
            ctx.stroke();
        }

        function drawFPS(ctx, state) {
            const now = Date.now();
            const deltaTime = (now - state.lastFrameTime) / 1000;
            state.lastFrameTime = now;
            
            const fps = 1 / deltaTime;
            state.frameTime.push(fps);
            if (state.frameTime.length > 30) {
                state.frameTime.shift();
            }
            
            const avgFPS = state.frameTime.reduce((a, b) => a + b, 0) / state.frameTime.length;
            state.fps = Math.round(avgFPS);
            
            ctx.fillStyle = '#00FF00';
            ctx.font = 'bold 12px monospace';
            ctx.textAlign = 'right';
            ctx.fillText(`FPS: ${state.fps}`, state.screenWidth - 10, 20);
        }

        function drawDoorHint(ctx, state) {
            let nearDoor = false;
            
            for (const doorKey in state.doors) {
                const [gx, gy] = doorKey.split(',').map(Number);
                const dx = state.player.x - gx - 0.5;
                const dy = state.player.y - gy - 0.5;
                const dist = Math.sqrt(dx * dx + dy * dy);
                
                if (dist < 1.5) {
                    nearDoor = true;
                    break;
                }
            }
            
            if (nearDoor) {
                ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.font = '14px monospace';
                ctx.textAlign = 'center';
                ctx.fillText('Press E to open door', state.screenWidth / 2, state.screenHeight - 30);
            }
        }

        function drawLevelComplete(ctx, state) {
            if (!state.levelComplete) return;
            
            const elapsed = state.levelCompleteTime - state.levelStartTime;
            const minutes = Math.floor(elapsed / 60000);
            const seconds = Math.floor((elapsed % 60000) / 1000);
            const time