oneshotlm
Compare all models on this prompt →

Wolfenstein raycaster · gemma-4-31b-it

Build a first-person raycasting maze shooter on a canvas in the Wolfenstein 3D style: WASD movement and arrow or mouse turning through a textured grid maze with enemies you can shoot. Procedural textures only, no external assets.

⚠ no runnable output

This one-shot run produced no runnable artifact.

timed out after 600000ms

The transcript below shows what the model did — read it to see why the one shot fell short.

transcript · 4 messages
user

Build a first-person raycasting maze shooter on a canvas in the Wolfenstein 3D style: WASD movement and arrow or mouse turning through a textured grid maze with enemies you can shoot. Procedural textures only, no external assets.

assistant
thinking
The objective is to create a first-person raycasting maze shooter (Wolfenstein 3D style) in a single `index.html` file.

    *   Raycasting engine (Wolfenstein 3D style).
    *   WASD movement.
    *   Arrow keys or Mouse turning (Mouse capture is better for FPS).
    *   Textured grid maze (procedural textures).
    *   Enemies in the maze.
    *   Shooting mechanism.
    *   Single-page app (CSS/JS/HTML in one file).
    *   No external assets (no images, no CDNs).

    *   `Map`: 2D array representing walls.
    *   `Player`: x, y, direction (angle), plane (for FOV).
    *   `Enemy`: x, y, health, state.
    *   `Raycaster`: For each vertical strip of the screen, cast a ray until it hits a wall.
    *   `Texture Generator`: Use a canvas to create procedural textures (e.g., brick, stone) and store them as image data or just use the canvas for lookup.

    *   *Player Movement*: Standard FPS controls.
    *   *Rendering*:
        *   Walls: Calculate distance, project height, sample texture.
        *   Floor/Ceiling: Simple colors or gradient.
        *   Enemies: Billboarding (rendering 2D sprites that always face the player).
    *   *Combat*: Line-of-sight check (raycast from player to enemy).
    *   *Textures*: Create a set of small canvas elements to serve as textures.

    *   *Texture Generation*: Since I can't use external images, I'll generate small 64x64 canvases with patterns.
    *   *Performance*: Use `requestAnimationFrame`. Use a TypedArray for the map if needed, but a small 2D array is fine for a simple maze.
    *   *Input Handling*: `keydown`/`keyup` for WASD, `mousemove` for rotation (using Pointer Lock API).

    *   *Setup*: Basic HTML structure, `<canvas>` element.
    *   *World*: Define a map.
    *   *Math*:
        *   `dx = cos(angle)`, `dy = sin(angle)`.
        *   DDA (Digital Differential Analysis) algorithm for fast ray-wall intersections.
    *   *Sprites*: Store enemies as objects. Sort by distance (z-buffer) to render correctly.
    *   *Combat*: Project a ray from player center. If it hits an enemy before a wall, it's a hit.

    *   Procedural Textures:
        *   Wall: Bricks.
        *   Player Gun: A simple drawn rectangle/shape at the bottom of the screen.
    *   UI: HUD (Health, Score, Crosshair).

    *   *Canvas*: 640x480 or dynamic.
    *   *DDA Algorithm*: Standard Wolf3D.
    *   *Texture Mapping*: Calculate `wallX` to map the texture column.
    *   *Sprite Rendering*: Simple billboarding.

