Created
May 22, 2025 18:43
-
-
Save danmindru/932ec39666ab56fbb6a3a7f52efed12a to your computer and use it in GitHub Desktop.
Tank AI game with Claude Sonnet 4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Tank Battle</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
overflow: hidden; | |
background-color: #222; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: 100vh; | |
font-family: "Courier New", monospace; | |
touch-action: none; | |
} | |
canvas { | |
display: block; | |
background-color: #333; | |
} | |
#ui-container { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
pointer-events: none; | |
z-index: 10; | |
} | |
.menu { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background-color: rgba(0, 0, 0, 0.8); | |
padding: 20px; | |
border-radius: 10px; | |
color: white; | |
text-align: center; | |
pointer-events: auto; | |
} | |
button { | |
background-color: #4caf50; | |
border: none; | |
color: white; | |
padding: 10px 20px; | |
text-align: center; | |
text-decoration: none; | |
display: inline-block; | |
font-size: 16px; | |
margin: 10px 2px; | |
cursor: pointer; | |
border-radius: 5px; | |
font-family: inherit; | |
} | |
.color-option { | |
display: inline-block; | |
width: 30px; | |
height: 30px; | |
margin: 5px; | |
border-radius: 50%; | |
cursor: pointer; | |
border: 2px solid transparent; | |
} | |
.color-option.selected { | |
border: 2px solid white; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="gameCanvas"></canvas> | |
<div id="ui-container"></div> | |
<script> | |
const canvas = document.getElementById("gameCanvas"); | |
const ctx = canvas.getContext("2d"); | |
const uiContainer = document.getElementById("ui-container"); | |
const GRID_SIZE = 36; | |
let TILE_SIZE; | |
const PLAYER_COLORS = [ | |
"#3498db", | |
"#e74c3c", | |
"#2ecc71", | |
"#f1c40f", | |
"#9b59b6", | |
]; | |
const ENEMY_COLORS = { | |
AGGRESSIVE: "#e74c3c", | |
DEFENSIVE: "#2ecc71", | |
PATROL: "#f1c40f", | |
}; | |
let gameState = "menu"; | |
let level = 1; | |
let score = 0; | |
let highScore = localStorage.getItem("tankBattleHighScore") || 0; | |
let startTime; | |
let playerColor = PLAYER_COLORS[0]; | |
let playerLives = 3; | |
let enemyCount = 3; | |
let enemies = []; | |
let projectiles = []; | |
let explosions = []; | |
let powerUps = []; | |
let lastShootTime = 0; | |
let killedEnemies = 0; | |
let player = { | |
x: 1, | |
y: 1, | |
direction: "right", | |
color: playerColor, | |
projectileColor: "#fff", | |
specialAmmo: false, | |
moving: false, | |
speedModifier: 1, | |
destroyed: false, | |
lastMove: 0, | |
}; | |
let terrain = []; | |
let touchStartX = 0; | |
let touchStartY = 0; | |
let isTouching = false; | |
function resizeCanvas() { | |
const windowWidth = window.innerWidth; | |
const windowHeight = window.innerHeight; | |
const size = Math.min(windowWidth, windowHeight) * 0.9; | |
canvas.width = size; | |
canvas.height = size; | |
TILE_SIZE = size / GRID_SIZE; | |
} | |
function init() { | |
resizeCanvas(); | |
window.addEventListener("resize", resizeCanvas); | |
window.addEventListener("keydown", handleKeyDown); | |
window.addEventListener("keyup", handleKeyUp); | |
canvas.addEventListener("touchstart", handleTouchStart); | |
canvas.addEventListener("touchmove", handleTouchMove); | |
canvas.addEventListener("touchend", handleTouchEnd); | |
showMainMenu(); | |
requestAnimationFrame(gameLoop); | |
} | |
function showMainMenu() { | |
uiContainer.innerHTML = ` | |
<div class="menu"> | |
<h1>TANK BATTLE</h1> | |
<h2>High Score: ${highScore}</h2> | |
<div> | |
<h3>Select Tank Color</h3> | |
<div id="color-selector"> | |
${PLAYER_COLORS.map( | |
(color, index) => | |
`<div class="color-option ${ | |
color === playerColor ? "selected" : "" | |
}" | |
style="background-color: ${color};" | |
data-color="${color}"></div>` | |
).join("")} | |
</div> | |
</div> | |
<div> | |
<h3>Enemy Tanks: <span id="enemy-count">${enemyCount}</span></h3> | |
<button id="decrease-enemies">-</button> | |
<button id="increase-enemies">+</button> | |
</div> | |
<button id="start-game">Start Game</button> | |
</div> | |
`; | |
document.querySelectorAll(".color-option").forEach((option) => { | |
option.addEventListener("click", () => { | |
playerColor = option.dataset.color; | |
document | |
.querySelectorAll(".color-option") | |
.forEach((o) => o.classList.remove("selected")); | |
option.classList.add("selected"); | |
player.color = playerColor; | |
}); | |
}); | |
document | |
.getElementById("decrease-enemies") | |
.addEventListener("click", () => { | |
if (enemyCount > 1) { | |
enemyCount--; | |
document.getElementById("enemy-count").textContent = enemyCount; | |
} | |
}); | |
document | |
.getElementById("increase-enemies") | |
.addEventListener("click", () => { | |
if (enemyCount < 10) { | |
enemyCount++; | |
document.getElementById("enemy-count").textContent = enemyCount; | |
} | |
}); | |
document | |
.getElementById("start-game") | |
.addEventListener("click", startGame); | |
} | |
function startGame() { | |
gameState = "game"; | |
uiContainer.innerHTML = ""; | |
player = { | |
x: 1, | |
y: 1, | |
direction: "right", | |
color: playerColor, | |
projectileColor: "#fff", | |
specialAmmo: false, | |
moving: false, | |
speedModifier: 1, | |
destroyed: false, | |
lastMove: 0, | |
}; | |
playerLives = 3; | |
level = 1; | |
score = 0; | |
killedEnemies = 0; | |
projectiles = []; | |
explosions = []; | |
powerUps = []; | |
startTime = Date.now(); | |
generateMap(); | |
spawnEnemies(); | |
} | |
function generateMap() { | |
terrain = Array(GRID_SIZE) | |
.fill() | |
.map(() => Array(GRID_SIZE).fill("open")); | |
for (let i = 0; i < GRID_SIZE; i++) { | |
terrain[0][i] = "solid"; | |
terrain[GRID_SIZE - 1][i] = "solid"; | |
terrain[i][0] = "solid"; | |
terrain[i][GRID_SIZE - 1] = "solid"; | |
} | |
for (let i = 2; i < GRID_SIZE - 2; i++) { | |
for (let j = 2; j < GRID_SIZE - 2; j++) { | |
const rand = Math.random(); | |
if (i <= 3 && j <= 3) continue; | |
if (rand < 0.05) { | |
terrain[i][j] = "solid"; | |
} else if (rand < 0.15) { | |
terrain[i][j] = "breakable"; | |
} else if (rand < 0.2) { | |
terrain[i][j] = "water"; | |
} else if (rand < 0.25) { | |
terrain[i][j] = "mud"; | |
} | |
} | |
} | |
createPaths(); | |
} | |
function createPaths() { | |
for (let i = 4; i < GRID_SIZE - 4; i += 4) { | |
for (let j = 0; j < GRID_SIZE; j++) { | |
if (terrain[i][j] !== "solid") { | |
terrain[i][j] = "open"; | |
} | |
if (terrain[j][i] !== "solid") { | |
terrain[j][i] = "open"; | |
} | |
} | |
} | |
} | |
function spawnEnemies() { | |
enemies = []; | |
for (let i = 0; i < enemyCount; i++) { | |
let x, y; | |
let validPosition = false; | |
while (!validPosition) { | |
x = Math.floor(Math.random() * (GRID_SIZE - 6)) + 3; | |
y = Math.floor(Math.random() * (GRID_SIZE - 6)) + 3; | |
if ( | |
terrain[x][y] === "open" && | |
(Math.abs(x - player.x) > 5 || Math.abs(y - player.y) > 5) | |
) { | |
validPosition = true; | |
} | |
} | |
const behaviorTypes = ["aggressive", "defensive", "patrol"]; | |
const behavior = | |
behaviorTypes[Math.floor(Math.random() * behaviorTypes.length)]; | |
enemies.push({ | |
x: x, | |
y: y, | |
direction: ["up", "right", "down", "left"][ | |
Math.floor(Math.random() * 4) | |
], | |
color: ENEMY_COLORS[behavior.toUpperCase()], | |
behavior: behavior, | |
lastMove: Date.now(), | |
lastShoot: Date.now(), | |
moveDelay: 500 - level * 20, | |
specialAmmo: false, | |
patrolDirection: Math.random() < 0.5 ? "horizontal" : "vertical", | |
patrolReverse: false, | |
}); | |
} | |
} | |
function handleKeyDown(e) { | |
if (gameState !== "game" || player.destroyed) return; | |
switch (e.key) { | |
case "ArrowUp": | |
player.direction = "up"; | |
player.moving = true; | |
break; | |
case "ArrowRight": | |
player.direction = "right"; | |
player.moving = true; | |
break; | |
case "ArrowDown": | |
player.direction = "down"; | |
player.moving = true; | |
break; | |
case "ArrowLeft": | |
player.direction = "left"; | |
player.moving = true; | |
break; | |
case " ": | |
playerShoot(); | |
break; | |
} | |
} | |
function handleKeyUp(e) { | |
if (gameState !== "game") return; | |
if ( | |
["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"].includes(e.key) | |
) { | |
player.moving = false; | |
} | |
} | |
function handleTouchStart(e) { | |
e.preventDefault(); | |
if (gameState !== "game" || player.destroyed) return; | |
const touch = e.touches[0]; | |
touchStartX = touch.clientX; | |
touchStartY = touch.clientY; | |
isTouching = true; | |
setTimeout(() => { | |
if (isTouching) { | |
playerShoot(); | |
} | |
}, 200); | |
} | |
function handleTouchMove(e) { | |
e.preventDefault(); | |
if (!isTouching || gameState !== "game" || player.destroyed) return; | |
const touch = e.touches[0]; | |
const dx = touch.clientX - touchStartX; | |
const dy = touch.clientY - touchStartY; | |
if (Math.abs(dx) > 20 || Math.abs(dy) > 20) { | |
if (Math.abs(dx) > Math.abs(dy)) { | |
player.direction = dx > 0 ? "right" : "left"; | |
} else { | |
player.direction = dy > 0 ? "down" : "up"; | |
} | |
player.moving = true; | |
touchStartX = touch.clientX; | |
touchStartY = touch.clientY; | |
} | |
} | |
function handleTouchEnd(e) { | |
e.preventDefault(); | |
isTouching = false; | |
player.moving = false; | |
} | |
function playerShoot() { | |
const currentTime = Date.now(); | |
if (currentTime - lastShootTime < 2000) return; | |
lastShootTime = currentTime; | |
const projectile = { | |
x: player.x, | |
y: player.y, | |
direction: player.direction, | |
color: player.projectileColor, | |
speed: 0.2, | |
isPlayerProjectile: true, | |
special: player.specialAmmo, | |
lastMove: Date.now(), | |
}; | |
projectiles.push(projectile); | |
} | |
function enemyShoot(enemy) { | |
const currentTime = Date.now(); | |
if (currentTime - enemy.lastShoot < 2000) return; | |
enemy.lastShoot = currentTime; | |
const projectile = { | |
x: enemy.x, | |
y: enemy.y, | |
direction: enemy.direction, | |
color: "#ff4d4d", | |
speed: 0.15, | |
isPlayerProjectile: false, | |
special: enemy.specialAmmo, | |
lastMove: Date.now(), | |
}; | |
projectiles.push(projectile); | |
} | |
function movePlayer() { | |
if (!player.moving || player.destroyed) return; | |
const currentTime = Date.now(); | |
let moveDelay = 200 / player.speedModifier; | |
if (currentTime - (player.lastMove || 0) < moveDelay) return; | |
player.lastMove = currentTime; | |
let newX = player.x; | |
let newY = player.y; | |
switch (player.direction) { | |
case "up": | |
newY--; | |
break; | |
case "right": | |
newX++; | |
break; | |
case "down": | |
newY++; | |
break; | |
case "left": | |
newX--; | |
break; | |
} | |
if (isValidMove(newX, newY)) { | |
player.x = newX; | |
player.y = newY; | |
player.speedModifier = terrain[newX][newY] === "mud" ? 0.5 : 1; | |
checkPowerUpCollision(); | |
} | |
} | |
function moveEnemies() { | |
const currentTime = Date.now(); | |
enemies.forEach((enemy) => { | |
if (currentTime - enemy.lastMove < enemy.moveDelay) return; | |
enemy.lastMove = currentTime; | |
switch (enemy.behavior) { | |
case "aggressive": | |
moveAggressiveEnemy(enemy); | |
break; | |
case "defensive": | |
moveDefensiveEnemy(enemy); | |
break; | |
case "patrol": | |
movePatrolEnemy(enemy); | |
break; | |
} | |
const shouldShoot = Math.random() < 0.2; | |
if (shouldShoot) { | |
enemyShoot(enemy); | |
} | |
}); | |
} | |
function moveAggressiveEnemy(enemy) { | |
const dx = player.x - enemy.x; | |
const dy = player.y - enemy.y; | |
if (Math.abs(dx) > Math.abs(dy)) { | |
enemy.direction = dx > 0 ? "right" : "left"; | |
} else { | |
enemy.direction = dy > 0 ? "down" : "up"; | |
} | |
let newX = enemy.x; | |
let newY = enemy.y; | |
switch (enemy.direction) { | |
case "up": | |
newY--; | |
break; | |
case "right": | |
newX++; | |
break; | |
case "down": | |
newY++; | |
break; | |
case "left": | |
newX--; | |
break; | |
} | |
if (isValidMove(newX, newY) && !isEnemyAtPosition(newX, newY)) { | |
enemy.x = newX; | |
enemy.y = newY; | |
} else { | |
const directions = ["up", "right", "down", "left"]; | |
const currentIndex = directions.indexOf(enemy.direction); | |
directions.splice(currentIndex, 1); | |
enemy.direction = directions[Math.floor(Math.random() * 3)]; | |
} | |
} | |
function moveDefensiveEnemy(enemy) { | |
const dx = enemy.x - player.x; | |
const dy = enemy.y - player.y; | |
const distanceToPlayer = Math.sqrt(dx * dx + dy * dy); | |
if (distanceToPlayer < 6) { | |
if (Math.abs(dx) > Math.abs(dy)) { | |
enemy.direction = dx > 0 ? "right" : "left"; | |
} else { | |
enemy.direction = dy > 0 ? "down" : "up"; | |
} | |
} else { | |
if (Math.random() < 0.3) { | |
enemy.direction = ["up", "right", "down", "left"][ | |
Math.floor(Math.random() * 4) | |
]; | |
} | |
} | |
let newX = enemy.x; | |
let newY = enemy.y; | |
switch (enemy.direction) { | |
case "up": | |
newY--; | |
break; | |
case "right": | |
newX++; | |
break; | |
case "down": | |
newY++; | |
break; | |
case "left": | |
newX--; | |
break; | |
} | |
if (isValidMove(newX, newY) && !isEnemyAtPosition(newX, newY)) { | |
enemy.x = newX; | |
enemy.y = newY; | |
} | |
const shootDx = player.x - enemy.x; | |
const shootDy = player.y - enemy.y; | |
if (Math.abs(shootDx) > Math.abs(shootDy)) { | |
enemy.direction = shootDx > 0 ? "right" : "left"; | |
} else { | |
enemy.direction = shootDy > 0 ? "down" : "up"; | |
} | |
} | |
function movePatrolEnemy(enemy) { | |
if (enemy.patrolDirection === "horizontal") { | |
enemy.direction = enemy.patrolReverse ? "left" : "right"; | |
} else { | |
enemy.direction = enemy.patrolReverse ? "up" : "down"; | |
} | |
let newX = enemy.x; | |
let newY = enemy.y; | |
switch (enemy.direction) { | |
case "up": | |
newY--; | |
break; | |
case "right": | |
newX++; | |
break; | |
case "down": | |
newY++; | |
break; | |
case "left": | |
newX--; | |
break; | |
} | |
if (isValidMove(newX, newY) && !isEnemyAtPosition(newX, newY)) { | |
enemy.x = newX; | |
enemy.y = newY; | |
} else { | |
enemy.patrolReverse = !enemy.patrolReverse; | |
} | |
if (Math.random() < 0.05) { | |
enemy.patrolDirection = | |
enemy.patrolDirection === "horizontal" ? "vertical" : "horizontal"; | |
} | |
} | |
function isValidMove(x, y) { | |
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) { | |
return false; | |
} | |
const terrainType = terrain[x][y]; | |
return ( | |
terrainType !== "solid" && | |
terrainType !== "breakable" && | |
terrainType !== "water" | |
); | |
} | |
function isEnemyAtPosition(x, y) { | |
return enemies.some((enemy) => enemy.x === x && enemy.y === y); | |
} | |
function moveProjectiles() { | |
const currentTime = Date.now(); | |
for (let i = projectiles.length - 1; i >= 0; i--) { | |
const projectile = projectiles[i]; | |
if ( | |
currentTime - projectile.lastMove < | |
1000 / (projectile.speed * 10) | |
) { | |
continue; | |
} | |
projectile.lastMove = currentTime; | |
let newX = projectile.x; | |
let newY = projectile.y; | |
switch (projectile.direction) { | |
case "up": | |
newY -= 0.5; | |
break; | |
case "right": | |
newX += 0.5; | |
break; | |
case "down": | |
newY += 0.5; | |
break; | |
case "left": | |
newX -= 0.5; | |
break; | |
} | |
projectile.x = newX; | |
projectile.y = newY; | |
if (checkProjectileCollision(projectile, i)) { | |
continue; | |
} | |
if (newX < 0 || newX >= GRID_SIZE || newY < 0 || newY >= GRID_SIZE) { | |
projectiles.splice(i, 1); | |
} | |
} | |
} | |
function checkProjectileCollision(projectile, index) { | |
const gridX = Math.floor(projectile.x); | |
const gridY = Math.floor(projectile.y); | |
if ( | |
gridX >= 0 && | |
gridX < GRID_SIZE && | |
gridY >= 0 && | |
gridY < GRID_SIZE | |
) { | |
const terrainType = terrain[gridX][gridY]; | |
if (terrainType === "solid") { | |
projectiles.splice(index, 1); | |
return true; | |
} else if (terrainType === "breakable") { | |
terrain[gridX][gridY] = "open"; | |
createExplosion(gridX, gridY, projectile.special); | |
projectiles.splice(index, 1); | |
return true; | |
} | |
} | |
if (!projectile.isPlayerProjectile) { | |
const playerGridX = Math.floor(player.x); | |
const playerGridY = Math.floor(player.y); | |
if ( | |
gridX === playerGridX && | |
gridY === playerGridY && | |
!player.destroyed | |
) { | |
playerHit(); | |
createExplosion(playerGridX, playerGridY, projectile.special); | |
projectiles.splice(index, 1); | |
return true; | |
} | |
} else { | |
for (let i = enemies.length - 1; i >= 0; i--) { | |
const enemy = enemies[i]; | |
const enemyGridX = Math.floor(enemy.x); | |
const enemyGridY = Math.floor(enemy.y); | |
if (gridX === enemyGridX && gridY === enemyGridY) { | |
enemyHit(i); | |
createExplosion(enemyGridX, enemyGridY, projectile.special); | |
projectiles.splice(index, 1); | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
function createExplosion(x, y, isSpecial) { | |
const radius = isSpecial ? 2 : 1; | |
explosions.push({ | |
x: x, | |
y: y, | |
radius: radius, | |
life: 10, | |
lastUpdate: Date.now(), | |
}); | |
if (isSpecial) { | |
for (let i = x - radius; i <= x + radius; i++) { | |
for (let j = y - radius; j <= y + radius; j++) { | |
if (i < 0 || i >= GRID_SIZE || j < 0 || j >= GRID_SIZE) continue; | |
if (terrain[i][j] === "breakable") { | |
terrain[i][j] = "open"; | |
} | |
for (let e = enemies.length - 1; e >= 0; e--) { | |
const enemy = enemies[e]; | |
const enemyGridX = Math.floor(enemy.x); | |
const enemyGridY = Math.floor(enemy.y); | |
if (i === enemyGridX && j === enemyGridY) { | |
enemyHit(e); | |
} | |
} | |
if (i === Math.floor(player.x) && j === Math.floor(player.y)) { | |
playerHit(); | |
} | |
} | |
} | |
} | |
} | |
function updateExplosions() { | |
const currentTime = Date.now(); | |
for (let i = explosions.length - 1; i >= 0; i--) { | |
const explosion = explosions[i]; | |
if (currentTime - explosion.lastUpdate > 100) { | |
explosion.lastUpdate = currentTime; | |
explosion.life--; | |
if (explosion.life <= 0) { | |
explosions.splice(i, 1); | |
} | |
} | |
} | |
} | |
function playerHit() { | |
playerLives--; | |
player.specialAmmo = false; | |
if (playerLives <= 0) { | |
player.destroyed = true; | |
setTimeout(gameOver, 2000); | |
} | |
} | |
function enemyHit(enemyIndex) { | |
const enemy = enemies[enemyIndex]; | |
enemies.splice(enemyIndex, 1); | |
score += 100; | |
killedEnemies++; | |
if (Math.random() < 0.3) { | |
powerUps.push({ | |
x: enemy.x, | |
y: enemy.y, | |
type: "specialAmmo", | |
collected: false, | |
}); | |
} | |
if (enemies.length === 0) { | |
setTimeout(nextLevel, 2000); | |
} | |
} | |
function nextLevel() { | |
level++; | |
projectiles = []; | |
explosions = []; | |
player.specialAmmo = false; | |
const timeBonus = Math.floor((30000 / (Date.now() - startTime)) * 1000); | |
score += timeBonus; | |
enemyCount = Math.min(enemyCount + 1, 10); | |
generateMap(); | |
spawnEnemies(); | |
startTime = Date.now(); | |
} | |
function gameOver() { | |
gameState = "gameOver"; | |
if (score > highScore) { | |
highScore = score; | |
localStorage.setItem("tankBattleHighScore", highScore); | |
} | |
uiContainer.innerHTML = ` | |
<div class="menu"> | |
<h1>GAME OVER</h1> | |
<h2>Score: ${score}</h2> | |
<h2>High Score: ${highScore}</h2> | |
<h3>Enemies Defeated: ${killedEnemies}</h3> | |
<button id="play-again">Play Again</button> | |
<button id="return-menu">Main Menu</button> | |
</div> | |
`; | |
document | |
.getElementById("play-again") | |
.addEventListener("click", startGame); | |
document.getElementById("return-menu").addEventListener("click", () => { | |
gameState = "menu"; | |
showMainMenu(); | |
}); | |
} | |
function checkPowerUpCollision() { | |
for (let i = powerUps.length - 1; i >= 0; i--) { | |
const powerUp = powerUps[i]; | |
if ( | |
Math.floor(player.x) === Math.floor(powerUp.x) && | |
Math.floor(player.y) === Math.floor(powerUp.y) | |
) { | |
if (powerUp.type === "specialAmmo") { | |
player.specialAmmo = true; | |
} | |
powerUps.splice(i, 1); | |
} | |
for (let j = 0; j < enemies.length; j++) { | |
const enemy = enemies[j]; | |
if ( | |
Math.floor(enemy.x) === Math.floor(powerUp.x) && | |
Math.floor(enemy.y) === Math.floor(powerUp.y) | |
) { | |
if (powerUp.type === "specialAmmo") { | |
enemy.specialAmmo = true; | |
} | |
powerUps.splice(i, 1); | |
break; | |
} | |
} | |
} | |
} | |
function draw() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
if (gameState === "game") { | |
drawTerrain(); | |
drawPowerUps(); | |
if (!player.destroyed) { | |
drawTank(player); | |
} | |
enemies.forEach((enemy) => drawTank(enemy)); | |
projectiles.forEach((projectile) => drawProjectile(projectile)); | |
explosions.forEach((explosion) => drawExplosion(explosion)); | |
drawHUD(); | |
} else { | |
ctx.fillStyle = "#7d916c"; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
for (let i = 0; i < 20; i++) { | |
for (let j = 0; j < 20; j++) { | |
if ((i + j) % 2 === 0) { | |
ctx.fillStyle = "#8da370"; | |
ctx.fillRect( | |
i * (canvas.width / 20), | |
j * (canvas.height / 20), | |
canvas.width / 20, | |
canvas.height / 20 | |
); | |
} | |
} | |
} | |
} | |
} | |
function drawTerrain() { | |
for (let i = 0; i < GRID_SIZE; i++) { | |
for (let j = 0; j < GRID_SIZE; j++) { | |
const terrainType = terrain[i][j]; | |
const x = i * TILE_SIZE; | |
const y = j * TILE_SIZE; | |
switch (terrainType) { | |
case "solid": | |
ctx.fillStyle = "#555"; | |
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); | |
ctx.strokeStyle = "#444"; | |
ctx.lineWidth = 1; | |
ctx.strokeRect(x + 2, y + 2, TILE_SIZE - 4, TILE_SIZE - 4); | |
break; | |
case "breakable": | |
ctx.fillStyle = "#a57164"; | |
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); | |
ctx.strokeStyle = "#8c5b4a"; | |
ctx.lineWidth = 1; | |
for (let bi = 0; bi < 2; bi++) { | |
for (let bj = 0; bj < 2; bj++) { | |
ctx.strokeRect( | |
x + bi * (TILE_SIZE / 2), | |
y + bj * (TILE_SIZE / 2), | |
TILE_SIZE / 2, | |
TILE_SIZE / 2 | |
); | |
} | |
} | |
break; | |
case "water": | |
ctx.fillStyle = "#4a90e2"; | |
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); | |
ctx.fillStyle = "#67a5e5"; | |
for (let w = 0; w < 3; w++) { | |
ctx.beginPath(); | |
ctx.arc( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE * (0.3 + w * 0.2), | |
TILE_SIZE / 3, | |
0, | |
Math.PI | |
); | |
ctx.fill(); | |
} | |
break; | |
case "mud": | |
ctx.fillStyle = "#8b6b4c"; | |
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); | |
ctx.fillStyle = "#7a5c3d"; | |
for (let m = 0; m < 5; m++) { | |
ctx.beginPath(); | |
ctx.arc( | |
x + Math.random() * TILE_SIZE, | |
y + Math.random() * TILE_SIZE, | |
TILE_SIZE / 8, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
break; | |
case "open": | |
ctx.fillStyle = "#7d916c"; | |
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE); | |
break; | |
} | |
} | |
} | |
} | |
function drawTank(tank) { | |
const x = tank.x * TILE_SIZE; | |
const y = tank.y * TILE_SIZE; | |
const size = TILE_SIZE * 0.8; | |
ctx.fillStyle = tank.color; | |
ctx.fillRect(x + TILE_SIZE * 0.1, y + TILE_SIZE * 0.1, size, size); | |
ctx.fillStyle = "#333"; | |
switch (tank.direction) { | |
case "up": | |
ctx.fillRect( | |
x + TILE_SIZE * 0.4, | |
y, | |
TILE_SIZE * 0.2, | |
TILE_SIZE * 0.4 | |
); | |
break; | |
case "right": | |
ctx.fillRect( | |
x + TILE_SIZE * 0.6, | |
y + TILE_SIZE * 0.4, | |
TILE_SIZE * 0.4, | |
TILE_SIZE * 0.2 | |
); | |
break; | |
case "down": | |
ctx.fillRect( | |
x + TILE_SIZE * 0.4, | |
y + TILE_SIZE * 0.6, | |
TILE_SIZE * 0.2, | |
TILE_SIZE * 0.4 | |
); | |
break; | |
case "left": | |
ctx.fillRect( | |
x, | |
y + TILE_SIZE * 0.4, | |
TILE_SIZE * 0.4, | |
TILE_SIZE * 0.2 | |
); | |
break; | |
} | |
ctx.fillStyle = "#333"; | |
ctx.fillRect( | |
x + TILE_SIZE * 0.1, | |
y + TILE_SIZE * 0.1, | |
TILE_SIZE * 0.15, | |
size | |
); | |
ctx.fillRect( | |
x + TILE_SIZE * 0.75, | |
y + TILE_SIZE * 0.1, | |
TILE_SIZE * 0.15, | |
size | |
); | |
if (tank.specialAmmo) { | |
ctx.fillStyle = "#ff0"; | |
ctx.beginPath(); | |
ctx.arc( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 6, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
} | |
function drawProjectile(projectile) { | |
const x = projectile.x * TILE_SIZE; | |
const y = projectile.y * TILE_SIZE; | |
ctx.fillStyle = projectile.color; | |
if (projectile.special) { | |
ctx.beginPath(); | |
ctx.arc( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 4, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
ctx.beginPath(); | |
const gradient = ctx.createRadialGradient( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 8, | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 2 | |
); | |
gradient.addColorStop(0, "rgba(255, 255, 0, 0.8)"); | |
gradient.addColorStop(1, "rgba(255, 255, 0, 0)"); | |
ctx.fillStyle = gradient; | |
ctx.arc( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 2, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
} else { | |
ctx.beginPath(); | |
ctx.arc( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 6, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
} | |
function drawExplosion(explosion) { | |
const x = explosion.x * TILE_SIZE; | |
const y = explosion.y * TILE_SIZE; | |
const radius = explosion.radius * TILE_SIZE; | |
const gradient = ctx.createRadialGradient( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
0, | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
radius | |
); | |
gradient.addColorStop( | |
0, | |
"rgba(255, 255, 255, " + explosion.life / 10 + ")" | |
); | |
gradient.addColorStop( | |
0.4, | |
"rgba(255, 200, 0, " + explosion.life / 12 + ")" | |
); | |
gradient.addColorStop( | |
0.6, | |
"rgba(255, 100, 0, " + explosion.life / 15 + ")" | |
); | |
gradient.addColorStop(1, "rgba(255, 0, 0, 0)"); | |
ctx.fillStyle = gradient; | |
ctx.beginPath(); | |
ctx.arc(x + TILE_SIZE / 2, y + TILE_SIZE / 2, radius, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
function drawPowerUps() { | |
powerUps.forEach((powerUp) => { | |
const x = powerUp.x * TILE_SIZE; | |
const y = powerUp.y * TILE_SIZE; | |
if (powerUp.type === "specialAmmo") { | |
ctx.fillStyle = "#ffcc00"; | |
ctx.beginPath(); | |
ctx.arc( | |
x + TILE_SIZE / 2, | |
y + TILE_SIZE / 2, | |
TILE_SIZE / 3, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
ctx.fillStyle = "#ff9900"; | |
const starPoints = 5; | |
const outerRadius = TILE_SIZE / 4; | |
const innerRadius = TILE_SIZE / 8; | |
ctx.beginPath(); | |
for (let i = 0; i < starPoints * 2; i++) { | |
const radius = i % 2 === 0 ? outerRadius : innerRadius; | |
const angle = (Math.PI * 2 * i) / (starPoints * 2); | |
const xPos = x + TILE_SIZE / 2 + radius * Math.cos(angle); | |
const yPos = y + TILE_SIZE / 2 + radius * Math.sin(angle); | |
if (i === 0) { | |
ctx.moveTo(xPos, yPos); | |
} else { | |
ctx.lineTo(xPos, yPos); | |
} | |
} | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
}); | |
} | |
function drawHUD() { | |
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; | |
ctx.fillRect(0, 0, canvas.width, TILE_SIZE * 1.5); | |
ctx.fillStyle = "#fff"; | |
ctx.font = `${TILE_SIZE / 2}px 'Courier New', monospace`; | |
ctx.fillText(`Lives: ${playerLives}`, TILE_SIZE, TILE_SIZE); | |
ctx.fillText(`Score: ${score}`, TILE_SIZE * 10, TILE_SIZE); | |
ctx.fillText(`Level: ${level}`, TILE_SIZE * 20, TILE_SIZE); | |
if (player.specialAmmo) { | |
ctx.fillStyle = "#ffcc00"; | |
ctx.fillText("SPECIAL AMMO", TILE_SIZE * 28, TILE_SIZE); | |
} | |
} | |
function gameLoop() { | |
if (gameState === "game") { | |
movePlayer(); | |
moveEnemies(); | |
moveProjectiles(); | |
updateExplosions(); | |
} | |
draw(); | |
requestAnimationFrame(gameLoop); | |
} | |
init(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment