oneshotlm
Compare all models on this prompt →

Wolfenstein raycaster · deepseek-v4-flash

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.

⛨ sandboxed Open in new tab ↗
transcript · 20 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 user wants me to build a first-person raycasting maze shooter in the style of Wolfenstein 3D, all in a single self-contained index.html file. Let me plan this out:

1. **Raycasting engine**: Raycasting from a player position through a grid maze
2. **Textures**: Procedurally generated textures (brick walls, floor/ceiling, etc.)
3. **Movement**: WASD for movement, arrow keys or mouse for turning
4. **Enemies**: Simple enemies that can be shot
5. **Canvas rendering**: All on a single canvas

Let me build this comprehensive single-page app.

Key components:
- Grid-based maze map
- Raycasting algorithm (DDA algorithm)
- Procedural wall textures
- Floor/ceiling rendering
- Sprites (enemies)
- Player movement with collision detection
- Shooting mechanics
- Mouse look and keyboard controls

Let me write this all out carefully.

Here's my plan for the code:

1. HTML structure with canvas
2. CSS for full screen
3. JavaScript:
   - Map definition (grid-based maze)
   - Player state (position, direction, FOV)
   - Raycasting with DDA
   - Texture generation (brick, stone, etc.)
   - Wall rendering with texture mapping
   - Floor/ceiling rendering
   - Enemy sprites (billboarding)
   - Input handling (WASD + arrows + mouse)
   - Shooting mechanics
   - Game loop

Let me write the complete file.
write /workspace/index.html (28428 bytes)
toolResult

Successfully wrote 28428 bytes to /workspace/index.html

assistant
thinking
The file is written. Let me verify it's there and check its size.
bash {"command":"ls -lh /workspace/index.html && head -5 /workspace/index.html && echo \"---\" && tail -5 /workspace/index.html"}
toolResult

-rw-r--r-- 1 node node 28K Jul 10 00:51 /workspace/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> --- updateUI(); gameLoop(0); </script> </body> </html>

assistant
thinking
The file is created at 28K. Let me verify it can be served correctly by checking the file structure quickly. Actually, let me just confirm everything looks good. The file is there and complete.

Let me also quickly check that there's no issue with the weapon sprite rendering - the getContext('2d') call on tex might work differently than expected when drawing with drawImage. Actually, when using createImageData and putImageData, the canvas should work fine as an image source for drawImage.

One thing I should check - in the render loop, I'm calling getContext('2d') on textures and then getImageData per-pixel. This is very inefficient - for each pixel on screen I'm creating a context and getting image data. Let me think about whether this will work...

Actually, `getContext('2d')` on a canvas always returns the same context object, so that's fine. But calling `getImageData` per pixel is extremely slow. For a reasonable framerate, I should pre-extract the texture data into arrays.

Let me fix this by pre-extracting texture data into arrays for faster access.
read {"path":"/workspace/index.html","offset":1,"limit":50}
toolResult

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wolfenstein 3D - Raycasting Maze Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: monospace; } canvas { display: block; cursor: crosshair; } #ui { position: absolute; bottom: 20px; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 24px; pointer-events: none; color: #fff; font-size: 14px; text-shadow: 1px 1px 2px #000; } #ui .stat { background: rgba(0,0,0,0.5); padding: 6px 14px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.15); } #ui .stat span { color: #f44; } #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); pointer-events: none; color: rgba(255,255,255,0.5); font-size: 28px; text-shadow: 0 0 4px #000; } #info { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: rgba(255,255,255,0.35); font-size: 12px; text-shadow: 0 0 4px #000; pointer-events: none; text-align: center; } #death { position: absolute; top: 40%; left: 50%; transform: translate(-50%,-50%); color: #f00; font-size: 64px; font-weight: bold; text-shadow: 0 0 20px #f00, 2px 2px 0 #000; display: none; pointer-events: none; text-align: center; } #death small { display: block; font-size: 18px; color: #aaa; margin-top: 12px; font-weight: normal; text-shadow: 1px 1px 0 #000; } #win { position: absolute; top: 40%; left: 50%; transform: translate(-50%,-50%); color: #0f0; font-size: 64px; font-weight: bold; text-shadow: 0 0 20px #0f0, 2px 2px 0 #000; display: none; pointer-events: none; text-align: center; } #win small { display: block; font-size: 18px; color: #aaa; margin-top: 12px; font-weight: normal; text-shadow: 1px 1px 0 #000; } </style> </head> <body> <canvas id="c"></canvas> <div id="crosshair">+</div> <div id="ui"> <div class="stat">HEALTH: <span id="health">100</span></div> <div class="stat">SCORE: <span id="score">0</span></div> <div class="stat">AMMO: <span id="ammo">10</span></div> </div> <div id="info">WASD move · Arrow keys / Mouse look · CLICK to shoot · R restart</div> <div id="death">YOU DIED<small>Press R to restart</small></div> <div id="win">YOU WIN!<small>Press R to restart</small></div> <script> // ============================================================ // RAYCASTING MAZE SHOOTER — Wolfenstein 3D style // ============================================================ const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); // ---------- canvas sizing ---------- function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } resize(); window.addEventListener('resize', resize); // ---------- map ---------- [864 more lines in file. Use offset=51 to continue.]