Let's start writing the code.
write {"content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-wi
toolResult

toolResult

Validation failed for tool "write": - path: must have required properties path Received arguments: { "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Raycast Maze Shooter</title>\n <style>\n body {\n margin: 0;\n padding: 0;\n background-color: #111;\n color: #eee;\n font-family: 'Courier New', Courier, monospace;\n overflow: hidden;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n user-select: none;\n }\n #game-container {\n position: relative;\n width: 800px;\n height: 600px;\n border: 4px solid #444;\n box-shadow: 0 0 20px rgba(0,0,0,0.5);\n }\n canvas {\n display: block;\n image-rendering: pixelated;\n }\n #hud {\n position: absolute;\n bottom: 20px;\n left: 20px;\n right: 20px;\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n pointer-events: none;\n }\n .stat-box {\n background: rgba(0, 0, 0, 0.7);\n padding: 10px 20px;\n border: 2px solid #888;\n font-size: 24px;\n font-weight: bold;\n color: #f00;\n text-shadow: 2px 2px #000;\n }\n #crosshair {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 20px;\n height: 20px;\n border: 2px solid rgba(255, 255, 255, 0.5);\n border-radius: 50%;\n transform: translate(-50%, -50%);\n pointer-events: none;\n }\n #crosshair::before, #crosshair::after {\n content: '';\n position: absolute;\n background: rgba(255, 255, 255, 0.5);\n }\n #crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; }\n #crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; }\n #instructions {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n background: rgba(0,0,0,0.8);\n padding: 20px;\n border: 2px solid #fff;\n z-index: 10;\n cursor: pointer;\n }\n </style>\n</head>\n<body>\n\n<div id=\"game-container\">\n <canvas id=\"gameCanvas\" width=\"800\" height=\"600\"></canvas>\n <div id=\"crosshair\"></div>\n <div id=\"hud\">\n <div class=\"stat-box\">HEALTH: <span id=\"health\">100</span></div>\n <div class=\"stat-box\">KILLS: <span id=\"kills\">0</span></div>\n </div>\n <div id=\"instructions\">\n <h1>MAZE SHOOTER</h1>\n <p>WASD to Move | Mouse to Look | Click to Shoot</p>\n <p>Click to Start</p>\n </div>\n</div>\n\n<script>\n /**\n * CONFIGURATION & CONSTANTS\n */\n const CANVAS_WIDTH = 800;\n const CANVAS_HEIGHT = 600;\n const TICK_RATE = 1000 / 60;\n const FOV = Math.PI / 3;\n const MAP_SIZE = 24;\n const TEX_SIZE = 64;\n\n const canvas = document.getElementById('gameCanvas');\n const ctx = canvas.getContext('2d');\n const healthEl = document.getElementById('health');\n const killsEl = document.getElementById('kills');\n const instructions = document.getElementById('instructions');\n\n // Game State\n let gameRunning = false;\n let health = 100;\n let kills = 0;\n let lastTime = 0;\n\n const keys = {\n w: false, a: false, s: false, d: false\n };\n\n const player = {\n x: 3.5,\n y: 3.5,\n dirX: -1,\n dirY: 0,\n planeX: 0,\n planeY: 0.66,\n moveSpeed: 0.05,\n rotSpeed: 0.003\n };\n\n // Map: 1 = Wall, 0 = Empty\n const worldMap = [\n [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],\n [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\n [1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,1],\n [1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1],\n [1,0,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,1,1,1,0,1],\n [1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1],\n [1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1,0,1],\n [1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1,0,1],\n [1,0,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,1,0,1,0,1,0,1],\n [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1],\n [1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,0,1,1,1,1,1,0,1],\n [1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\n [1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,0,1],\n [1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1],\n [1,0,1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1,0,1,0,1],\n [1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1],\n [1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,0,1],\n [1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,1],\n [1,0,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,1,0,1,0,1],\n [1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,1,0,0,0,0,1],\n [1,0,1,0,1,1,1,1,1,0,1,1,1,0,1,0,1,0,1,0,1,1,1,1],\n [1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1],\n [1,0,0,0,1,1,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,1,0,1],\n [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],\n ];\n\n /**\n * PROCEDURAL TEXTURES\n */\n const textures = {};\n function createProceduralTextures() {\n const texCanvases = {\n wall: (ctx) => {\n // Brick wall\n ctx.fillStyle = '#800';\n ctx.fillRect(0, 0, TEX_SIZE, TEX_SIZE);\n ctx.fillStyle = '#600';\n for(let i=0; i<4; i++) {\n ctx.fillRect(0, i*16, TEX_SIZE, 2);\n ctx.fillRect(0, i*16+2, TEX_SIZE, 14);\n }\n ctx.fillStyle = '#a00';\n for(let i=0; i<4; i++) {\n let offset = (i%2)*16;\n for(let j=0; j<4; j++) {\n ctx.fillRect(j*16 + offset, j*16, 2, 16);\n }\n }\n ctx.strokeStyle = '#300';\n ctx.lineWidth = 2;\n ctx.strokeRect(0,0,TEX_SIZE,TEX_SIZE);\n },\n enemy: (ctx) => {\n // Simple Alien\n ctx.fillStyle = '#0f0';\n ctx.fillRect(16, 16, 32, 32);\n ctx.fillStyle = '#000';\n ctx.fillRect(24, 24, 8, 8);\n ctx.fillRect(32, 24, 8, 8);\n ctx.fillStyle = '#f00';\n ctx.fillRect(24, 36, 16, 4);\n // Body\n ctx.fillStyle = '#0a0';\n ctx.fillRect(16, 48, 32, 16);\n },\n gun: (ctx) => {\n // Gun image\n ctx.fillStyle = '#333';\n ctx.fillRect(20, 0, 24, 64);\n ctx.fillStyle = '#222';\n ctx.fillRect(28, 0, 8, 64);\n ctx.fillStyle = '#555';\n ctx.fillRect(20, 10, 24, 5);\n }\n };\n\n for (let key in texCanvases) {\n const c = document.createElement('canvas');\n c.width = TEX_SIZE;\n c.height = TEX_SIZE;\n texCanvases[key](c.getContext('2d'));\n textures[key] = c;\n }\n }\n\n /**\n * ENEMIES\n */\n const enemies = [];\n function spawnEnemies() {\n for (let i = 0; i < 15; i++) {\n let x, y;\n do {\n x = Math.random() * MAP_SIZE;\n y = Math.random() * MAP_SIZE;\n } while (worldMap[Math.floor(y)][Math.floor(x)] !== 0);\n enemies.push({ x, y, health: 3, alive: true });\n }\n }\n\n /**\n * GAME ENGINE\n */\n function update(dt) {\n if (!gameRunning) return;\n\n // Movement\n const moveStep = player.moveSpeed;\n let newX = player.x;\n let newY = player.y;\n\n if (keys.w) {\n newX += player.dirX * moveStep;\n newY += player.dirY * moveStep;\n }\n if (keys.s) {\n newX -= player.dirX * moveStep;\n newY -= player.dirY * moveStep;\n }\n if (keys.a) {\n newX -= player.planeX * moveStep;\n newY -= player.planeY * moveStep;\n }\n if (keys.d) {\n newX += player.planeX * moveStep;\n newY += player.planeY * moveStep;\n }\n\n // Collision detection (simple)\n if (worldMap[Math.floor(player.y)][Math.floor(newX)] === 0) player.x = newX;\n if (worldMap[Math.floor(newY)][Math.floor(player.x)] === 0) player.y = newY;\n\n // Enemy AI & Combat\n enemies.forEach(en => {\n if (!en.alive) return;\n \n // Move slowly towards player\n const dx = player.x - en.x;\n const dy = player.y - en.y;\n const dist = Math.sqrt(dx*dx + dy*dy);\n if (dist < 10) {\n en.x += (dx / dist) * 0.02;\n en.y += (dy / dist) * 0.02;\n }\n\n // Player damage\n if (dist < 0.5) {\n health -= 0.1;\n if (health <= 0) {\n health = 0;\n gameOver();\n }\n }\n });\n\n healthEl.innerText = Math.ceil(health);\n killsEl.innerText = kills;\n }\n\n function shoot() {\n if (!gameRunning) return;\n\n // Shoot along the player's direction\n // Simple raycast to check if any enemy is in the center of view\n let bestEnemy = null;\n let minDist = Infinity;\n\n enemies.forEach(en => {\n if (!en.alive) return;\n \n // Vector from player to enemy\n const vx = en.x - player.x;\n const vy = en.y - player.y;\n const dist = Math.sqrt(vx*vx + vy*vy);\n \n // Project player direction onto enemy vector\n const dot = (vx * player.dirX + vy * player.dirY) / dist;\n \n // Only if enemy is in front of player\n if (dot > 0.98) { \n // Check if there's a wall between them\n if (!isWallBetween(player.x, player.y, en.x, en.y)) {\n if (dist < minDist) {\n minDist = dist;\n bestEnemy = en;\n }\n }\n }\n });\n\n if (bestEnemy) {\n bestEnemy.health--;\n if (bestEnemy.health <= 0) {\n bestEnemy.alive = false;\n kills++;\n }\n }\n }\n\n function isWallBetween(x1, y1, x2, y2) {\n let currX = x1;\n let currY = y1;\n const dist = Math.sqrt((x2-x1)**2 + (y2-y1)**2);\n const stepX = (x2-x1)/dist * 0.1;\n const stepY = (y2-y1)/dist * 0.1;\n for (let i=0; i<dist/0.1; i++) {\n currX += stepX;\n currY += stepY;\n if (worldMap[Math.floor(currY)][Math.floor(currX)] === 1) return true;\n }\n return false;\n }\n\n function gameOver() {\n gameRunning = false;\n alert(`GAME OVER! Kills: ${kills}`);\n location.reload();\n }\n\n function render() {\n // Background\n ctx.fillStyle = '#333'; // Ceiling\n ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT / 2);\n ctx.fillStyle = '#666'; // Floor\n ctx.fillRect(0, CANVAS_HEIGHT / 2, CANVAS_WIDTH, CANVAS_HEIGHT / 2);\n\n const zBuffer = new Array(CANVAS_WIDTH).fill(Infinity);\n\n // Wall Raycasting\n for (let x = 0; x < CANVAS_WIDTH; x++) {\n const cameraX = 2 * x / CANVAS_WIDTH - 1;\n const rayDirX = player.dirX + player.planeX * cameraX;\n const rayDirY = player.dirY + player.planeY * cameraX;\n\n let mapX = Math.floor(player.x);\n let mapY = Math.floor(player.y);\n\n const deltaDistX = Math.abs(1 / rayDirX);\n const deltaDistY = Math.abs(1 / rayDirY);\n\n let stepX, stepY, sideDistX, sideDistY;\n\n if (rayDirX < 0) {\n stepX = -1;\n sideDistX = (player.x - mapX) * deltaDistX;\n } else {\n stepX = 1;\n sideDistX = (mapX + 1.0 - player.x) * deltaDistX;\n }\n\n if (rayDirY < 0) {\n stepY = -1;\n sideDistY = (player.y - mapY) * deltaDistY;\n } else {\n stepY = 1;\n sideDistY = (mapY + 1.0 - player.y) * deltaDistY;\n }\n\n let hit = 0;\n let side = 0;\n while (hit === 0) {\n if (side === 0) {\n sideDistX += deltaDistX;\n if (worldMap[mapY][mapX + stepX] === 1) {\n hit = 1;\n side = 0;\n } else {\n mapX += stepX;\n }\n } else {\n sideDistY += deltaDistY;\n if (worldMap[mapY + stepY][mapX] === 1) {\n hit = 1;\n side = 1;\n } else {\n mapY += stepY;\n }\n }\n }\n\n let perpWallDist;\n if (side === 0) perpWallDist = (sideDistX - deltaDistX);\n else perpWallDist = (sideDistY - deltaDistY);\n\n zBuffer[x] = perpWallDist;\n\n const lineHeight = Math.floor(CANVAS_HEIGHT / perpWallDist);\n const drawStart = -lineHeight / 2 + CANVAS_HEIGHT / 2;\n \n // Texture mapping\n let wallX;\n if (side === 0) wallX = player.y + perpWallDist * rayDirY;\n else wallX = player.x + perpWallDist * rayDirX;\n wallX -= Math.floor(wallX);\n\n const texX = Math.floor(wallX * TEX_SIZE);\n \n // Draw vertical strip\n ctx.fillStyle = side === 1 ? '#555' : '#777'; // Fallback\n \n // We can use a small temporary canvas or just sample from our texture canvas\n // For performance in JS, we'll just draw using a slice of the texture canvas\n ctx.drawImage(\n textures.wall, \n texX, 0, 1, TEX_SIZE, \n x, drawStart, 1, lineHeight\n );\n \n // Shading\n if (side === 1) {\n ctx.fillStyle = 'rgba(0,0,0,0.3)';\n ctx.fillRect(x, drawStart, 1, lineHeight);\n }\n }\n\n // Sprite Rendering (Enemies)\n const sortedEnemies = enemies\n .filter(en => en.alive)\n .map(en => {\n const vx = en.x - player.x;\n const vy = en.y - player.y;\n return { ...en, dist: Math.sqrt(vx*vx + vy*vy), vx, vy };\n })\n .sort((a, b) => b.dist - a.dist);\n\n sortedEnemies.forEach(en => {\n const transformX = - (player.planeX * 0 + (en.vx * player.dirY - en.vy * player.dirX));\n const transformY = (player.planeX * 0 + (en.vx * player.dirX + en.vy * player.dirY));\n \n // Simplified projection:\n // Vector from player to enemy\n const relX = en.x - player.x;\n const relY = en.y - player.y;\n \n // Rotate relative to player view\n const rotX = relX * player.dirX + relY * player.dirY;\n const rotY = -relX * player.dirY + relY * player.dirX;\n \n // rotX is depth, rotY is horizontal offset\n if (rotX <= 0) return;\n\n const spriteHeight = Math.abs(Math.floor(CANVAS_HEIGHT / rotX));\n const spriteWidth = spriteHeight;\n const screenX = Math.floor((CANVAS_WIDTH / 2) * (1 + rotY / rotX));\n const screenY = -spriteHeight / 2 + CANVAS_HEIGHT / 2;\n\n // Z-Buffer check\n let canDraw = true;\n for (let x = screenX; x < screenX + spriteWidth && x >= 0 && x < CANVAS_WIDTH; x++) {\n if (zBuffer[x] < rotX) {\n // This is a simplification; we'd usually clip the sprite\n }\n }\n\n // Clipping & Drawing\n const drawW = Math.min(spriteWidth, CANVAS_WIDTH - screenX);\n if (screenX >= 0 && screenX < CANVAS_WIDTH) {\n // Clip vertical strips of the sprite based on z-buffer\n for (let x = 0; x < spriteWidth; x++) {\n const screenX_cur = screenX + x;\n if (screenX_cur >= 0 && screenX_cur < CANVAS_WIDTH && zBuffer[screenX_cur] > rotX) {\n ctx.drawImage(\n textures.enemy,\n (x / spriteWidth) * TEX_SIZE, 0, 1, TEX_SIZE,\n screenX_cur, screenY, 1, spriteHeight\n );\n }\n }\n }\n });\n\n // Gun\n ctx.drawImage(textures.gun, CANVAS_WIDTH/2 - TEX_SIZE/2, CANVAS_HEIGHT - TEX_SIZE, TEX_SIZE, TEX_SIZE);\n }\n\n function gameLoop(time) {\n const dt = time - lastTime;\n lastTime = time;\n\n update(dt);\n render();\n requestAnimationFrame(gameLoop);\n }\n\n /**\n * INPUT HANDLING\n */\n window.addEventListener('keydown', e => {\n if (e.key.toLowerCase() === 'w') keys.w = true;\n if (e.key.toLowerCase() === 'a') keys.a = true;\n if (e.key.toLowerCase() === 's') keys.s = true;\n if (e.key.toLowerCase() === 'd') keys.d = true;\n });\n\n window.addEventListener('keyup', e => {\n if (e.key.toLowerCase() === 'w') keys.w = false;\n if (e.key.toLowerCase() === 'a') keys.a = false;\n if (e.key.toLowerCase() === 's') keys.s = false;\n if (e.key.toLowerCase() === 'd') keys.d = false;\n });\n\n canvas.addEventListener('click', () => {\n if (!gameRunning) return;\n shoot();\n });\n\n // Mouse look\n canvas.addEventListener('mousemove', e => {\n if (!gameRunning) return;\n \n const rot = e.movementX * player.rotSpeed;\n \n // Rotate player direction and plane\n const oldDirX = player.dirX;\n const oldDirY = player.dirY;\n \n player.dirX = player.dirX * Math.cos(-rot) - player.dirY * Math.sin(-rot);\n player.dirY = oldDirX * Math.sin(-rot) + player.dirY * Math.cos(-rot);\n \n const oldPlaneX = player.planeX;\n const oldPlaneY = player.planeY;\n \n player.planeX = player.planeX * Math.cos(-rot) - player.planeY * Math.sin(-rot);\n player.planeY = oldPlaneX * Math.sin(-rot) + player.planeY * Math.cos(-rot);\n });\n\n instructions.addEventListener('click', () => {\n instructions.style.display = 'none';\n gameRunning = true;\n canvas.requestPointerLock();\n });\n\n // Init\n createProceduralTextures();\n spawnEnemies();\n requestAnimationFrame(gameLoop);\n\n</script>\n</body>\n</html>\n" }

TAKE IT TO CODE

This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.

RUN

modelgemma-4-31b-it
vendorGoogle
harnesspi-agent
generated2026-07-22

METRICS

runtime
tokens in1.5k
tokens out7.2k
cached0
cost / run$0.0027