Wolfenstein raycaster · laguna-xs-2.1
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.
This one-shot run produced no runnable artifact.
pi container exit 124: Warning: Model "poolside/laguna-xs-2.1" not found for provider "openrouter". Using custom model id.
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 10 messages
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.
thinking
The user wants me to build a first-person raycasting maze shooter in the style of Wolfenstein 3D. This is a classic game that uses raycasting to render a 3D-like perspective from a 2D map. Let me break down what I need to implement: 1. A 2D map/grid representing the maze 2. Raycasting algorithm to render walls in perspective 3. Textured walls (procedural textures) 4. First-person movement with WASD 5. Turning with arrow keys or mouse 6. Enemies that can be shot 7. All in a single HTML file with inline CSS and JS This is a complex project. Let me think about the key components: 1. **Raycasting**: For each column of the screen, cast a ray from the player's position in the direction they're looking. Find where it hits a wall, then draw a vertical slice with the appropriate texture. 2. **Movement**: WASD for forward/backward/strafe, arrow keys for turning. Mouse support for turning. 3. **Textures**: Procedural textures using patterns or noise algorithms. 4. **Enemies**: Simple sprites that are rendered based on distance and can be shot. 5. **Shooting**: A simple raycast to detect if an enemy is in the line of fire. Let me create a comprehensive implementation. I'll need to be careful with performance and make sure the raycasting algorithm is correct. The classic raycasting approach: - For each x coordinate on screen: - Calculate the ray direction for that column - Cast the ray through the 2D map - Find the distance to the nearest wall - Apply fish-eye correction - Calculate wall height based on distance - Draw the wall slice with texture For enemies, I'll use a simple sprite rendering approach where I check if the player's ray would hit an enemy. Let me write this complete implementation.
I'll build a complete Wolfenstein 3D style raycasting shooter in a single HTML file. This is a complex project that requires implementing raycasting, texture mapping, movement, and shooting mechanics.
/workspace/index.html (25150 bytes)Successfully wrote 25150 bytes to /workspace/index.html
thinking
I've written the initial version. Let me test it to make sure it works, and then I can improve it if needed. The current implementation has: 1. A 16x16 maze with walls and paths 2. Raycasting for wall rendering with procedural brick/stone/metal textures 3. Enemy sprites that can be shot 4. WASD movement and arrow key turning 5. Mouse support for turning 6. Shooting with SPACE Let me verify the file was created and maybe run a quick test to see if there are any issues.
Let me test the game to make sure it works properly:
{"command":"cd /workspace && ls -la index.html && head -100 index.html"}-rw-r--r-- 1 node node 25150 Jul 22 18:20 index.html <!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 Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; font-family: monospace; color: #fff; } canvas { display: block; background: #000; } #ui { position: absolute; top: 10px; left: 10px; font-size: 14px; pointer-events: none; } #crosshair { position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; margin: -10px 0 0 -10px; border: 2px solid #fff; border-radius: 50%; pointer-events: none; } #message { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; background: rgba(0,0,0,0.7); border: 2px solid #fff; display: none; text-align: center; } </style> </head> <body> <canvas id="canvas"></canvas> <div id="ui"> <div>WASD - Move | Arrow Keys - Turn | Mouse - Turn | SPACE - Shoot</div> <div id="score">Score: 0</div> <div id="health">Health: 100</div> </div> <div id="crosshair"></div> <div id="message"></div> <script> // Game constants const CanvasW = 640; const CanvasH = 480; const MapSize = 16; const TileSize = 64; const Fov = PI / 3; const HalfFov = Fov / 2; const NumRays = CanvasW; const MaxDist = 20; const PlayerSize = 0.3; // Game state let canvas, ctx; let map = []; let enemies = []; let player = { x: 3.5, y: 3.5, dir: 0, health: 100, score: 0 }; let keys = {}; let mouseDelta = 0; let shooting = false; let shootTimer = 0; let death = false; // Initialize map with maze layout function initMap() { // Create empty map for (let i = 0; i < MapSize; i++) { map[i] = []; for (let j = 0; j < MapSize; j++) { map[i][j] = 0; } } // Outer walls for (let i = 0; i < MapSize; i++) { map[0][i] = 1; map[MapSize-1][i] = 1; map[i][0] = 1; map[i][MapSize-1] = 1; }
thinking
The file was created successfully. Let me check if there are any syntax errors by examining the JavaScript more carefully. I notice there might be an issue with how I'm checking for enemy hits in the raycast - I'm using a variable `dist` that shadows a global variable. Let me also improve the enemy rendering and add some polish. Let me read the full file to review it:
{"path":"/workspace/index.html"}<!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 Shooter</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #000; overflow: hidden; font-family: monospace; color: #fff; } canvas { display: block; background: #000; } #ui { position: absolute; top: 10px; left: 10px; font-size: 14px; pointer-events: none; } #crosshair { position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; margin: -10px 0 0 -10px; border: 2px solid #fff; border-radius: 50%; pointer-events: none; } #message { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; background: rgba(0,0,0,0.7); border: 2px solid #fff; display: none; text-align: center; } </style> </head> <body> <canvas id="canvas"></canvas> <div id="ui"> <div>WASD - Move | Arrow Keys - Turn | Mouse - Turn | SPACE - Shoot</div> <div id="score">Score: 0</div> <div id="health">Health: 100</div> </div> <div id="crosshair"></div> <div id="message"></div> <script> // Game constants const CanvasW = 640; const CanvasH = 480; const MapSize = 16; const TileSize = 64; const Fov = PI / 3; const HalfFov = Fov / 2; const NumRays = CanvasW; const MaxDist = 20; const PlayerSize = 0.3; // Game state let canvas, ctx; let map = []; let enemies = []; let player = { x: 3.5, y: 3.5, dir: 0, health: 100, score: 0 }; let keys = {}; let mouseDelta = 0; let shooting = false; let shootTimer = 0; let death = false; // Initialize map with maze layout function initMap() { // Create empty map for (let i = 0; i < MapSize; i++) { map[i] = []; for (let j = 0; j < MapSize; j++) { map[i][j] = 0; } } // Outer walls for (let i = 0; i < MapSize; i++) { map[0][i] = 1; map[MapSize-1][i] = 1; map[i][0] = 1; map[i][MapSize-1] = 1; } // Inner walls - create a maze map[1][1] = 1; map[1][2] = 1; map[1][3] = 1; map[2][1] = 1; map[2][5] = 1; map[3][1] = 1; map[3][4] = 1; map[3][5] = 1; map[4][3] = 1; map[4][5] = 1; map[5][2] = 1; map[5][3] = 1; map[5][4] = 1; map[5][6] = 1; map[6][6] = 1; map[6][7] = 1; map[6][8] = 1; map[6][9] = 1; map[7][6] = 1; map[7][9] = 1; map[8][6] = 1; map[8][9] = 1; map[8][10] = 1; map[9][6] = 1; map[9][10] = 1; map[9][11] = 1; map[10][8] = 1; map[10][11] = 1; map[11][8] = 1; map[11][12] = 1; map[11][13] = 1; map[12][12] = 1; map[12][13] = 1; map[12][14] = 1; map[13][12] = 1; map[13][14] = 1; map[13][15] = 1; map[14][14] = 1; map[14][15] = 1; // Create paths (0 = empty) map[1][4] = 0; map[1][6] = 0; map[1][7] = 0; map[2][2] = 0; map[2][3] = 0; map[2][4] = 0; map[2][6] = 0; map[2][7] = 0; map[2][8] = 0; map[3][2] = 0; map[3][3] = 0; map[3][6] = 0; map[3][7] = 0; map[3][8] = 0; map[4][2] = 0; map[4][4] = 0; map[4][6] = 0; map[4][7] = 0; map[4][8] = 0; map[4][9] = 0; map[5][5] = 0; map[5][7] = 0; map[5][8] = 0; map[6][10] = 0; map[6][11] = 0; map[7][7] = 0; map[7][8] = 0; map[7][10] = 0; map[7][11] = 0; map[8][8] = 0; map[8][11] = 0; map[8][12] = 0; map[9][7] = 0; map[9][8] = 0; map[9][9] = 0; map[9][12] = 0; map[9][13] = 0; map[10][9] = 0; map[10][10] = 0; map[10][13] = 0; map[10][14] = 0; map[11][11] = 0; map[11][14] = 0; map[12][13] = 0; map[12][15] = 0; map[13][13] = 0; map[13][15] = 0; // Place some collectibles map[2][10] = 2; // Item map[7][12] = 2; // Item map[10][15] = 2; // Item } // Initialize enemies function initEnemies() { enemies = [ { x: 8, y: 3, alive: true, health: 3 }, { x: 12, y: 4, alive: true, health: 2 }, { x: 5, y: 10, alive: true, health: 4 }, { x: 14, y: 3, alive: true, health: 2 } ]; } // Procedural texture generation function createTexture(w, h, type) { const data = new Uint8ClampedArray(w * h * 4); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const i = (y * w + x) * 4; if (type === 0) { // Brick wall const pattern = ((Math.floor(x / 8) + Math.floor(y / 8)) % 2 === 0); const c = pattern ? 200 : 150; data[i] = c; data[i+1] = c * 0.7; data[i+2] = c * 0.4; data[i+3] = 255; } else if (type === 1) { // Stone wall const noise = (Math.sin(x * 0.3) * Math.cos(y * 0.2) + 1) / 2; const c = 180 + noise * 40; data[i] = c; data[i+1] = c * 0.8; data[i+2] = c * 0.6; data[i+3] = 255; } else if (type === 2) { // Metal wall const strip = Math.floor(x / 4) % 2; const c = strip ? 220 : 180; data[i] = c; data[i+1] = c; data[i+2] = c; data[i+3] = 255; } else if (type === 3) { // Enemy texture const r = Math.sin(x * 0.5) * 50 + 100; const g = Math.cos(y * 0.5) * 50 + 100; const b = Math.sin(x * 0.3 + y * 0.3) * 50 + 100; data[i] = r; data[i+1] = g; data[i+2] = b; data[i+3] = 255; } else if (type === 4) { // Floor texture const pattern = (Math.floor(x / 16) + Math.floor(y / 16)) % 2; const c = pattern ? 80 : 100; data[i] = c; data[i+1] = c * 0.8; data[i+2] = c * 0.6; data[i+3] = 255; } else if (type === 5) { // Ceiling texture const pattern = (Math.floor(x / 32) + Math.floor(y / 32)) % 2; const c = pattern ? 60 : 80; data[i] = c; data[i+1] = c; data[i+2] = c; data[i+3] = 255; } else if (type === 6) { // Blood texture const noise = (Math.sin(x * 0.5) * Math.cos(y * 0.5) + 1) / 2; const r = 150 + noise * 50; const g = 30 + noise * 20; const b = 30 + noise * 20; data[i] = r; data[i+1] = g; data[i+2] = b; data[i+3] = 255; } return data; } } const img = new ImageData(data, w, h); return img; } // Global textures let wallTextures = []; let floorCeilingTextures = []; let enemyTexture; function initTextures() { // Wall textures wallTextures = [ createTexture(64, 64, 0), createTexture(64, 64, 1), createTexture(64, 64, 2) ]; // Floor and ceiling floorCeilingTextures = [ createTexture(32, 32, 4), createTexture(32, 32, 5) ]; // Enemy texture enemyTexture = createTexture(32, 32, 3); } // Raycasting function castRay(rayAngle, x) { const sin = Math.sin(rayAngle); const cos = Math.cos(rayAngle); let shadow = false; let maxDist = MaxDist; // Check for enemies in the ray path for (let e of enemies) { if (!e.alive) continue; const dx = e.x - player.x; const dy = e.y - player.y; const dist = Math.sqrt(dx*dx + dy*dy); const angleToEnemy = Math.atan2(dy, dx); const angleDiff = Math.abs(angleDiff(angleToEnemy - player.dir)); if (dist < 1.5 && angleDiff < 0.1) { // Hit enemy return { hit: 'enemy', dist: dist, x: e.x, y: e.y }; } } // DDA algorithm for wall collision let mapX = Math.floor(player.x); let mapY = Math.floor(player.y); let deltaDistX = Math.abs(1 / cos); let deltaDistY = Math.abs(1 / sin); let sideDistX, sideDistY; let stepX, stepY; let side = 0; if (cos < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; } if (sin < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } // Perform DDA let hit = 0; while (!hit && maxDist > 0) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } if (mapX >= 0 && mapX < MapSize && mapY >= 0 && mapY < MapSize) { if (map[mapY][mapX] > 0) { hit = map[mapY][mapX]; maxDist -= 0.1; } } else { hit = 1; maxDist -= 0.1; } } if (maxDist <= 0) { return { hit: 'wall', dist: 999 }; } let perpWallDist; if (side === 0) { perpWallDist = (mapX - player.x + (1 - stepX) / 2) / cos; } else { perpWallDist = (mapY - player.y + (1 - stepY) / 2) / sin; } // Fish eye correction perpWallDist = perpWallDist * Math.cos(rayAngle - player.dir); return { hit: 'wall', dist: perpWallDist, mapX: mapX, mapY: mapY, side: side, wallType: hit }; } // Calculate angle difference function angleDiff(a) { while (a > PI) a -= 2 * PI; while (a < -PI) a += 2 * PI; return a; } // Render scene function render() { const w = canvas.width = CanvasW; const h = canvas.height = CanvasH; // Clear canvas ctx.fillStyle = '#000'; ctx.fillRect(0, 0, w, h); // Draw floor and ceiling for (let i = 0; i < w; i += 32) { const texX = (i / 32) % 2; for (let j = h/2; j < h; j += 32) { const texY = ((j - h/2) / 32) % 2; const pattern = (texX + texY) % 2; ctx.fillStyle = pattern ? '#505050' : '#404040'; ctx.fillRect(i, j, 32, 32); } for (let j = 0; j < h/2; j += 32) { const texY = (j / 32) % 2; const pattern = (texX + texY) % 2; ctx.fillStyle = pattern ? '#303030' : '#202020'; ctx.fillRect(i, j, 32, 32); } } // Raycasting for walls const projPlaneDist = (CanvasW / 2) / Math.tan(HalfFov / 2); for (let x = 0; x < w; x++) { const cameraX = (x / w) * 2 - 1; const rayAngle = player.dir - cameraX * HalfFov; const result = castRay(rayAngle, x); if (result.hit === 'enemy') { const lineHeight = Math.floor(h / result.dist); const drawStart = Math.floor((h - lineHeight) / 2); const drawEnd = Math.floor((h + lineHeight) / 2); ctx.fillStyle = '#ff0000'; ctx.fillRect(x, drawStart, 1, lineHeight); } else if (result.hit === 'wall') { const lineHeight = Math.floor(h / (result.dist + 0.0001)); const drawStart = Math.floor((h - lineHeight) / 2); const drawEnd = Math.floor((h + lineHeight) / 2); if (lineHeight > 0 && drawStart < h && drawEnd > 0) { // Calculate texture coordinate let texX; if (result.side === 0) { texX = Math.floor((player.y + result.dist * Math.sin(rayAngle) - Math.floor(player.y + result.dist * Math.sin(rayAngle))) * 64); } else { texX = Math.floor((player.x + result.dist * Math.cos(rayAngle) - Math.floor(player.x + result.dist * Math.cos(rayAngle))) * 64); } // Get texture const texture = wallTextures[result.wallType - 1] || wallTextures[0]; // Draw wall slice ctx.fillStyle = '#808080'; for (let y = drawStart; y < drawEnd; y++) { const texY = Math.floor(((y - drawStart) / lineHeight) * 64); const color = texture.data[(texY * 64 + texX) * 4 + 0]; const r = texture.data[(texY * 64 + texX) * 4 + 0]; const g = texture.data[(texY * 64 + texX) * 4 + 1]; const b = texture.data[(texY * 64 + texX) * 4 + 2]; ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.fillRect(x, y, 1, 1); } } } } // Draw enemies (simple sprite approach) for (let e of enemies) { if (!e.alive) continue; const dx = e.x - player.x; const dy = e.y - player.y; const dist = Math.sqrt(dx*dx + dy*dy); const angleToEnemy = Math.atan2(dy, dx); const angleDiff = Math.abs(angleDiff(angleToEnemy - player.dir)); if (dist < 3 && angleDiff < HalfFov) { const screenX = Math.floor((angleDiff / HalfFov + 0.5) * w / 2); const lineHeight = Math.floor(h / dist); const drawStart = Math.floor((h - lineHeight) / 2); const drawEnd = Math.floor((h + lineHeight) / 2); // Draw enemy sprite for (let y = Math.max(0, drawStart); y < Math.min(h, drawEnd); y++) { const texY = Math.floor(((y - drawStart) / lineHeight) * 32); for (let x = screenX - 4; x < screenX + 4; x++) { if (x >= 0 && x < w) { const texX = Math.floor(((x - screenX + 4) / 8) * 32) % 32; const color = enemyTexture.data[(texY * 32 + texX) * 4 + 0]; const r = enemyTexture.data[(texY * 32 + texX) * 4 + 0]; const g = enemyTexture.data[(texY * 32 + texX) * 4 + 1]; const b = enemyTexture.data[(texY * 32 + texX) * 4 + 2]; ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.fillRect(x, y, 1, 1); } } } } } // Update UI document.getElementById('score').textContent = `Score: ${player.score}`; document.getElementById('health').textContent = `Health: ${player.health}`; } // Movement function movePlayer() { const speed = 0.05; const rotSpeed = 0.04; let moveX = 0; let moveY = 0; if (keys['KeyW']) { moveX += Math.cos(player.dir) * speed; moveY += Math.sin(player.dir) * speed; } if (keys['KeyS']) { moveX -= Math.cos(player.dir) * speed; moveY -= Math.sin(player.dir) * speed; } if (keys['KeyA']) { moveX -= Math.cos(player.dir - PI/2) * speed; moveY -= Math.sin(player.dir - PI/2) * speed; } if (keys['KeyD']) { moveX += Math.cos(player.dir - PI/2) * speed; moveY += Math.sin(player.dir - PI/2) * speed; } // Check collision with walls const newX = player.x + moveX; const newY = player.y + moveY; if (map[Math.floor(newY)][Math.floor(player.x)] === 0) { player.x = newX; } if (map[Math.floor(player.y)][Math.floor(newX)] === 0) { player.y = newY; } // Rotation if (keys['ArrowLeft'] || mouseDelta < 0) { player.dir -= rotSpeed; } if (keys['ArrowRight'] || mouseDelta > 0) { player.dir += rotSpeed; } mouseDelta = 0; // Keep angle in range if (player.dir > PI) player.dir -= 2 * PI; if (player.dir < -PI) player.dir += 2 * PI; } // Shooting function shoot() { if (shootTimer > 0) return; shootTimer = 20; shooting = true; // Cast a ray forward to check for enemy hit const rayAngle = player.dir; const sin = Math.sin(rayAngle); const cos = Math.cos(rayAngle); let mapX = Math.floor(player.x); let mapY = Math.floor(player.y); let deltaDistX = Math.abs(1 / cos); let deltaDistY = Math.abs(1 / sin); let sideDistX, sideDistY; let stepX, stepY; if (cos < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; } if (sin < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } let hit = false; while (!hit && sideDistX < 10 && sideDistY < 10) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; } else { sideDistY += deltaDistY; mapY += stepY; } if (mapX >= 0 && mapX < MapSize && mapY >= 0 && mapY < MapSize) { if (map[mapY][mapX] > 0) { hit = true; } } } // Check enemy hit for (let e of enemies) { if (!e.alive) continue; const dx = e.x - player.x; const dy = e.y - player.y; const dist = Math.sqrt(dx*dx + dy*dy); const angleToEnemy = Math.atan2(dy, dx); const diff = Math.abs(angleDiff(angleToEnemy - player.dir)); if (dist < 2 && diff < 0.1) { e.health--; if (e.health <= 0) { e.alive = false; player.score += 100; showMessage("KILL!"); } } } } // Show message function showMessage(text) { const msg = document.getElementById('message'); msg.textContent = text; msg.style.display = 'block'; setTimeout(() => { msg.style.display = 'none'; }, 500); } // Game over function gameOver() { death = true; const msg = document.getElementById('message'); msg.textContent = 'GAME OVER - Press R to Restart'; msg.style.display = 'block'; } // Main game loop function gameLoop() { if (!death) { movePlayer(); } render(); // Update shoot timer if (shootTimer > 0) { shootTimer--; } // Draw muzzle flash if (shooting) { ctx.fillStyle = 'rgba(255, 200, 100, 0.5)'; ctx.fillRect(CanvasW/2 - 10, CanvasH/2 - 10, 20, 20); shooting = false; } if (!death) { requestAnimationFrame(gameLoop); } } // Event listeners function initEvents() { window.addEventListener('keydown', (e) => { keys[e.code] = true; if (e.code === 'Space') { e.preventDefault(); if (!death) shoot(); } if (e.code === 'KeyR' && death) { restart(); } }); window.addEventListener('keyup', (e) => { keys[e.code] = false; }); canvas.addEventListener('mousemove', (e) => { if (canvas.ownerDocument.defaultView === window) { mouseDelta += e.movementX || 0; } }); // Request pointer lock on canvas click canvas.addEventListener('click', () => { canvas.requestPointerLock(); }); } function restart() { player = { x: 3.5, y: 3.5, dir: 0, health: 100, score: 0 }; death = false; initEnemies(); gameLoop(); } // Initialize function init() { canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); initTextures(); initMap(); initEnemies(); initEvents(); gameLoop(); } // Start game init(); </script> </body> </html>
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.