Wolfenstein raycaster · deepseek-chat
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.
agent produced no index.html
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 2 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.
Here's a complete implementation of a Wolfenstein 3D-style first-person raycasting shooter in a single `index.html` file. It includes procedural textures, WASD movement, mouse controls for turning, and enemies you can shoot. function<|tool▁sep|>write json {"path": "index.html", "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>Wolfenstein 3D-Style Raycasting Shooter</title>\n <style>\n body {\n margin: 0;\n padding: 0;\n overflow: hidden;\n background: #000;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n }\n canvas {\n display: block;\n }\n #ui {\n position: absolute;\n top: 10px;\n left: 10px;\n color: white;\n font-family: monospace;\n pointer-events: none;\n }\n </style>\n</head>\n<body>\n <canvas id=\"gameCanvas\"></canvas>\n <div id=\"ui\">Health: <span id=\"health\">100</span> | Enemies: <span id=\"enemies\">0</span></div>\n\n <script>\n const canvas = document.getElementById('gameCanvas');\n const ctx = canvas.getContext('2d');\n const uiHealth = document.getElementById('health');\n const uiEnemies = document.getElementById('enemies');\n\n // Set canvas size to window size\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n\n // Game state\n const state = {\n player: {\n x: 1.5,\n y: 1.5,\n dirX: 1,\n dirY: 0,\n planeX: 0,\n planeY: 0.66,\n speed: 0.05,\n rotSpeed: 0.04,\n health: 100\n },\n enemies: [],\n map: [\n [1, 1, 1, 1, 1, 1, 1, 1],\n [1, 0, 0, 0, 0, 0, 0, 1],\n [1, 0, 2, 0, 0, 2, 0, 1],\n [1, 0, 0, 0, 0, 0, 0, 1],\n [1, 0, 0, 0, 0, 0, 0, 1],\n [1, 0, 2, 0, 0, 2, 0, 1],\n [1, 0, 0, 0, 0, 0, 0, 1],\n [1, 1, 1, 1, 1, 1, 1, 1]\n ],\n textures: {\n wall: null,\n enemy: null\n },\n keys: {},\n mouse: {\n x: 0,\n y: 0,\n prevX: 0,\n locked: false\n },\n shots: [],\n gameOver: false\n };\n\n // Generate procedural textures\n function generateTextures() {\n // Wall texture (brick pattern)\n state.textures.wall = document.createElement('canvas');\n state.textures.wall.width = 64;\n state.textures.wall.height = 64;\n const wallCtx = state.textures.wall.getContext('2d');\n \n wallCtx.fillStyle = '#8B4513';\n wallCtx.fillRect(0, 0, 64, 64);\n \n wallCtx.fillStyle = '#A0522D';\n for (let y = 0; y < 8; y++) {\n for (let x = 0; x < 8; x++) {\n if ((x + y) % 2 === 0) {\n wallCtx.fillRect(x * 8, y * 8, 6, 6);\n }\n }\n }\n \n // Enemy texture\n state.textures.enemy = document.createElement('canvas');\n state.textures.enemy.width = 64;\n state.textures.enemy.height = 64;\n const enemyCtx = state.textures.enemy.getContext('2d');\n \n enemyCtx.fillStyle = '#333';\n enemyCtx.beginPath();\n enemyCtx.arc(32, 32, 30, 0, Math.PI * 2);\n enemyCtx.fill();\n \n enemyCtx.fillStyle = '#FF0000';\n enemyCtx.beginPath();\n enemyCtx.arc(32, 32, 25, 0, Math.PI * 2);\n enemyCtx.fill();\n \n enemyCtx.fillStyle = '#000';\n enemyCtx.beginPath();\n enemyCtx.arc(20, 20, 5, 0, Math.PI * 2);\n enemyCtx.arc(44, 20, 5, 0, Math.PI * 2);\n enemyCtx.fill();\n \n enemyCtx.strokeStyle = '#000';\n enemyCtx.lineWidth = 2;\n enemyCtx.beginPath();\n enemyCtx.arc(32, 35, 10, 0, Math.PI);\n enemyCtx.stroke();\n }\n\n // Generate enemies\n function generateEnemies() {\n for (let y = 0; y < state.map.length; y++) {\n for (let x = 0; x < state.map[y].length; x++) {\n if (state.map[y][x] === 2) {\n state.enemies.push({\n x: x + 0.5,\n y: y + 0.5,\n health: 100,\n speed: 0.02,\n damage: 5,\n alive: true\n });\n }\n }\n }\n updateUI();\n }\n\n // Update UI elements\n function updateUI() {\n uiHealth.textContent = state.player.health;\n uiEnemies.textContent = state.enemies.filter(e => e.alive).length;\n }\n\n // Mouse lock handling\n function initMouse() {\n canvas.addEventListener('click', () => {\n canvas.requestPointerLock = canvas.requestPointerLock ||\n canvas.mozRequestPointerLock ||\n canvas.webkitRequestPointerLock;\n canvas.requestPointerLock();\n });\n\n document.addEventListener('pointerlockchange', lockChange, false);\n document.addEventListener('mozpointerlockchange', lockChange, false);\n \n function lockChange() {\n if (document.pointerLockElement === canvas ||\n document.mozPointerLockElement === canvas) {\n state.mouse.locked = true;\n document.addEventListener('mousemove', mouseMove, false);\n } else {\n state.mouse.locked = false;\n document.removeEventListener('mousemove', mouseMove, false);\n }\n }\n \n function mouseMove(e) {\n if (!state.mouse.locked) return;\n state.mouse.x += e.movementX;\n }\n }\n\n // Keyboard controls\n function initKeyboard() {\n document.addEventListener('keydown', (e) => {\n state.keys[e.key.toLowerCase()] = true;\n if (e.key === ' ') {\n shoot();\n }\n });\n \n document.addEventListener('keyup', (e) => {\n state.keys[e.key.toLowerCase()] = false;\n });\n }\n\n // Shooting mechanics\n function shoot() {\n // Check if player can shoot (cooldown could be added)\n state.shots.push({\n x: state.player.x,\n y: state.player.y,\n dirX: state.player.dirX,\n dirY: state.player.dirY,\n distance: 0,\n maxDistance: 10\n });\n }\n\n // Check for hits\n function checkHits() {\n for (let i = state.shots.length - 1; i >= 0; i--) {\n const shot = state.shots[i];\n shot.distance += 0.2;\n shot.x += shot.dirX * 0.2;\n shot.y += shot.dirY * 0.2;\n \n // Check if shot hit a wall\n const mapX = Math.floor(shot.x);\n const mapY = Math.floor(shot.y);\n if (state.map[mapY] && state.map[mapY][mapX] === 1) {\n state.shots.splice(i, 1);\n continue;\n }\n \n // Check if shot hit an enemy\n for (const enemy of state.enemies) {\n if (enemy.alive && Math.hypot(shot.x - enemy.x, shot.y - enemy.y) < 0.5) {\n enemy.health -= 25;\n if (enemy.health <= 0) {\n enemy.alive = false;\n updateUI();\n }\n state.shots.splice(i, 1);\n break;\n }\n }\n \n // Remove shot if it travels too far\n if (shot.distance >= shot.maxDistance) {\n state.shots.splice(i, 1);\n }\n }\n }\n\n // Enemy AI\n function updateEnemies() {\n for (const enemy of state.enemies) {\n if (!enemy.alive) continue;\n \n // Calculate vector to player\n const dx = state.player.x - enemy.x;\n const dy = state.player.y - enemy.y;\n const dist = Math.hypot(dx, dy);\n \n // Move toward player\n if (dist > 1) {\n enemy.x += (dx / dist) * enemy.speed;\n enemy.y += (dy / dist) * enemy.speed;\n } else {\n // Attack player\n state.player.health -= enemy.damage * 0.1;\n updateUI();\n if (state.player.health <= 0) {\n state.gameOver = true;\n }\n }\n }\n }\n\n // Movement and rotation\n function updatePlayer(dt) {\n if (state.gameOver) return;\n \n // Rotation with mouse\n if (state.mouse.locked) {\n const rotSpeed = state.player.rotSpeed * dt * 10;\n const oldDirX = state.player.dirX;\n state.player.dirX = state.player.dirX * Math.cos(-state.mouse.x * rotSpeed) - state.player.dirY * Math.sin(-state.mouse.x * rotSpeed);\n state.player.dirY = oldDirX * Math.sin(-state.mouse.x * rotSpeed) + state.player.dirY * Math.cos(-state.mouse.x * rotSpeed);\n \n const oldPlaneX = state.player.planeX;\n state.player.planeX = state.player.planeX * Math.cos(-state.mouse.x * rotSpeed) - state.player.planeY * Math.sin(-state.mouse.x * rotSpeed);\n state.player.planeY = oldPlaneX * Math.sin(-state.mouse.x * rotSpeed) + state.player.planeY * Math.cos(-state.mouse.x * rotSpeed);\n \n state.mouse.x = 0;\n }\n \n // Movement\n const moveSpeed = state.player.speed * dt * 10;\n if (state.keys['w']) {\n if (state.map[Math.floor(state.player.y)][Math.floor(state.player.x + state.player.dirX * moveSpeed * 2)] === 0) {\n state.player.x += state.player.dirX * moveSpeed;\n }\n if (state.map[Math.floor(state.player.y + state.player.dirY * moveSpeed * 2)][Math.floor(state.player.x)] === 0) {\n state.player.y += state.player.dirY * moveSpeed;\n }\n }\n if (state.keys['s']) {\n if (state.map[Math.floor(state.player.y)][Math.floor(state.player.x - state.player.dirX * moveSpeed * 2)] === 0) {\n state.player.x -= state.player.dirX * moveSpeed;\n }\n if (state.map[Math.floor(state.player.y - state.player.dirY * moveSpeed * 2)][Math.floor(state.player.x)] === 0) {\n state.player.y -= state.player.dirY * moveSpeed;\n }\n }\n if (state.keys['a']) {\n // Strafe left\n if (state.map[Math.floor(state.player.y)][Math.floor(state.player.x - state.player.dirY * moveSpeed * 2)] === 0) {\n state.player.x -= state.player.dirY * moveSpeed;\n }\n if (state.map[Math.floor(state.player.y + state.player.dirX * moveSpeed * 2)][Math.floor(state.player.x)] === 0) {\n state.player.y += state.player.dirX * moveSpeed;\n }\n }\n if (state.keys['d']) {\n // Strafe right\n if (state.map[Math.floor(state.player.y)][Math.floor(state.player.x + state.player.dirY * moveSpeed * 2)] === 0) {\n state.player.x += state.player.dirY * moveSpeed;\n }\n if (state.map[Math.floor(state.player.y - state.player.dirX * moveSpeed * 2)][Math.floor(state.player.x)] === 0) {\n state.player.y -= state.player.dirX * moveSpeed;\n }\n }\n }\n\n // Raycasting\n function castRays() {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n \n // Draw ceiling and floor\n const height = canvas.height;\n ctx.fillStyle = '#555';\n ctx.fillRect(0, 0, canvas.width, height / 2);\n ctx.fillStyle = '#333';\n ctx.fillRect(0, height / 2, canvas.width, height / 2);\n \n for (let x = 0; x < canvas.width; x++) {\n // Calculate ray position and direction\n const cameraX = 2 * x / canvas.width - 1;\n const rayDirX = state.player.dirX + state.player.planeX * cameraX;\n const rayDirY = state.player.dirY + state.player.planeY * cameraX;\n \n // Current player position in map\n let mapX = Math.floor(state.player.x);\n let mapY = Math.floor(state.player.y);\n \n // Length of ray from one x or y-side to next x or y-side\n let deltaDistX = Math.abs(1 / rayDirX);\n let deltaDistY = Math.abs(1 / rayDirY);\n \n let sideDistX, sideDistY;\n \n // Direction to step in x or y direction (either +1 or -1)\n let stepX, stepY;\n \n if (rayDirX < 0) {\n stepX = -1;\n sideDistX = (state.player.x - mapX) * deltaDistX;\n } else {\n stepX = 1;\n sideDistX = (mapX + 1.0 - state.player.x) * deltaDistX;\n }\n \n if (rayDirY < 0) {\n stepY = -1;\n sideDistY = (state.player.y - mapY) * deltaDistY;\n } else {\n stepY = 1;\n sideDistY = (mapY + 1.0 - state.player.y) * deltaDistY;\n }\n \n // Perform DDA (Digital Differential Analysis)\n let hit = false;\n let side;\n \n while (!hit) {\n if (sideDistX < sideDistY) {\n sideDistX += deltaDistX;\n mapX += stepX;\n side = 0;\n } else {\n sideDistY += deltaDistY;\n mapY += stepY;\n side = 1;\n }\n \n // Check if ray hit\n if (state.map[mapY][mapX] > 0) {\n hit = true;\n }\n }\n \n // Calculate distance projected on camera direction\n let wallDist;\n if (side === 0) {\n wallDist = (mapX - state.player.x + (1 - stepX) / 2) / rayDirX;\n } else {\n wallDist = (mapY - state.player.y + (1 - stepY) / 2) / rayDirY;\n }\n \n // Calculate height of wall slice to draw\n let lineHeight = Math.floor(canvas.height / wallDist);\n \n // Calculate lowest and highest pixel to fill in current stripe\n let drawStart = -lineHeight / 2 + canvas.height / 2;\n if (drawStart < 0) drawStart = 0;\n let drawEnd = lineHeight / 2 + canvas.height / 2;\n if (drawEnd >= canvas.height) drawEnd = canvas.height - 1;\n \n // Choose wall color based on map value and side\n let color;\n const val = state.map[mapY][mapX];\n if (val === 1) {\n if (side === 1) color = '#8B4513'; // Darker color for y-sides (north/south)\n else color = '#A0522D';\n } else if (val === 2) {\n color = '#FF0000';\n }\n \n // Draw the wall slice\n if (val === 1 || val === 2) {\n ctx.fillStyle = color;\n ctx.fillRect(x, drawStart, 1, drawEnd - drawStart);\n }\n \n // Textured walls\n if (val === 1) {\n // Calculate texture coordinate\n let texX;\n let wallX;\n if (side === 0) wallX = state.player.y + wallDist * rayDirY;\n else wallX = state.player.x + wallDist * rayDirX;\n wallX -= Math.floor(wallX);\n \n texX = Math.floor(wallX * state.textures.wall.width);\n if (side === 0 && rayDirX > 0) texX = state.textures.wall.width - texX - 1;\n if (side === 1 && rayDirY < 0) texX = state.textures.wall.width - texX - 1;\n \n // Draw textured wall slice\n const texH = lineHeight;\n const texYStart = (state.textures.wall.height - texH) / 2;\n \n ctx.drawImage(\n state.textures.wall,\n texX, texYStart, 1, state.textures.wall.height,\n x, drawStart, 1, drawEnd - drawStart\n );\n }\n }\n \n // Draw enemies\n for (const enemy of state.enemies) {\n if (!enemy.alive) continue;\n \n // Calculate enemy position relative to player\n const dx = enemy.x - state.player.x;\n const dy = enemy.y - state.player.y;\n \n // Determine sprite position on screen\n const invDet = 1.0 / (state.player.planeX * state.player.dirY - state.player.dirX * state.player.planeY);\n const transformX = invDet * (state.player.dirY * dx - state.player.dirX * dy);\n const transformY = invDet * (-state.player.planeY * dx + state.player.planeX * dy);\n \n const spriteScreenX = Math.floor((canvas.width / 2) * (1 + transformX / transformY));\n \n // Calculate sprite height and width\n const spriteHeight = Math.abs(Math.floor(canvas.height / transformY));\n const spriteWidth = Math.abs(Math.floor(canvas.height / transformY));\n \n // Calculate drawing area\n let drawStartY = -spriteHeight / 2 + canvas.height / 2;\n if (drawStartY < 0) drawStartY = 0;\n let drawEndY = spriteHeight / 2 + canvas.height / 2;\n if (drawEndY >= canvas.height) drawEndY = canvas.height - 1;\n \n let drawStartX = -spriteWidth / 2 + spriteScreenX;\n if (drawStartX < 0) drawStartX = 0;\n let drawEndX = spriteWidth / 2 + spriteScreenX;\n if (drawEndX >= canvas.width) drawEndX = canvas.width - 1;\n \n // Draw enemy\n ctx.drawImage(\n state.textures.enemy,\n 0, 0, state.textures.enemy.width, state.textures.enemy.height,\n drawStartX, drawStartY, drawEndX - drawStartX, drawEndY - drawStartY\n );\n }\n \n // Draw shots\n ctx.fillStyle = '#FFFF00';\n for (const shot of state.shots) {\n const dx = shot.x - state.player.x;\n const dy = shot.y - state.player.y;\n \n const invDet = 1.0 / (state.player.planeX * state.player.dirY - state.player.dirX * state.player.planeY);\n const transformX = invDet * (state.player.dirY * dx - state.player.dirX * dy);\n const transformY = invDet * (-state.player.planeY * dx + state.player.planeX * dy);\n \n const spriteScreenX = Math.floor((canvas.width / 2) * (1 + transformX / transformY));\n \n ctx.beginPath();\n ctx.arc(spriteScreenX, canvas.height / 2, 2, 0, Math.PI * 2);\n ctx.fill();\n }\n \n // Draw gun\n if (!state.gameOver) {\n ctx.drawImage(\n state.textures.enemy,\n 0, 0, state.textures.enemy.width, state.textures.enemy.height,\n canvas.width / 2 - 100, canvas.height - 200, 200, 200\n );\n }\n \n // Draw game over screen\n if (state.gameOver) {\n ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = '#FF0000';\n ctx.font = '48px monospace';\n ctx.textAlign = 'center';\n ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2);\n ctx.font = '24px monospace';\n ctx.fillText('Click to restart', canvas.width / 2, canvas.height / 2 + 50);\n \n canvas.addEventListener('click', restartGame, { once: true });\n }\n }\n\n // Restart game\n function restartGame() {\n state.player = {\n x: 1.5,\n y: 1.5,\n dirX: 1,\n dirY: 0,\n planeX: 0,\n planeY: 0.66,\n speed: 0.05,\n rotSpeed: 0.04,\n health: 100\n };\n state.enemies = [];\n state.shots = [];\n state.gameOver = false;\n generateEnemies();\n }\n\n // Main game loop\n let lastTime = 0;\n function gameLoop(time) {\n const deltaTime = (time - lastTime) / 1000;\n lastTime = time;\n \n updatePlayer(deltaTime);\n updateEnemies();\n checkHits();\n castRays();\n \n requestAnimationFrame(gameLoop);\n }\n\n // Initialize the game\n function init() {\n generateTextures();\n generateEnemies();\n initMouse();\n initKeyboard();\n requestAnimationFrame(gameLoop);\n }\n\n window.addEventListener('load', init);\n window.addEventListener('resize', () => {\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n });\n </script>\n</body>\n</html>"}
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.