assistant
thinking
Let me look at the rendering section to optimize the texture access pattern. I need to pre-extract texture pixel data into arrays so I'm not calling getImageData per pixel.
read {"offset":100,"path":"/workspace/index.html","limit":80}
toolResult

img.height = TEX_SIZE; const c = img.getContext('2d'); const d = c.createImageData(TEX_SIZE, TEX_SIZE); const data = d.data; for (let y = 0; y < TEX_SIZE; y++) { for (let x = 0; x < TEX_SIZE; x++) { const i = (y * TEX_SIZE + x) * 4; switch (index) { case 1: // brick wall - red/brown { const brickH = 16, brickW = 32; const by = y % brickH; const bx = x % brickW; const row = Math.floor(y / brickH); const offset = (row % 2 === 0) ? 0 : brickW / 2; const inBrick = (bx + offset) % brickW < brickW - 2 && by < brickH - 2; if (!inBrick) { data[i]=100; data[i+1]=80; data[i+2]=60; data[i+3]=255; } else { const shade = 0.7 + 0.3 * Math.sin(x * 0.3) * Math.sin(y * 0.2); data[i]=160*shade; data[i+1]=50*shade; data[i+2]=30*shade; data[i+3]=255; } break; } case 2: // stone - gray { const val = 100 + 80 * (Math.sin(x*0.5)*Math.cos(y*0.7)*0.5+0.5) + 30 * (Math.random() * (Math.sin(x*13+y*7)>0.5?1:0)); data[i]=val; data[i+1]=val; data[i+2]=val; data[i+3]=255; break; } case 3: // wood - brown { const grain = (Math.sin(y * 0.4 + Math.sin(x * 0.15) * 3) * 0.5 + 0.5); const knot = Math.hypot(x-32, y-32) < 8 ? 0.3 : 1; const val = (80 + 60 * grain) * knot; data[i]=val*1.2; data[i+1]=val*0.7; data[i+2]=val*0.3; data[i+3]=255; break; } case 4: // blue stone { const v = 80 + 70 * (Math.sin(x*0.3+y*0.4)*0.5+0.5); data[i]=v*0.5; data[i+1]=v*0.6; data[i+2]=v*1.0; data[i+3]=255; break; } case 5: // gold/treasure { const sparkle = Math.sin(x*0.8)*Math.sin(y*0.8)*0.5+0.5; data[i]=200+50*sparkle; data[i+1]=170+50*sparkle; data[i+2]=30; data[i+3]=255; break; } case 6: // mossy stone { const g = 80 + 60 * Math.sin(x*0.2+y*0.3)*0.5+0.5; const moss = Math.sin(x*0.5)*Math.cos(y*0.6)>0.3?1:0; data[i]=80-40*moss; data[i+1]=g+40*moss; data[i+2]=60-20*moss; data[i+3]=255; break; } default: data[i]=128; data[i+1]=128; data[i+2]=128; data[i+3]=255; } } } c.putImageData(d, 0, 0); return img; } // Generate all textures for (let i = 0; i < 7; i++) { textures.push(genTexture(i)); } // Floor/ceiling textures (procedural) const floorTex = document.createElement('canvas'); floorTex.width = 64; floorTex.height = 64; { const c = floorTex.getContext('2d'); const d = c.createImageData(64, 64); for (let y = 0; y < 64; y++) { [735 more lines in file. Use offset=180 to continue.]

assistant
thinking
I need to replace the render function to pre-extract texture pixel data rather than calling getImageData per pixel which is extremely slow. Let me replace the texture storage and rendering approach.

I'll pre-extract all texture pixel data into Uint8ClampedArray arrays and use those directly in the render loop.

I need to significantly optimize the rendering performance. Currently it calls `getImageData` per pixel which is extremely slow. Let me rewrite the texture storage and rendering to use pre-extracted pixel arrays.

read {"offset":180,"path":"/workspace/index.html","limit":60}
toolResult

for (let x = 0; x < 64; x++) { const i = (y*64+x)*4; const checker = ((Math.floor(x/8)+Math.floor(y/8))%2===0)?1:0; d.data[i]=50+30*checker; d.data[i+1]=50+30*checker; d.data[i+2]=50+30*checker; d.data[i+3]=255; } } c.putImageData(d, 0, 0); } const ceilTex = document.createElement('canvas'); ceilTex.width = 64; ceilTex.height = 64; { const c = ceilTex.getContext('2d'); const d = c.createImageData(64, 64); for (let y = 0; y < 64; y++) { for (let x = 0; x < 64; x++) { const i = (y*64+x)*4; d.data[i]=30; d.data[i+1]=30; d.data[i+2]=45; d.data[i+3]=255; } } c.putImageData(d, 0, 0); } // ---------- player ---------- const player = { x: 1.5, y: 1.5, dir: 0, // radians health: 100, score: 0, ammo: 10, alive: true, won: false }; const FOV = Math.PI / 3; const MOVE_SPEED = 3.5; const ROT_SPEED = 2.5; const MOUSE_SENS = 0.004; // ---------- enemies ---------- const enemies = []; function resetEnemies() { enemies.length = 0; const positions = [ [4.5, 4.5], [16.5, 4.5], [10.5, 8.5], [18.5, 12.5], [4.5, 14.5], [12.5, 16.5], [20.5, 3.5], [8.5, 20.5], [16.5, 20.5], [21.5, 10.5], [6.5, 10.5], [14.5, 13.5], [3.5, 18.5], [20.5, 18.5], [11.5, 20.5], [22.5, 21.5], [2.5, 8.5], [17.5, 3.5], [7.5, 4.5], [21.5, 15.5] ]; for (const p of positions) { enemies.push({ x: p[0], y: p[1], health: 3, alive: true, type: Math.floor(Math.random() * 3), animTimer: 0 [675 more lines in file. Use offset=240 to continue.]

assistant
thinking
Let me rewrite the entire file with the optimized rendering approach. I'll pre-extract pixel data into arrays for all textures (walls, floor, ceiling, enemy sprites, weapon) and use direct array access in the render loop.

Let me rewrite the whole file.

Let me rewrite the entire file with a fully optimized renderer using pre-extracted pixel arrays.

write /workspace/index.html (22196 bytes)
toolResult

Successfully wrote 22196 bytes to /workspace/index.html

assistant
thinking
Let me verify the file was written correctly and check its size.
bash {"command":"ls -lh /workspace/index.html && wc -l /workspace/index.html"}
toolResult

-rw-r--r-- 1 node node 22K Jul 10 00:52 /workspace/index.html 622 /workspace/index.html

assistant
thinking
Let me do a quick syntax check to make sure the JavaScript is valid.
bash {"command":"node -e \"\nconst fs = require('fs');\nconst html = fs.readFileSync('/workspace/index.html', 'utf8');\nconst scriptStart = html.
toolResult

JavaScript syntax OK

assistant
thinking
The syntax is valid. Let me verify the file works by serving it with a simple HTTP server and checking it can be opened. Actually, I can't really test a browser app from the command line, but the syntax check passed and the file is complete. Let me just verify the structure is correct.

Let me also check for any potential issues:
1. The `putImageData` for the weapon is called after the main `putImageData` which might clear the weapon. Wait no - each `putImageData` call draws on top of the existing canvas content, so the weapon will appear on top. That's correct.

2. The sprite rendering modifies the imageData before it's put to the canvas, which is correct.

3. The weapon drawing uses a separate putImageData after the main one is already rendered, so it overlays on top. This is correct.

Let me double check one thing - the `wallBuffer` is used for sprite occlusion. The sprites are drawn into the imageData before `putImageData` is called, and `wallBuffer` contains the perpendicular distances. This is correct since sprites draw into the same imageData before it's composited.

One concern: the weapon is drawn after `putImageData` with a separate `putImageData` call, which means it overlays. This is fine.

Actually, I realize there might be a problem: `drawWeapon()` uses `ctx.createImageData(drawW, drawH)` and then `ctx.putImageData(wImg, ox, oy)`. The `createImageData` with width/height parameters creates a new ImageData object. This should work fine.

Let me verify the overall structure once more by reading a few key sections.
bash {"command":"grep -c \"function \" /workspace/index.html; grep -n \"putImageData\\|createImageData\\|getImageData\" /workspace/index.html"}
toolResult

13 315: const imgData = ctx.createImageData(W, H); 447: ctx.putImageData(imgData, 0, 0); 459: const wImg = ctx.createImageData(drawW, drawH); 486: ctx.putImageData(wImg, ox, oy);

assistant
thinking
Everything looks good. The file is complete with:
- 13 functions defined
- Only 2 putImageData calls (one for the main scene, one for the weapon overlay)
- No getImageData calls (the textures are pre-extracted pixel arrays)
- Valid JavaScript syntax

The app has:
- ✅ Raycasting engine with DDA algorithm
- ✅ Procedural textures (brick, stone, wood, blue stone, gold, mossy stone)
- ✅ Floor and ceiling with texture mapping
- ✅ WASD movement with collision detection
- ✅ Arrow key and mouse look (pointer lock)
- ✅ 20 enemies that chase and shoot at the player
- ✅ Shooting mechanics with ammo, muzzle flash, and weapon bob
- ✅ Health, score, and ammo HUD
- ✅ Death and win screens
- ✅ R to restart
- ✅ All in one self-contained HTML file, no external assets

The file is complete and valid. Here's what's built into `/workspace/index.html`: **Raycasting Engine** - DDA (Digital Differential Analyzer) raycasting for accurate wall rendering - Full FOV with perspective-correct texture mapping - Distance-based shading and side darkening for depth **Procedural Textures** (all generated in code, zero external assets) - 6 wall types: red brick, gray stone, brown wood, blue stone, gold/treasure, mossy stone - Checkered floor / dark blue ceiling - 3 enemy types (red/blue/green guards) with pixel art rendering - Weapon sprite with muzzle flash effect **Gameplay** - WASD movement with wall sliding collision detection - Arrow keys or mouse look (pointer lock) to turn - Click or Space to shoot — hitscan targeting with line-of-sight check - 20 enemies that chase you and shoot back - Health, score, ammo HUD - Death screen ("YOU DIED") and win screen ("YOU WIN!") - Press R to restart at any time **Performance** - All texture pixel data pre-extracted to `Uint8ClampedArray` buffers - Single `createImageData`/`putImageData` for the main scene (plus one for the weapon overlay) - No per-pixel `getImageData` calls during rendering

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modeldeepseek-v4-flash
vendorDeepSeek
harnesspi-agent
generated2026-07-10

EVALUATION

3/5 partial

Raycasted maze shooter with textured walls and enemies, but player dies quickly and can't restart with tested keys (R not tried).

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.2%
on click3.4%
on drag58.7%
on wheel63.2%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread43.1 / 255
console errors0
js errors none

METRICS

runtime234.4s
tokens in65k
tokens out21k
cached107k
cost / run$0.0118