Wolfenstein raycaster · qwen3-coder-30b-a3b-instruct
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.
```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 Style Raycasting</title> <style> body { margin: 0; padding: 0; overflow: hidden; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Courier New', monospace; } canvas { border: 1px solid #444; background: #222; } #gameInfo { position: absolute; top: 10px; left: 10px; color: white; font-size: 14px; z-index: 10; } #startScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 20; text-align: center; } button { background: #555; color: white; border: none; padding: 10px 20px; font-size: 18px; font-family: 'Courier New', monospace; cursor: pointer; margin-top: 20px; border-radius: 5px; } button:hover { background: #777; } h1 { color: #ff5555; text-shadow: 0 0 10px rgba(255, 85, 85, 0.7); margin-bottom: 20px; } </style> </head> <body> <div id="gameInfo"> <div>Health: <span id="health">100</span></div> <div>Enemies: <span id="enemies">0</span></div> <div>Ammo: <span id="ammo">50</span></div> </div> <div id="startScreen"> <h1>WOLFENSTEIN 3D</h1> <p>Use WASD to move, Arrow Keys or Mouse to look around</p> <p>Click to shoot enemies</p> <button id="startButton">START GAME</button> </div> <canvas id="gameCanvas" width="800" height="600"></canvas> <script> // Game constants const MAP_SIZE = 10; const TILE_SIZE = 64; const FOV = Math.PI / 3; // 60 degrees const PLAYER_HEIGHT = 32; const PLAYER_SPEED = 0.05; const ROTATION_SPEED = 0.04; const ENEMY_SPEED = 0.02; const BULLET_SPEED = 0.15; const MAX_ENEMIES = 5; const WALL_HEIGHT = 64; const TEXTURE_SIZE = 64; // Game state let player = { x: 1.5, y: 1.5, angle: 0, health: 100, ammo: 50 }; let map = []; let enemies = []; let bullets = []; let textures = []; let keys = {}; let mouseX = 0; let mouseY = 0; let mouseSensitivity = 0.002; let gameRunning = false; // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const startScreen = document.getElementById('startScreen'); const startButton = document.getElementById('startButton'); const healthDisplay = document.getElementById('health'); const enemiesDisplay = document.getElementById('enemies'); const ammoDisplay = document.getElementById('ammo'); // Initialize the game function init() { generateMap(); generateTextures(); generateEnemies(); // Event listeners window.addEventListener('keydown', (e) => keys[e.key] = true); window.addEventListener('keyup', (e) => keys[e.key] = false); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('click', shoot); startButton.addEventListener('click', startGame); // Prevent context menu on right click canvas.addEventListener('contextmenu', (e) => e.preventDefault()); // Start rendering loop requestAnimationFrame(gameLoop); } function generateMap() { // Create a simple maze-like map map = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 1, 0, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 1, 0, 0, 0, 1], [1, 1, 1, 1, 0, 1, 0, 1, 1, 1], [1, 0, 0, 0, 0, 1, 0, 0, 0, 1], [1, 0, 1, 1, 0, 1, 1, 1, 0, 1], [1, 0, 0, 1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ]; } function generateTextures() { // Create procedural textures textures = []; // Wall texture const wallCanvas = document.createElement('canvas'); wallCanvas.width = TEXTURE_SIZE; wallCanvas.height = TEXTURE_SIZE; const wallCtx = wallCanvas.getContext('2d'); // Create a pattern with different colors for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const color = (x + y) % 4; wallCtx.fillStyle = color === 0 ? '#444' : color === 1 ? '#666' : color === 2 ? '#888' : '#aaa'; wallCtx.fillRect(x, y, 1, 1); } } textures.wall = wallCanvas; // Enemy texture const enemyCanvas = document.createElement('canvas'); enemyCanvas.width = TEXTURE_SIZE; enemyCanvas.height = TEXTURE_SIZE; const enemyCtx = enemyCanvas.getContext('2d'); // Create a simple enemy shape - a red square with black details enemyCtx.fillStyle = '#ff0000'; enemyCtx.fillRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE); // Eyes enemyCtx.fillStyle = '#000'; enemyCtx.fillRect(15, 15, 10, 10); enemyCtx.fillRect(40, 15, 10, 10); // Mouth enemyCtx.fillRect(20, 40, 25, 5); textures.enemy = enemyCanvas; // Bullet texture const bulletCanvas = document.createElement('canvas'); bulletCanvas.width = 4; bulletCanvas.height = 4; const bulletCtx = bulletCanvas.getContext('2d'); bulletCtx.fillStyle = '#ffff00'; bulletCtx.fillRect(0, 0, 4, 4); textures.bullet = bulletCanvas; } function generateEnemies() { enemies = []; for (let i = 0; i < MAX_ENEMIES; i++) { let validPosition = false; let enemy = {}; // Find a valid position for the enemy (not on a wall) while (!validPosition) { enemy.x = Math.random() * (MAP_SIZE - 2) + 1; enemy.y = Math.random() * (MAP_SIZE - 2) + 1; // Make sure it's not in a wall or on the player if (map[Math.floor(enemy.y)][Math.floor(enemy.x)] === 0 && (Math.abs(enemy.x - player.x) > 1 || Math.abs(enemy.y - player.y) > 1)) { validPosition = true; } } enemy.health = 100; enemy.angle = Math.random() * Math.PI * 2; enemies.push(enemy); } updateEnemiesDisplay(); } function updateEnemiesDisplay() { enemiesDisplay.textContent = enemies.length; } function startGame() { gameRunning = true; startScreen.style.display = 'none'; canvas.focus(); } function handleMouseMove(event) { if (!gameRunning) return; const rect = canvas.getBoundingClientRect(); mouseX = event.clientX - rect.left; mouseY = event.clientY - rect.top; // Rotate player based on mouse movement player.angle += (event.movementX || 0) * mouseSensitivity; } function shoot() { if (!gameRunning || player.ammo <= 0) return; player.ammo--; ammoDisplay.textContent = player.ammo; // Create a bullet bullets.push({ x: player.x, y: player.y, angle: player.angle, distance: 0 }); } function movePlayer() { const moveX = Math.cos(player.angle) * PLAYER_SPEED; const moveY = Math.sin(player.angle) * PLAYER_SPEED; // Check for collision if (keys['w'] || keys['W']) { if (map[Math.floor(player.y)][Math.floor(player.x + moveX)] === 0) { player.x += moveX; } if (map[Math.floor(player.y + moveY)][Math.floor(player.x)] === 0) { player.y += moveY; } } if (keys['s'] || keys['S']) { if (map[Math.floor(player.y)][Math.floor(player.x - moveX)] === 0) { player.x -= moveX; } if (map[Math.floor(player.y - moveY)][Math.floor(player.x)] === 0) { player.y -= moveY; } } if (keys['a'] || keys['A']) { const rot90 = player.angle - Math.PI/2; if (map[Math.floor(player.y)][Math.floor(player.x + Math.cos(rot90) * PLAYER_SPEED)] === 0) { player.x += Math.cos(rot90) * PLAYER_SPEED; } if (map[Math.floor(player.y + Math.sin(rot90) * PLAYER_SPEED)][Math.floor(player.x)] === 0) { player.y += Math.sin(rot90) * PLAYER_SPEED; } } if (keys['d'] || keys['D']) { const rot90 = player.angle + Math.PI/2; if (map[Math.floor(player.y)][Math.floor(player.x + Math.cos(rot90) * PLAYER_SPEED)] === 0) { player.x += Math.cos(rot90) * PLAYER_SPEED; } if (map[Math.floor(player.y + Math.sin(rot90) * PLAYER_SPEED)][Math.floor(player.x)] === 0) { player.y += Math.sin(rot90) * PLAYER_SPEED; } } // Rotate player with arrow keys if (keys['ArrowLeft']) { player.angle -= ROTATION_SPEED; } if (keys['ArrowRight']) { player.angle += ROTATION_SPEED; } } function updateEnemies() { for (let i = 0; i < enemies.length; i++) { let enemy = enemies[i]; // Simple AI: move towards player const dx = player.x - enemy.x; const dy = player.y - enemy.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0.5) { enemy.angle = Math.atan2(dy, dx); enemy.x += Math.cos(enemy.angle) * ENEMY_SPEED; enemy.y += Math.sin(enemy.angle) * ENEMY_SPEED; } // Check collision with player if (distance < 0.7) { player.health -= 0.5; healthDisplay.textContent = Math.floor(player.health); if (player.health <= 0) { gameOver(); } } } } function updateBullets() { for (let i = bullets.length - 1; i >= 0; i--) { let bullet = bullets[i]; // Move bullet bullet.x += Math.cos(bullet.angle) * BULLET_SPEED; bullet.y += Math.sin(bullet.angle) * BULLET_SPEED; bullet.distance += BULLET_SPEED; // Check if bullet is out of bounds if (bullet.distance > 20 || bullet.x < 0 || bullet.x >= MAP_SIZE || bullet.y < 0 || bullet.y >= MAP_SIZE) { bullets.splice(i, 1); continue; } // Check collision with walls const mapX = Math.floor(bullet.x); const mapY = Math.floor(bullet.y); if (map[mapY] && map[mapY][mapX] === 1) { bullets.splice(i, 1); continue; } // Check collision with enemies for (let j = enemies.length - 1; j >= 0; j--) { const enemy = enemies[j]; const dx = bullet.x - enemy.x; const dy = bullet.y - enemy.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 0.5) { enemies.splice(j, 1); bullets.splice(i, 1); updateEnemiesDisplay(); break; } } } } function castRay(rayAngle) { // Calculate ray direction const rayDirX = Math.cos(rayAngle); const rayDirY = Math.sin(rayAngle); // Player's position const posX = player.x; const posY = player.y; // Length of ray from current position to next x or y-side let sideDistX; let sideDistY; // Length of ray from one x or y-side to next x or y-side const deltaDistX = Math.abs(1 / rayDirX); const deltaDistY = Math.abs(1 / rayDirY); // Direction to step in x or y direction (either +1 or -1) let stepX; let stepY; // Length of ray from current position to next x or y-side let hit = 0; // was there a wall hit? let side; // was a NS or a EW wall hit? // Calculate step and initial sideDist if (rayDirX < 0) { stepX = -1; sideDistX = (posX - Math.floor(posX)) * deltaDistX; } else { stepX = 1; sideDistX = (Math.floor(posX) + 1.0 - posX) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (posY - Math.floor(posY)) * deltaDistY; } else { stepY = 1; sideDistY = (Math.floor(posY) + 1.0 - posY) * deltaDistY; } // Perform DDA (Digital Differential Analysis) let mapX = Math.floor(posX); let mapY = Math.floor(posY); while (hit === 0) { // Jump to next map square if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check if ray has hit a wall if (mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE) { if (map[mapY][mapX] > 0) { hit = 1; } } else { hit = 1; // Out of bounds } } // Calculate distance projected on camera direction let perpWallDist; if (side === 0) { perpWallDist = (mapX - posX + (1 - stepX) / 2) / rayDirX; } else { perpWallDist = (mapY - posY + (1 - stepY) / 2) / rayDirY; } // Calculate height of line to draw on screen const lineHeight = Math.min(canvas.height, Math.floor(canvas.height / perpWallDist)); // Calculate lowest and highest pixel to fill in current stripe const drawStart = Math.max(0, -lineHeight / 2 + canvas.height / 2); const drawEnd = Math.min(canvas.height - 1, lineHeight / 2 + canvas.height / 2); // Calculate value of wallX let wallX; if (side === 0) { wallX = posY + perpWallDist * rayDirY; } else { wallX = posX + perpWallDist * rayDirX; } wallX -= Math.floor(wallX); return { length: perpWallDist, height: lineHeight, start: drawStart, end: drawEnd, wallX: wallX, side: side }; } function draw3DView() { const numRays = canvas.width; const angleStep = FOV / numRays; // Draw sky (dark blue) ctx.fillStyle = '#1a2c60'; ctx.fillRect(0, 0, canvas.width, canvas.height / 2); // Draw ground (darker grey) ctx.fillStyle = '#333'; ctx.fillRect(0, canvas.height / 2, canvas.width, canvas.height / 2); // Draw walls for (let i = 0; i < numRays; i++) { const rayAngle = player.angle - FOV / 2 + i * angleStep; const ray = castRay(rayAngle); // Calculate wall texture coordinates const texX = Math.floor(ray.wallX * TEXTURE_SIZE); const texY = Math.floor((ray.start - canvas.height / 2 + ray.height / 2) / ray.height * TEXTURE_SIZE); // Only render walls that are in view if (ray.height > 0) { const texture = textures.wall; const srcX = texX; const srcY = texY; const srcWidth = 1; const srcHeight = 1; const destX = i; const destY = ray.start; const destWidth = 1; const destHeight = ray.height; // Adjust texture brightness based on distance and side const brightness = Math.max(0.2, 0.8 - ray.length * 0.05); // Draw the texture with lighting effect ctx.fillStyle = `rgba(100, 100, 100, ${brightness})`; ctx.fillRect(destX, destY, destWidth, destHeight); // Add some rotation effect for max visual impact if (ray.side === 1) { ctx.fillStyle = `rgba(150, 150, 150, ${brightness})`; ctx.fillRect(destX, destY, destWidth, destHeight); } } } } function drawEnemies() { for (const enemy of enemies) { // Calculate direction from player to enemy const dx = enemy.x - player.x; const dy = enemy.y - player.y; const distance = Math.sqrt(dx*dx + dy*dy); // Normalize direction const dirX = dx / distance; const dirY = dy / distance; // Calculate angle relative to player direction const angle = Math.atan2(dirY, dirX) - player.angle; // Normalize angle to between -pi and pi const normalizedAngle = (angle + Math.PI) % (2 * Math.PI) - Math.PI; // Only draw if the enemy is in field of view if (Math.abs(normalizedAngle) < FOV / 2) { // Calculate screen position const screenX = canvas.width / 2 + normalizedAngle / FOV * canvas.width; const size = Math.min(canvas.height, Math.floor(canvas.height / distance * 0.8)); // Draw the enemy if (size > 0) { ctx.drawImage(textures.enemy, screenX - size/2, canvas.height/2 - size/2, size, size); } } } } function drawBullets() { for (const bullet of bullets) { // Calculate direction from player to bullet const dx = bullet.x - player.x; const dy = bullet.y - player.y; const distance = Math.sqrt(dx*dx + dy*dy); // Normalize direction const dirX = dx / distance; const dirY = dy / distance; // Calculate angle relative to player direction const angle = Math.atan2(dirY, dirX) - player.angle; // Normalize angle to between -pi and pi const normalizedAngle = (angle + Math.PI) % (2 * Math.PI) - Math.PI; // Only draw if the bullet is in field of view if (Math.abs(normalizedAngle) < FOV / 2) { // Calculate screen position const screenX = canvas.width / 2 + normalizedAngle / FOV * canvas.width; const size = Math.min(canvas.height, Math.floor(canvas.height / distance * 0.5)); // Draw the bullet if (size > 0) { ctx.drawImage(textures.bullet, screenX - size/2, canvas.height/2 - size/2, size, size); } } } } function gameOver() { gameRunning = false; startScreen.style.display = 'flex'; startScreen.innerHTML = ` <h1>GAME OVER</h1> <p>Your final score: ${Math.floor(player.health)}</p> <button id="restartButton">PLAY AGAIN</button> `; document.getElementById('restartButton').addEventListener('click', () => { // Reset game state player = { x: 1.5, y: 1.5, angle: 0, health: 100, ammo: 50 }; healthDisplay.textContent = player.health; ammoDisplay.textContent = player.ammo; generateEnemies(); startGame(); }); } function gameLoop() { if (gameRunning) { // Handle player movement movePlayer(); // Update game objects updateEnemies(); updateBullets(); // Update displays healthDisplay.textContent = Math.floor(player.health); ammoDisplay.textContent = player.ammo; // Draw everything draw3DView(); drawEnemies(); drawBullets(); } requestAnimationFrame(gameLoop); } // Start the game when page loads window.onload = 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.