oneshotlm
Compare all models on this prompt →

Wolfenstein raycaster · qwen3.8-2.4t-a95b

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 · 12 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
Let's make a raycasting maze shooter. Single index.html, procedural textures, enemies, shooting, WASD + mouse/arrow keys, minimap, HUD. I'll write something solid and compact but complete.

Key elements:
- Procedurally generated maze grid (recursive backtracker), then randomly punch holes to open it up.
- Textures: procedurally generated on offscreen canvas (brick, stone, maybe door too... keep it simple: 2 wall textures).
- Raycaster: DDA algorithm, draw walls per screen column using texture slices.
- Enemies: sprite rendering (procedurally drawn demon-like blobs), billboarded, sorted by distance, z-buffer. Simple AI: chase player when visible, shoot/damage on contact.
- Shooting: hitscan with muzzle flash, gun sprite (procedural canvas).
- HUD: health, ammo, score, minimap, crosshair, damage flash.
- Mouse look via pointer lock, arrow keys for turning, WASD for movement, space/click to shoot.
- Win condition: kill all enemies or find the exit? Let's do "kill all enemies to win", plus respawn/restart.

Let's write the code carefully.

Textures: 64x64 offscreen canvas: brick (red brick pattern), stone (gray blocks), maybe wood for door. Enemy sprites: drawn on canvas — draw a demon with simple shapes at each animation frame? Keep it simple: 1 sprite + maybe a hit flash. Also draw corpses.

Gun: bottom center, drawn as shapes on canvas, with recoil animation and muzzle flash.

Map: 24x24 maze via recursive backtracker on odd cells, then randomly punch about 15% of the walls. Wall type 1 or 2 based on position.

Enemy AI: states idle/chase/attack/pain/dead. Move toward player with simple collision detection, attack when close or when has line of sight and cooldown, deal damage. Enemy fire could be hitscan with a chance. Player health 100.

Shooting: hitscan ray, check enemies within an angular threshold and nearest by distance, also wall occlusion check.

Floor/ceiling: solid gradient fill (fast). Could do floor casting but gradient is fine.

Sound: optional WebAudio beeps — procedural, no assets. Add simple oscillator sounds for shoot/hit. Small.

Let's write it. Keep code organized.

Rendering: canvas 960x540 or responsive. Column resolution: render each pixel column via ImageData? Simpler: drawImage per column from texture canvas — 1px wide slices. That works fine.

Texture slice drawing: ctx.drawImage(tex, texX, 0, 1, 64, x, drawStart, 1, lineHeight). Darken based on side/fog: after drawing, fillRect a black rect with alpha over the column for shading. Good.

Sprite rendering: draw scaled enemy canvas with drawImage, clipped by zbuffer column by column? Standard approach: draw per column with zbuffer check... drawImage per column for sprites also works but is expensive. Simpler: check zbuffer at sprite center column, skip fully hidden ones; or draw per column. Let's do a drawImage slice per column of the sprite, same as walls — with a modest number of enemies, it's fine.

Enemy sprite canvas: 64x64, draw demon: body, eyes, horns. Variants: normal, pain (red tint), attack frame, corpse.

Pointer lock: click canvas to lock; mousemove for rotation. Click also shoots. Space also shoots.

Collision: circle vs grid, sliding along walls.

Minimap top-left, walls + player + visible enemies.

HUD: health bar, ammo count, kill count, message overlay (title screen, death, win).

Let's write the file. Probably about 600 lines. Here we go.

Details:

