Wolfenstein raycaster · deepseek-r1
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.
thinking
We are going to build a single-page web app that is a first-person raycasting maze shooter in the style of Wolfenstein 3D.
The entire app must be in one file: index.html, with CSS in a <style> tag and JavaScript in a <script> tag.
We cannot use external resources, so all textures must be procedural (generated by code).
Steps:
1. We'll create an HTML5 canvas for rendering.
2. We'll set up the game loop and handle user input for movement (WASD) and turning (arrow keys or mouse).
3. We'll generate a maze (grid of walls and paths) and place the player and enemies.
4. We'll implement raycasting to render the 3D view.
5. We'll generate procedural textures for walls, floor, ceiling, and enemies.
6. We'll implement shooting mechanics.
Important: The entire code must be self-contained in one HTML file.
Let's break down the components:
HTML Structure:
- A <canvas> element for rendering the game.
- Possibly a UI overlay for health, ammo, etc.
CSS:
- Style the canvas to take the full viewport.
JavaScript:
- Define constants for the game (e.g., canvas dimensions, player settings, etc.)
- Generate the maze (we can use a simple algorithm like recursive backtracking).
- Define the player: position, direction, movement speed, turning speed.
- Define enemies: position, health, AI (simple movement towards player?).
- Raycasting engine: cast rays for each column of the canvas and draw walls with textures.
- Textures: create functions to generate canvas patterns or use offscreen canvases for textures.
- Input handling: listen for key events for WASD and arrow keys, and mouse movement for turning.
- Shooting: when the user clicks or presses a key (e.g., space), check if an enemy is hit.
Since we cannot use external assets, we'll generate textures on the fly. For example:
- Wall textures: we can create a checkerboard pattern or random noise patterns.
- Enemies: we can draw them as 2D sprites using the canvas API.
We'll structure the code:
<!DOCTYPE html>
<html>
<head>
<title>Raycasting Maze Shooter</title>
<style>
body { margin: 0; padding: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// All JavaScript code here
// Constants and settings
const WIDTH = 800;
const HEIGHT = 600;
const FOV = Math.PI / 3; // Field of view in radians
// ... rest of the code ...
</script>
</body>
</html>
We'll write the entire code in one go and then write it to index.html.
However, note: the problem says "End with index.html present", so we must write the entire HTML content to a file named index.html.
Since we are in the current directory /tmp/ws, we will write the file there.
Let's generate the content for index.html.
Due to the complexity, we'll break the JavaScript into sections:
1. Canvas setup and context
2. Maze generation
3. Player and enemy definitions
4. Texture generation
5. Input handling
6. Raycasting and rendering
7. Game loop
We'll try to keep it as concise as possible but complete.
Note: This is a non-trivial task. We'll write a simplified version.
Implementation plan for raycasting:
We'll use a grid for the maze (2D array). Each cell is either a wall (1) or empty (0).
We'll cast rays for each column of the canvas. For each ray, we step through the grid until we hit a wall.
We then calculate the distance and the height of the wall slice to draw.
We'll use a texture for the wall slice. Since we are generating textures procedurally, we can create a pattern for each wall type.
For enemies, we can represent them as objects with positions and then during raycasting, we can also check for enemy sprites.
However, note: raycasting engines that include sprites usually do a second pass for sprites. We'll keep it simple and only have walls for the raycasting and then draw enemies as 2D sprites on top? But that wouldn't be 3D. Alternatively, we can do sprite casting as in the original Wolfenstein.
Given the complexity, we'll focus on walls first and then add enemies as sprites that are drawn in the 3D view.
Steps for sprite casting:
- For each enemy, transform their position relative to the camera.
- Calculate the distance and the screen position (x, y, height).
- Draw the enemy texture at that position.
We'll generate a simple enemy texture (a colored rectangle or a simple drawn shape).
Let's start coding.
We'll write the entire HTML content as a string and then write it to index.html.
However, note: the instructions say we can use the write function to write the file.
We'll compose the entire HTML content and then call write with path: "index.html" and content: the HTML string.
Since the code is long, we'll break it into manageable parts.
Important: We must not exceed the token limit in the function call. We'll write the entire HTML content as a string.
Let's generate the content.
I'll create a self-contained Wolfenstein-style raycasting shooter with procedural textures. Here's the complete implementation: ```html <!DOCTYPE html> <html> <head> <title>Raycasting Maze Shooter</title> <style> body { margin: 0; padding: 0; overflow: hidden; background: #000; font-family: 'Courier New', monospace; color: #fff; } canvas { display: block; } #ui { position: absolute; top: 10px; left: 10px; z-index: 10; } #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgba(255, 0, 0, 0.8); font-size: 24px; z-index: 5; pointer-events: none; } </style> </head> <body> <canvas id="gameCanvas"></canvas> <div id="ui">Health: <span id="health">100</span> | Enemies: <span id="enemies">5</span></div> <div id="crosshair">+</div> <script> // Game constants const WIDTH = 800; const HEIGHT = 600; const FOV = Math.PI / 3; // Field of view const RESOLUTION = 120; // Number of rays cast const WALL_HEIGHT = 200; const MOVE_SPEED = 0.1; const ROT_SPEED = 0.05; const ENEMY_SPEED = 0.02; const SHOOT_DISTANCE = 3; // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = WIDTH; canvas.height = HEIGHT; // Game state const player = { x: 1.5, y: 1.5, angle: 0, health: 100 }; let enemies = [ { x: 5.5, y: 5.5, alive: true }, { x: 8.5, y: 3.5, alive: true }, { x: 3.5, y: 8.5, alive: true }, { x: 7.5, y: 7.5, alive: true }, { x: 2.5, y: 2.5, alive: true } ]; // Maze definition (1 = wall, 0 = path) const maze = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 1, 1, 0, 1, 0, 1], [1, 0, 1, 0, 0, 0, 0, 1, 0, 1], [1, 0, 1, 1, 1, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 1, 0, 1, 1, 1, 0, 1], [1, 0, 1, 0, 0, 0, 0, 1, 0, 1], [1, 0, 0, 0, 1, 1, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ]; // Input state const keys = {}; let mouseX = 0; let mouseDown = false; // Generate procedural texture patterns function createWallTexture() { const texCanvas = document.createElement('canvas'); texCanvas.width = 64; texCanvas.height = 64; const texCtx = texCanvas.getContext('2d'); // Base color texCtx.fillStyle = '#8B4513'; texCtx.fillRect(0, 0, 64, 64); // Brick pattern texCtx.strokeStyle = '#5D2906'; texCtx.lineWidth = 2; for (let y = 0; y < 64; y += 16) { for (let x = 0; x < 64; x += 32) { texCtx.strokeRect(x, y, 32, 16); } } // Noise effect const imgData = texCtx.getImageData(0, 0, 64, 64); const data = imgData.data; for (let i = 0; i < data.length; i += 4) { const noise = Math.random() * 20; data[i] = Math.max(0, data[i] - noise); data[i+1] = Math.max(0, data[i+1] - noise); data[i+2] = Math.max(0, data[i+2] - noise); } texCtx.putImageData(imgData, 0, 0); return texCanvas; } function createEnemyTexture() { const texCanvas = document.createElement('canvas'); texCanvas.width = 64; texCanvas.height = 64; const texCtx = texCanvas.getContext('2d'); // Body texCtx.fillStyle = '#8B0000'; texCtx.beginPath(); texCtx.arc(32, 32, 24, 0, Math.PI * 2); texCtx.fill(); // Eyes texCtx.fillStyle = '#000'; texCtx.beginPath(); texCtx.arc(24, 24, 4, 0, Math.PI * 2); texCtx.arc(40, 24, 4, 0, Math.PI * 2); texCtx.fill(); // Mouth texCtx.strokeStyle = '#000'; texCtx.lineWidth = 3; texCtx.beginPath(); texCtx.arc(32, 36, 10, 0, Math.PI); texCtx.stroke(); return texCanvas; } // Create textures const wallTexture = createWallTexture(); const enemyTexture = createEnemyTexture(); // Raycasting function function castRay(angle) { const rayDirX = Math.cos(angle); const rayDirY = Math.sin(angle); let mapX = Math.floor(player.x); let mapY = Math.floor(player.y); let deltaDistX = Math.abs(1 / rayDirX); let deltaDistY = Math.abs(1 / rayDirY); let stepX, stepY; let sideDistX, sideDistY; if (rayDirX < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1 - player.x) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1 - player.y) * deltaDistY; } let hit = false; let side; while (!hit) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } if (maze[mapY][mapX] > 0) hit = true; } let dist; if (side === 0) { dist = (mapX - player.x + (1 - stepX) / 2) / rayDirX; } else { dist = (mapY - player.y + (1 - stepY) / 2) / rayDirY; } return { dist: Math.abs(dist), side: side, mapX: mapX, mapY: mapY }; } // Draw enemy sprites function drawEnemies() { // Sort enemies by distance (farthest first) const visibleEnemies = enemies .filter(e => e.alive) .map(e => { const dx = e.x - player.x; const dy = e.y - player.y; const dist = Math.sqrt(dx*dx + dy*dy); const angle = Math.atan2(dy, dx) - player.angle; return { ...e, dist, angle }; }) .filter(e => Math.abs(e.angle) < FOV/2 && e.dist < 15) .sort((a, b) => b.dist - a.dist); for (const enemy of visibleEnemies) { // Calculate sprite position on screen const spriteHeight = Math.min(500, WALL_HEIGHT / enemy.dist * 2); const spriteWidth = spriteHeight; const spriteX = (enemy.angle * WIDTH / FOV) + WIDTH/2 - spriteWidth/2; const spriteY = HEIGHT/2 - spriteHeight/2; // Draw sprite ctx.drawImage( enemyTexture, 0, 0, enemyTexture.width, enemyTexture.height, spriteX, spriteY, spriteWidth, spriteHeight ); } } // Update enemy positions function updateEnemies() { for (const enemy of enemies) { if (!enemy.alive) continue; const dx = player.x - enemy.x; const dy = player.y - enemy.y; const dist = Math.sqrt(dx*dx + dy*dy); if (dist < 0.5) { player.health -= 0.5; document.getElementById('health').textContent = Math.floor(player.health); } else if (dist < 8) { enemy.x += dx / dist * ENEMY_SPEED; enemy.y += dy / dist * ENEMY_SPEED; } } } // Shooting mechanism function shoot() { // Cast a ray in the center of the screen const ray = castRay(player.angle); // Check if we hit an enemy for (const enemy of enemies) { if (!enemy.alive) continue; // Calculate distance to enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const dist = Math.sqrt(dx*dx + dy*dy); // Check if enemy is in shooting direction and range const angleToEnemy = Math.atan2(dy, dx); const angleDiff = Math.abs(angleToEnemy - player.angle); if (dist < SHOOT_DISTANCE && angleDiff < 0.1) { enemy.alive = false; updateEnemyCount(); break; } } } function updateEnemyCount() { const aliveCount = enemies.filter(e => e.alive).length; document.getElementById('enemies').textContent = aliveCount; } // Render the scene function render() { // Clear screen ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, WIDTH, HEIGHT/2); ctx.fillStyle = '#16213e'; ctx.fillRect(0, HEIGHT/2, WIDTH, HEIGHT/2); // Draw walls for (let col = 0; col < RESOLUTION; col++) { const rayAngle = player.angle - FOV/2 + (col / RESOLUTION) * FOV; const ray = castRay(rayAngle); // Calculate wall height const dist = ray.dist; const height = Math.min(500, WALL_HEIGHT / dist); // Calculate texture coordinates let texX; if (ray.side === 0) { texX = (player.y + ray.dist * Math.sin(rayAngle)) % 1; } else { texX = (player.x + ray.dist * Math.cos(rayAngle)) % 1; } texX = Math.floor(texX * wallTexture.width); // Draw wall slice const wallX = col * (WIDTH / RESOLUTION); const wallY = HEIGHT/2 - height/2; ctx.drawImage( wallTexture, texX, 0, 1, wallTexture.height, wallX, wallY, WIDTH/RESOLUTION + 1, height ); } // Draw enemies drawEnemies(); // Draw weapon ctx.fillStyle = '#333'; ctx.fillRect(WIDTH/2 - 20, HEIGHT - 100, 40, 80); ctx.fillRect(WIDTH/2 - 10, HEIGHT - 120, 20, 20); } // Handle keyboard input function handleInput() { // Rotation if (keys['ArrowLeft'] || keys['a']) player.angle -= ROT_SPEED; if (keys['ArrowRight'] || keys['d']) player.angle += ROT_SPEED; // Movement let moveX = 0; let moveY = 0; if (keys['w'] || keys['ArrowUp']) { moveX += Math.cos(player.angle) * MOVE_SPEED; moveY += Math.sin(player.angle) * MOVE_SPEED; } if (keys['s'] || keys['ArrowDown']) { moveX -= Math.cos(player.angle) * MOVE_SPEED; moveY -= Math.sin(player.angle) * MOVE_SPEED; } // Collision detection const newX = player.x + moveX; const newY = player.y + moveY; if (maze[Math.floor(newY)][Math.floor(newX)] === 0) { player.x = newX; player.y = newY; } else if (maze[Math.floor(newY)][Math.floor(player.x)] === 0) { player.y = newY; } else if (maze[Math.floor(player.y)][Math.floor(newX)] === 0) { player.x = newX; } } // Game loop function gameLoop() { handleInput(); updateEnemies(); render(); // Check game over if (player.health <= 0) { alert('Game Over!'); resetGame(); } else if (enemies.filter(e => e.alive).length === 0) { alert('You Win!'); resetGame(); } else { requestAnimationFrame(gameLoop); } } function resetGame() { player.x = 1.5; player.y = 1.5; player.angle = 0; player.health = 100; enemies.forEach(e => e.alive = true); document.getElementById('health').textContent = player.health; updateEnemyCount(); gameLoop(); } // Event listeners window.addEventListener('keydown', e => keys[e.key] = true); window.addEventListener('keyup', e => keys[e.key] = false); canvas.addEventListener('mousemove', e => { mouseX = e.clientX; player.angle += (e.movementX * 0.005); }); canvas.addEventListener('mousedown', () => { mouseDown = true; shoot(); }); canvas.addEventListener('mouseup', () => mouseDown = false); canvas.addEventListener('click', () => { if (document.pointerLockElement !== canvas) { canvas.requestPointerLock(); } }); // Initialize UI document.getElementById('health').textContent = player.health; updateEnemyCount(); // Start game gameLoop(); </script> </body> </html> ``` This implementation includes: 1. **Raycasting Engine**: - Casts rays for each column to render walls - Distance-based wall height calculation - Procedural brick wall textures - Field of view simulation 2. **Player Controls**: - WASD for movement - Mouse look for rotation - Click to shoot - Pointer lock for immersive experience 3. **Enemies**: - 5 enemies placed in maze - Simple AI to chase player - Shooting mechanics with hit detection - Procedural enemy sprites 4. **Game Elements**: - Health system - Enemy counter - Win/lose conditions - Maze collision detection 5. **Procedural Textures**: - Dynamically generated brick walls - Enemy sprites created programmatically - No external assets used The game features a dark color scheme with red enemies and a weapon visualization. All code is contained in a single HTML file with embedded CSS and JavaScript as required. Would you like me to explain any specific part of the implementation in more detail?
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.