Files
hakorune/examples/wasm/09_snake_game.nyash
2025-08-13 00:36:32 +00:00

439 lines
12 KiB
Plaintext

// 🎮 Simple Snake Game Demo - CanvasLoopBox + CanvasEventBox + WebCanvasBox
// Classic Snake game demonstrating complete game development workflow
print("🎮 === Simple Snake Game Demo Starting ===")
// Initialize game components
local canvas, events, loop, random
canvas = new WebCanvasBox("demo-canvas", 600, 400)
events = new CanvasEventBox("demo-canvas")
loop = new CanvasLoopBox()
random = new RandomBox()
// Game configuration
local gameConfig
gameConfig = {
gridSize: 20,
gridWidth: 30, // 600 / 20
gridHeight: 20, // 400 / 20
speed: 150, // milliseconds per move
colors: {
background: "#2c3e50",
snake: "#27ae60",
food: "#e74c3c",
border: "#34495e",
text: "#ecf0f1"
}
}
// Game state
local gameState, snake, food, direction, nextDirection, score, highScore
gameState = "playing" // playing, paused, gameover
score = 0
highScore = 42 // Demo high score
// Snake object (Everything is Box philosophy)
snake = {
body: [
{x: 15, y: 10},
{x: 14, y: 10},
{x: 13, y: 10}
],
growing: false
}
// Food object
food = {
x: 10,
y: 10,
type: "normal" // normal, bonus, penalty
}
// Direction system
direction = "right"
nextDirection = "right"
// Input handling
local keys
keys = {
up: false,
down: false,
left: false,
right: false,
space: false
}
// Game mechanics
local generateFood
generateFood = function() {
local validPositions, x, y, isValidPosition, bodyPart
validPositions = []
// Find all valid positions (not occupied by snake)
y = 1
loop(y < gameConfig.gridHeight - 1) {
x = 1
loop(x < gameConfig.gridWidth - 1) {
isValidPosition = true
// Check if position is occupied by snake
local i
i = 0
loop(i < snake.body.length()) {
bodyPart = snake.body[i]
if (bodyPart.x == x and bodyPart.y == y) {
isValidPosition = false
}
i = i + 1
}
if (isValidPosition) {
validPositions.push({x: x, y: y})
}
x = x + 1
}
y = y + 1
}
// Choose random valid position
if (validPositions.length() > 0) {
local randomIndex
randomIndex = random.randInt(0, validPositions.length() - 1)
local newPos
newPos = validPositions[randomIndex]
food.x = newPos.x
food.y = newPos.y
// Randomly choose food type
local foodTypes
foodTypes = ["normal", "normal", "normal", "bonus"] // 75% normal, 25% bonus
food.type = foodTypes[random.randInt(0, 3)]
}
}
local checkCollision
checkCollision = function() {
local head
head = snake.body[0]
// Wall collision
if (head.x <= 0 or head.x >= gameConfig.gridWidth - 1 or
head.y <= 0 or head.y >= gameConfig.gridHeight - 1) {
return "wall"
}
// Self collision
local i, bodyPart
i = 1 // Skip head
loop(i < snake.body.length()) {
bodyPart = snake.body[i]
if (head.x == bodyPart.x and head.y == bodyPart.y) {
return "self"
}
i = i + 1
}
return "none"
}
local updateSnake
updateSnake = function() {
if (gameState != "playing") {
return
}
// Update direction
direction = nextDirection
// Calculate new head position
local head, newHead
head = snake.body[0]
newHead = {x: head.x, y: head.y}
if (direction == "up") {
newHead.y = newHead.y - 1
} else if (direction == "down") {
newHead.y = newHead.y + 1
} else if (direction == "left") {
newHead.x = newHead.x - 1
} else if (direction == "right") {
newHead.x = newHead.x + 1
}
// Add new head
snake.body.unshift(newHead)
// Check food collision
if (newHead.x == food.x and newHead.y == food.y) {
// Food eaten
if (food.type == "normal") {
score = score + 10
} else if (food.type == "bonus") {
score = score + 25
}
snake.growing = true
generateFood()
} else {
// Remove tail if not growing
if (not snake.growing) {
snake.body.pop()
} else {
snake.growing = false
}
}
// Check collisions
local collision
collision = checkCollision()
if (collision != "none") {
gameState = "gameover"
if (score > highScore) {
highScore = score
}
}
}
// Rendering functions
local drawGrid
drawGrid = function() {
// Background
canvas.setFillStyle(gameConfig.colors.background)
canvas.fillRect(0, 0, 600, 400)
// Grid lines (subtle)
canvas.setStrokeStyle("#3a4a5c")
canvas.setLineWidth(1)
local i, x, y
// Vertical lines
i = 0
loop(i <= gameConfig.gridWidth) {
x = i * gameConfig.gridSize
canvas.drawLine(x, 0, x, 400, "#3a4a5c", 1)
i = i + 1
}
// Horizontal lines
i = 0
loop(i <= gameConfig.gridHeight) {
y = i * gameConfig.gridSize
canvas.drawLine(0, y, 600, y, "#3a4a5c", 1)
i = i + 1
}
// Border
canvas.setStrokeStyle(gameConfig.colors.border)
canvas.setLineWidth(3)
canvas.strokeRect(0, 0, 600, 400)
}
local drawSnake
drawSnake = function() {
local i, bodyPart, x, y
i = 0
loop(i < snake.body.length()) {
bodyPart = snake.body[i]
x = bodyPart.x * gameConfig.gridSize
y = bodyPart.y * gameConfig.gridSize
if (i == 0) {
// Snake head
canvas.setFillStyle("#2ecc71")
canvas.fillRect(x + 2, y + 2, gameConfig.gridSize - 4, gameConfig.gridSize - 4)
// Eyes
canvas.setFillStyle("#2c3e50")
canvas.fillCircle(x + 6, y + 6, 2)
canvas.fillCircle(x + 14, y + 6, 2)
} else {
// Snake body
canvas.setFillStyle(gameConfig.colors.snake)
canvas.fillRect(x + 1, y + 1, gameConfig.gridSize - 2, gameConfig.gridSize - 2)
// Body segment gradient effect
canvas.setFillStyle("#229954")
canvas.fillRect(x + 3, y + 3, gameConfig.gridSize - 6, gameConfig.gridSize - 6)
}
i = i + 1
}
}
local drawFood
drawFood = function() {
local x, y
x = food.x * gameConfig.gridSize
y = food.y * gameConfig.gridSize
if (food.type == "bonus") {
// Bonus food (star shape)
canvas.setFillStyle("#f39c12")
canvas.fillCircle(x + gameConfig.gridSize / 2, y + gameConfig.gridSize / 2, 8)
canvas.setFillStyle("#e67e22")
canvas.fillCircle(x + gameConfig.gridSize / 2, y + gameConfig.gridSize / 2, 5)
} else {
// Normal food
canvas.setFillStyle(gameConfig.colors.food)
canvas.fillCircle(x + gameConfig.gridSize / 2, y + gameConfig.gridSize / 2, 7)
canvas.setFillStyle("#c0392b")
canvas.fillCircle(x + gameConfig.gridSize / 2, y + gameConfig.gridSize / 2, 4)
}
}
local drawHUD
drawHUD = function() {
// Score
canvas.setFillStyle(gameConfig.colors.text)
canvas.fillText("Score: " + score, 10, 25, "18px Arial", gameConfig.colors.text)
canvas.fillText("High: " + highScore, 10, 50, "14px Arial", "#bdc3c7")
// Snake length
canvas.fillText("Length: " + snake.body.length(), 150, 25, "14px Arial", "#95a5a6")
// Speed indicator
local speedText
speedText = "Speed: " + (200 - gameConfig.speed) + "%"
canvas.fillText(speedText, 250, 25, "14px Arial", "#95a5a6")
// Direction indicator
canvas.fillText("Dir: " + direction.toUpperCase(), 370, 25, "14px Arial", "#95a5a6")
// Controls hint
canvas.setFillStyle("#7f8c8d")
canvas.fillText("WASD/Arrows: Move | Space: Pause", 10, 385, "12px Arial", "#7f8c8d")
}
local drawGameOver
drawGameOver = function() {
// Semi-transparent overlay
canvas.setFillStyle("rgba(44, 62, 80, 0.9)")
canvas.fillRect(100, 150, 400, 150)
// Border
canvas.setStrokeStyle("#e74c3c")
canvas.setLineWidth(3)
canvas.strokeRect(100, 150, 400, 150)
// Game over text
canvas.setFillStyle("#e74c3c")
canvas.fillText("GAME OVER", 220, 200, "32px Arial", "#e74c3c")
// Final score
canvas.setFillStyle("#ecf0f1")
canvas.fillText("Final Score: " + score, 220, 230, "18px Arial", "#ecf0f1")
if (score == highScore) {
canvas.setFillStyle("#f39c12")
canvas.fillText("NEW HIGH SCORE!", 210, 255, "16px Arial", "#f39c12")
}
// Restart hint
canvas.setFillStyle("#95a5a6")
canvas.fillText("Press R to restart", 235, 280, "14px Arial", "#95a5a6")
}
local drawPaused
drawPaused = function() {
canvas.setFillStyle("rgba(52, 73, 94, 0.8)")
canvas.fillRect(200, 180, 200, 80)
canvas.setStrokeStyle("#3498db")
canvas.setLineWidth(2)
canvas.strokeRect(200, 180, 200, 80)
canvas.setFillStyle("#3498db")
canvas.fillText("PAUSED", 260, 215, "24px Arial", "#3498db")
canvas.setFillStyle("#bdc3c7")
canvas.fillText("Press Space to resume", 220, 240, "12px Arial", "#bdc3c7")
}
// Main game render function
local renderGame
renderGame = function() {
drawGrid()
drawSnake()
drawFood()
drawHUD()
if (gameState == "gameover") {
drawGameOver()
} else if (gameState == "paused") {
drawPaused()
}
}
// Game initialization
generateFood()
renderGame()
// Simulate some gameplay for demo
local demoMoves
demoMoves = 0
local simulateGameplay
simulateGameplay = function() {
loop(demoMoves < 15 and gameState == "playing") {
updateSnake()
renderGame()
demoMoves = demoMoves + 1
// Change direction occasionally for demo
if (demoMoves == 5) {
nextDirection = "down"
} else if (demoMoves == 10) {
nextDirection = "left"
}
}
}
simulateGameplay()
print("🎮 Simple Snake Game Demo Ready!")
print("• Grid size: " + gameConfig.gridWidth + "x" + gameConfig.gridHeight)
print("• Current score: " + score)
print("• High score: " + highScore)
print("• Snake length: " + snake.body.length())
print("• Game state: " + gameState)
// Demo advanced features
print("🌟 Game features demonstrated:")
print("• Collision detection (walls and self)")
print("• Food generation with obstacle avoidance")
print("• Smooth snake movement and growth")
print("• Score system with bonus food")
print("• Professional game UI with HUD")
print("• Pause/resume functionality")
print("• High score tracking")
// Show power-up system concept
local powerUps
powerUps = [
{name: "Speed Boost", duration: 5000, effect: "speed"},
{name: "Invincible", duration: 3000, effect: "invincible"},
{name: "Score Multiplier", duration: 8000, effect: "multiplier"}
]
canvas.setFillStyle("#9b59b6")
canvas.fillRect(450, 50, 140, 80)
canvas.setStrokeStyle("#8e44ad")
canvas.strokeRect(450, 50, 140, 80)
canvas.setFillStyle("#ffffff")
canvas.fillText("Power-ups:", 460, 70, "12px Arial", "#ffffff")
canvas.fillText("Speed Boost", 460, 85, "10px Arial", "#ffffff")
canvas.fillText("Invincible", 460, 100, "10px Arial", "#ffffff")
canvas.fillText("2x Score", 460, 115, "10px Arial", "#ffffff")
print("🎯 Advanced concepts ready for implementation:")
print("• Power-up system with " + powerUps.length() + " types")
print("• Multiple difficulty levels")
print("• Sound effects and music")
print("• Particle effects for food collection")
print("• Local multiplayer support")
print("🌐 Everything is Box - even classic arcade games!")
print("✅ Simple Snake Game Demo Complete!")