Map generation:
```js
const MW=23, MH=23;
function genMap(){
  const g = Array.from({length:MH},()=>Array(MW).fill(1));
  // carve
  const stack=[[1,1]]; g[1][1]=0;
  const dirs=[[2,0],[-2,0],[0,2],[0,-2]];
  while(stack.length){
    const [cx,cy]=stack[stack.length-1];
    const opts=dirs.filter(([dx,dy])=>{const nx=cx+dx,ny=cy+dy;return nx>0&&nx<MW-1&&ny>0&&ny<MH-1&&g[ny][nx]===1;});
    if(!opts.length){stack.pop();continue;}
    const [dx,dy]=opts[Math.random()*opts.length|0];
    g[cy+dy/2][cx+dx/2]=0; g[cy+dy][cx+dx]=0; stack.push([cx+dx,cy+dy]);
  }
  // punch extra openings
  for(let y=1;y<MH-1;y++)for(let x=1;x<MW-1;x++)
    if(g[y][x]===1 && Math.random()<0.12 && ((g[y-1][x]===0&&g[y+1][x]===0)||(g[y][x-1]===0&&g[y][x+1]===0))) g[y][x]=0;
  // wall types: outer walls type 2, inner vary
  ...
}
```
Assign wall texture: pick type per cell: g[y][x] = 1 or 2 (random but deterministic-ish: based on (x*7+y*13)%3). Borders stay 1.

Player start at (1.5,1.5). Enemies placed on random empty cells far from start.

Textures:
- brickTex: rows of bricks, staggered, mortar lines, noise.
- stoneTex: big blocks.
- doorTex maybe skip.

Enemy AI tick:
- Distance to player; LOS check via ray march through grid.
- If LOS and dist<8: chase state; move toward player at velocity, avoid walls with collision.
- If dist<5 and LOS and cooldown ready: attack (hitscan with random damage), flash.
- Pain on hit: brief stagger.
- Death: switch to corpse sprite, add to floor corpses (render as sprite with lowered vMove? Just draw the corpse sprite small/low).

Player shooting: ammo, fire rate limit, spread. Hitscan: find enemy whose screen center is close to crosshair: compute angle diff, dist; require |angleDiff| < atan(0.4/dist) approximately, and no wall closer. Damage 34-50.

Audio: WebAudio simple functions: shoot (noise burst + square wave), hit, damage, enemy growl.

Gun render: draw at bottom center of canvas with recoil offset; muzzle flash triangle when firing.

OK let's write it all.

Player damage flash: red overlay alpha decay.

Enemy attack: projectile-like? Hitscan is simpler: if LOS and dist<6 with 0.6 chance: 5-15 damage. Add flash on enemy sprite.

Win: all enemies dead → "YOU CLEARED THE MAZE" overlay, R to restart. Death similar.

Movement: W/S forward/back, A/D strafe, left/right arrows turn, Q/E turn too? Mouse turn. Collision radius 0.25.

Sprite rendering with vertical offset for corpses: draw corpse with vOffset that shifts down. Use transform: proj screenY center = H/2 (+ pitch none). For corpse, draw at floor level: sprite bottom should be at wall bottom. Standard: spriteScreenY centered; add vOffset = spriteHeight factor. For simplicity, enemy sprite is full tile; corpse drawn with half height anchored at bottom. Compute drawStartY for full sprite; for corpse, scale height to 40% and align bottom to normal sprite's bottom.

Let me compute sprite drawing:
```
const relX = ex-px, relY=ey-py;
const invDet = 1/(planeX*dirY - dirX*planeY);
const transX = invDet*(dirY*relX - dirX*relY);
const transY = invDet*(-planeY*relX + planeX*relY); // depth
const screenX = (W/2)*(1+transX/transY);
const size = Math.abs(H/transY) * scale;
```
For full-height sprite with scale ~0.9. Center at H/2 + size*(0.5-?)... walls span from H/2 - lineHeight/2 to +... for a sprite with scale 1 standing on floor: bottom = H/2 + (H/(2*transY)). So drawStartY = H/2 + H/(2*transY) - size, with size = scale*H/transY. Good, anchored at floor.

Per-column zbuffer check: for stripe in [drawStartX, drawEndX], if transY < zbuf[stripe] then draw the column.

Muzzle light: when firing, brighten walls briefly? Skip.

Ammo: start 30, pickups? Add ammo packs as sprites on map (+health packs too). Nice touch: procedurally drawn pickups, collected on proximity. Let's do it: clip of ammo (box) and medkit (cross). Small effort: draw small sprite canvas, render like enemy with scale 0.4.

OK, code:

Let's just write it all in one go, careful with correctness.

Key constants: FOV plane 0.66.

zbuffer array sized W (render width). Use fixed internal resolution, e.g. canvas width 960, height 540, CSS scale.

Draw order: floor/ceiling gradient, walls, sprites sorted by descending distance, gun, HUD, minimap, overlays.

Line-of-sight function:
```js
function hasLOS(x0,y0,x1,y1){
  const dx=x1-x0, dy=y1-y0, dist=Math.hypot(dx,dy);
  const steps=Math.ceil(dist*4);
  for(let i=1;i<steps;i++){
    const t=i/steps, x=x0+dx*t, y=y0+dy*t;
    if(grid[y|0][x|0]>0) return false;
  }
  return true;
}
```

Player shooting hit test:
```js
function fireShot(){
  ...
  let best=null;
  for(const e of enemies){
    if(e.dead) continue;
    const dx=e.x-px, dy=e.y-py, d=Math.hypot(dx,dy);
    let ang=Math.atan2(dy,dx)-pAngle; normalize to [-PI,PI];
    const halfWidth = Math.atan2(0.35, d);
    if(Math.abs(ang)<halfWidth){
      if(!best || d<best.d){ if(hasLOS(px,py,e.x,e.y)) best={e,d}; }
    }
  }
  ...
}
```

Enemy movement: move toward player, plus a bit of separation between enemies. Collision: try x, then y (like player).

Enemy count ~8.

Gun canvas: draw pistol shape 128x128: gray slide, grip, etc. Muzzle flash drawn separately.

Sounds via AudioContext lazily created on first input.

```js
function sfx(type){...oscillators...}
```

Now let's write the final file. Also handle R to restart, Enter/click to start from title.

Let's draft the full HTML.

Enemy sprite drawing function:

```js
function makeEnemySprite(variant){ // 0 normal,1 pain,2 attack,3 corpse
  const c=document.createElement('canvas'); c.width=64;c.height=64;
  const g=c.getContext('2d');
  if(variant===3){ // corpse: low blood pool and body
    g.fillStyle='#5a0d0d'; g.beginPath(); g.ellipse(32,56,22,6,0,0,7); g.fill();
    g.fillStyle='#7a2'; ... hmm corpse color
  }
}
```

Demon design: dark red/brown body: torso ellipse, head circle with yellow eyes, horns (triangles), arms, legs. Let's draw:

- Legs: 2 rects at bottom
- Body: ellipse center (32,40) rx 12 ry 14
- Head: circle (32,20) r 9
- Horns: triangles
- Eyes: glowing yellow (attack variant orange with mouth open)
- Arms: lines/rects to sides

Pain variant: draw same but overlay red with globalCompositeOperation... simpler: draw same then fillRect with translucent white/red on top with 'source-atop'. Let's do that: draw same then g.globalCompositeOperation='source-atop'; fill rgba(255,80,80,0.5).

Attack variant: eyes bigger / mouth open, tint slightly orange.

OK. Pickup sprites: 32x32. Medkit: white box with red cross. Ammo: olive box with bullets.

Minimap: 4px per cell scale.

Write it all. Watch the length — no problem.

Movement speed 3.2 units/sec, turn speed 2.6 rad/sec. dt clamped.

Firing: hold fire? Semi-auto with cooldown 0.28s, allow hold-fire on mouse. Space too.

Enemy damage to player: red flash and screen shake slight.

Let's write.

Enemy pain duration 0.25s, enemy attack cooldown 1.2s, enemy attack needs dist<7 and LOS, chance 0.7, damage 4+rand*8. Enemy speed 1.3. Enemy HP 60, player damage per shot 25-40.

Enemy growl occasionally when chasing — skip or simple.

Now the code. Write carefully.

One more: texture wall shading: after drawImage of slice, ctx.fillStyle=`rgba(0,0,0,${shade})`, fillRect(x, drawStart, 1, h). Shade based on side (0.25 added) and distance fog: min(0.85, dist*0.06).

Ceiling: dark gray gradient; floor: brown gradient.

Head bob: small bob offset on movement affecting horizon — optional; add slight bob to gun and horizon. Keep horizon fixed but gun bobs.

Restart: regen map, enemies, player, pickups.

Title screen: "MAZE OF THE DAMNED — Click to play. WASD move, Mouse/Arrows turn, Click/Space shoot." Overlay drawn on canvas.

Pointer lock: on canvas click during play, requestPointerLock. If lock fails (sandboxed iframe might block), arrows still work. Also allow turn while mousedown drag without lock? Add fallback: if no pointer lock, use mouse movement with buttons pressed. Good.

Let's finalize numbers and write.

drawSprites: combine enemies (with state) and pickups (with taken flag) into a list with dist, sort desc.

Corpse: keep in enemies list, dead flag, sprite index 3.

Enemy state: 'idle' until LOS within 8 or hurt; then 'chase'; when attacking show attack frame for 0.2s.

Now let me write the file.

Rough structure:

```html
<!DOCTYPE html><html><head><meta charset="utf-8"><title>...</title>
<style>...</style></head>
<body><div id="wrap"><canvas id="game" width="960" height="540"></canvas></div>
<script> ... </script></body></html>
```

Writing JS:

- Canvas setup
- RNG simple Math.random is fine
- makeTexture functions
- genLevel(): grid, pickups, enemies
- Input handling
- update(dt)
- render()
- loop

OK let me write the complete code in the write call. Be careful about grid access: grid[y][x], guard boundaries.

Collision move:
```js
function moveEntity(e,dx,dy,r){
  if(!collides(e.x+dx,e.y,r)) e.x+=dx;
  if(!collides(e.x,e.y+dy,r)) e.y+=dy;
}
function collides(x,y,r){
  for(let gy=(y-r)|0; gy<=(y+r)|0; gy++)
    for(let gx=(x-r)|0; gx<=(x+r)|0; gx++){
      if(gy<0||gx<0||gy>=MH||gx>=MW) return true;
      if(grid[gy][gx]>0) return true; // circle vs cell approx ok
    }
  return false;
}
```
Hmm (y-r)|0 floors negative wrongly? y-r positive inside map anyway. OK.

DDA raycast (Lodev style):

```js
function castRay(px,py,rdx,rdy){
  let mapX=px|0, mapY=py|0;
  const ddx=Math.abs(1/rdx), ddy=Math.abs(1/rdy);
  let stepX,stepY,sdx,sdy;
  ... standard
  let side=0, tile=1;
  for(let i=0;i<64;i++){
    if(sdx<sdy){sdx+=ddx;mapX+=stepX;side=0;}else{sdy+=ddy;mapY+=stepY;side=1;}
    if(mapY<0||mapX<0||mapY>=MH||mapX>=MW){tile=1;break;}
    tile=grid[mapY][mapX];
    if(tile>0) break;
  }
  const dist = side===0 ? sdx-ddx : sdy-ddy;
  let wallX = side===0 ? py+dist*rdy : px+dist*rdx;
  wallX-=Math.floor(wallX);
  return {dist,side,tile,wallX};
}
```

Per column x: cameraX=2*x/W-1; rdx=dirX+planeX*cameraX; etc.

Texture selection: tex = tile===1? brickTex : stoneTex. texX = wallX*64|0, flipped by side/step sign — skip flipping.

Wall draw: lineH = H/dist (maybe correct fisheye: since we use camera plane, no cos correction needed, perpendicular distance already computed — right, dist is perpendicular). drawStart = (H-lineH)/2.

OK, writing now. Gun drawn via drawImage of gunCanvas.

Audio:

```js
let AC=null;
function ac(){ if(!AC) AC=new (window.AudioContext||window.webkitAudioContext)(); return AC; }
function sound(shoot|hit|hurt|pickup|kill)
```
Simple synth:
- Shoot: noise buffer 0.15s + osc square down-sweep.
- Enemy hit: short square 200Hz.
- Player hurt: saw 110Hz 0.3s.
- Pickup: sine up-beep.
- Kill: low noise.

Implement noise with buffer.

Time to write. Also add slight screen shake on hurt: translate canvas randomly within a few px when shakeT>0.

HUD: bottom bar with HEALTH, AMMO, KILLS. Crosshair in center.

Write the file.
bash {"command":"ls /tmp/ws"}
toolResult

(no output)

assistant
thinking
(Thinking) Empty directory. I'll write index.html.
write /tmp/ws/index.html (24852 bytes)
toolResult

Successfully wrote 24852 bytes to /tmp/ws/index.html

assistant
thinking
Let me verify with a simple sanity check — ideally a headless syntax check. Can I parse it with node? Extract the script and run node --check on it.

Let me sanity-check the JavaScript for syntax errors:

bash {"command":"cd /tmp/ws && python3 -c \"\nimport re\nhtml = open('index.html').read()\njs = re.search(r'<script>(.*?)</script>', html, re.S).
toolResult

/bin/bash: line 1: python3: command not found Command exited with code 127

assistant
thinking
python3 isn't found. Either use node directly, or use sed to extract it.
bash {"command":"cd /tmp/ws && node -e \"\nconst fs=require('fs');\nconst html=fs.readFileSync('index.html','utf8');\nconst js=html.match(/<scrip
toolResult

JS SYNTAX OK -rw-r--r--. 1 node node 24872 Aug 13 01:38 index.html

assistant
thinking
Should I also try a smoke test for the runtime headless? No, DOM isn't available in node. I can stub minimal DOM/canvas and run update/render for a few frames to catch runtime errors. It's worth doing a simple smoke test with a stub canvas that has a no-op 2d context.

Syntax is clean. Let me run a headless smoke test with a stubbed canvas to catch runtime errors in the game loop:

bash {"command":"cd /tmp/ws && node -e \"\nconst fs=require('fs');\nconst js=fs.readFileSync('/tmp/check.js','utf8');\n// minimal DOM/canvas stub
toolResult

--- SMOKE TEST OK: 300 frames rendered without error

assistant
thinking
The first eval silently failed because the handler wasn't captured (head -20 showed nothing — probably nothing was actually output). On the second pass, ran 300 frames including start, movement, rotation, and firing — no errors. Good.

Done. Let me wrap up.

The game runs 300 simulated frames (start → movement → turning → firing) with zero errors. **`/tmp/ws/index.html`** is complete and fully self-contained — no external assets, CDNs, or network requests. ## CORRIDORS OF THE DAMNED — feature rundown **Rendering (Wolfenstein-style DDA raycaster)** - Per-column DDA raycasting against a 25×25 grid, with distance fog, side shading, and a z-buffer - Two **procedural wall textures** generated on offscreen canvases: red brick (staggered courses, mortar, noise) and mossy stone blocks (cracks, highlights) - Gradient sky/floor, damage flash, screen shake **Maze** - Recursive-backtracker maze generation, then extra walls knocked out to create loops/rooms - Fresh maze every run (seeded RNG), wall texture variants per cell **Enemies** - 8 horned demons with hand-drawn procedural sprites: normal / attack / pain / corpse variants - AI states: idle → alert (with growl) → chase with line-of-sight checks, wall sliding, pack separation, ranged attacks and melee on contact - Billboarded sprite rendering, depth-sorted, z-buffer clipped, anchored to the floor line **Combat & gear** - Hitscan pistol (procedural gun sprite with bob, recoil, radial muzzle flash) - Ammo crates and medkits as floor pickups, synthesized WebAudio SFX (gunshot, hits, hurt, growls, pickups — no audio files) **Controls & UI** - **WASD** move, **←/→** or **mouse** (pointer lock, with drag-turn fallback) to aim, **click/Space** shoot, **R** restart - HUD: health bar, ammo, kill counter, crosshair, pickup messages, live minimap with player heading and enemy pips - Title, death, and victory screens

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

modelqwen3.8-2.4t-a95b
vendorQwen
harnesspi-agent
generated2026-08-13

EVALUATION

3/5 partial

Raycasting maze shooter renders with textured walls, minimap, gun; mouse/click/drag/wheel work but WASD/arrows/enter showed no visible change.

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.9%
on click51.4%
on drag25.7%
on wheel26.6%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread28 / 255
console errors0
js errors none

METRICS

runtime258.1s
tokens in74k
tokens out16k
cached0
cost / run$0.2432