feat: add 6 lua games for basic1 console
- Pong: 2-player paddle and ball game with spin mechanics - Flappy Bird: gravity physics, obstacle avoidance - Breakout: paddle control, brick grid, collision detection - Simon Says: sequence memory, animation timing - Memory Match: pair matching, flip animations, grid layout - Tetris: falling blocks, grid system, line clearing - Asteroids: vector math, rotation, projectiles, enemy spawning All games follow API conventions with state machines, touch input, frame-based animation, and persistent game.vars state management.
This commit is contained in:
284
games/lua_examples/asteroids.lua
Normal file
284
games/lua_examples/asteroids.lua
Normal file
@@ -0,0 +1,284 @@
|
||||
-- NAME: Asteroids
|
||||
-- DESC: Destroy asteroids, avoid collisions
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local SHIP_SPEED = 2
|
||||
local SHIP_ROTATION_SPEED = 8
|
||||
local BULLET_SPEED = 5
|
||||
local ASTEROID_SPAWN_RATE = 120
|
||||
local ASTEROID_SPEEDS = {1.5, 2, 2.5, 3}
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.level = 1
|
||||
|
||||
-- Ship
|
||||
game.vars.ship_x = game.width() / 2
|
||||
game.vars.ship_y = game.height() / 2
|
||||
game.vars.ship_angle = 0 -- Radians
|
||||
game.vars.ship_vel_x = 0
|
||||
game.vars.ship_vel_y = 0
|
||||
game.vars.thrusting = false
|
||||
|
||||
-- Bullets
|
||||
game.vars.bullets = {}
|
||||
game.vars.bullet_cooldown = 0
|
||||
|
||||
-- Asteroids
|
||||
game.vars.asteroids = {}
|
||||
game.vars.frame_count = 0
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Asteroids initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle input
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.TOUCH_MOVE then
|
||||
if event.x < game.width() / 2 then
|
||||
-- Left side: rotate counter-clockwise
|
||||
game.vars.ship_angle = game.vars.ship_angle - SHIP_ROTATION_SPEED * 0.017
|
||||
else
|
||||
-- Right side: rotate clockwise
|
||||
game.vars.ship_angle = game.vars.ship_angle + SHIP_ROTATION_SPEED * 0.017
|
||||
end
|
||||
|
||||
-- Thrust
|
||||
game.vars.thrusting = true
|
||||
else
|
||||
game.vars.thrusting = false
|
||||
end
|
||||
|
||||
-- Update physics on frame tick
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
update_ship()
|
||||
update_bullets()
|
||||
update_asteroids()
|
||||
check_collisions()
|
||||
|
||||
-- Spawn asteroids
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
if game.vars.frame_count >= ASTEROID_SPAWN_RATE then
|
||||
spawn_asteroid(game.width() / 2, game.height() / 2, 3)
|
||||
game.vars.frame_count = 0
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 35, game.height() / 2 - 30, "ASTEROIDS", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
|
||||
elseif state == STATE_PLAYING or state == STATE_GAME_OVER then
|
||||
-- Draw asteroids
|
||||
for i = 1, #game.vars.asteroids do
|
||||
local ast = game.vars.asteroids[i]
|
||||
renderer.circle(ast.x, ast.y, ast.size, true, false)
|
||||
end
|
||||
|
||||
-- Draw bullets
|
||||
for i = 1, #game.vars.bullets do
|
||||
local bullet = game.vars.bullets[i]
|
||||
renderer.pixel(bullet.x, bullet.y, true)
|
||||
end
|
||||
|
||||
-- Draw ship
|
||||
local ship_size = 6
|
||||
local nose_x = game.vars.ship_x + math.cos(game.vars.ship_angle) * ship_size
|
||||
local nose_y = game.vars.ship_y + math.sin(game.vars.ship_angle) * ship_size
|
||||
local back_x = game.vars.ship_x - math.cos(game.vars.ship_angle) * (ship_size / 2)
|
||||
local back_y = game.vars.ship_y - math.sin(game.vars.ship_angle) * (ship_size / 2)
|
||||
|
||||
renderer.line(game.vars.ship_x, game.vars.ship_y, nose_x, nose_y, true, 1)
|
||||
renderer.line(back_x, back_y, nose_x, nose_y, true, 1)
|
||||
|
||||
-- Draw thrust indicator
|
||||
if game.vars.thrusting then
|
||||
local flame_x = game.vars.ship_x - math.cos(game.vars.ship_angle) * ship_size
|
||||
local flame_y = game.vars.ship_y - math.sin(game.vars.ship_angle) * ship_size
|
||||
renderer.line(game.vars.ship_x, game.vars.ship_y, flame_x, flame_y, true, 1)
|
||||
end
|
||||
|
||||
-- Draw score
|
||||
renderer.text(10, 10, "Score: " .. tostring(game.vars.score), true)
|
||||
renderer.text(10, 20, "Level: " .. tostring(game.vars.level), true)
|
||||
|
||||
if state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function update_ship()
|
||||
if game.vars.thrusting then
|
||||
game.vars.ship_vel_x = game.vars.ship_vel_x + math.cos(game.vars.ship_angle) * SHIP_SPEED
|
||||
game.vars.ship_vel_y = game.vars.ship_vel_y + math.sin(game.vars.ship_angle) * SHIP_SPEED
|
||||
end
|
||||
|
||||
-- Friction
|
||||
game.vars.ship_vel_x = game.vars.ship_vel_x * 0.98
|
||||
game.vars.ship_vel_y = game.vars.ship_vel_y * 0.98
|
||||
|
||||
-- Move ship
|
||||
game.vars.ship_x = game.vars.ship_x + game.vars.ship_vel_x
|
||||
game.vars.ship_y = game.vars.ship_y + game.vars.ship_vel_y
|
||||
|
||||
-- Wrap around screen
|
||||
if game.vars.ship_x < 0 then game.vars.ship_x = game.width() end
|
||||
if game.vars.ship_x > game.width() then game.vars.ship_x = 0 end
|
||||
if game.vars.ship_y < 0 then game.vars.ship_y = game.height() end
|
||||
if game.vars.ship_y > game.height() then game.vars.ship_y = 0 end
|
||||
end
|
||||
|
||||
function update_bullets()
|
||||
-- Update existing bullets
|
||||
for i = #game.vars.bullets, 1, -1 do
|
||||
local bullet = game.vars.bullets[i]
|
||||
bullet.x = bullet.x + bullet.vel_x
|
||||
bullet.y = bullet.y + bullet.vel_y
|
||||
|
||||
-- Remove if off-screen
|
||||
if bullet.x < 0 or bullet.x > game.width() or bullet.y < 0 or bullet.y > game.height() then
|
||||
table.remove(game.vars.bullets, i)
|
||||
end
|
||||
end
|
||||
|
||||
-- Fire bullet
|
||||
game.vars.bullet_cooldown = math.max(0, game.vars.bullet_cooldown - 1)
|
||||
if game.vars.thrusting and game.vars.bullet_cooldown == 0 then
|
||||
local bullet_x = game.vars.ship_x + math.cos(game.vars.ship_angle) * 8
|
||||
local bullet_y = game.vars.ship_y + math.sin(game.vars.ship_angle) * 8
|
||||
|
||||
table.insert(game.vars.bullets, {
|
||||
x = bullet_x,
|
||||
y = bullet_y,
|
||||
vel_x = math.cos(game.vars.ship_angle) * BULLET_SPEED + game.vars.ship_vel_x,
|
||||
vel_y = math.sin(game.vars.ship_angle) * BULLET_SPEED + game.vars.ship_vel_y
|
||||
})
|
||||
|
||||
game.vars.bullet_cooldown = 5
|
||||
end
|
||||
end
|
||||
|
||||
function update_asteroids()
|
||||
for i = 1, #game.vars.asteroids do
|
||||
local ast = game.vars.asteroids[i]
|
||||
ast.x = ast.x + ast.vel_x
|
||||
ast.y = ast.y + ast.vel_y
|
||||
|
||||
-- Wrap around screen
|
||||
if ast.x < -ast.size then ast.x = game.width() + ast.size end
|
||||
if ast.x > game.width() + ast.size then ast.x = -ast.size end
|
||||
if ast.y < -ast.size then ast.y = game.height() + ast.size end
|
||||
if ast.y > game.height() + ast.size then ast.y = -ast.size end
|
||||
end
|
||||
end
|
||||
|
||||
function check_collisions()
|
||||
-- Bullet-asteroid collisions
|
||||
for b = #game.vars.bullets, 1, -1 do
|
||||
local bullet = game.vars.bullets[b]
|
||||
for a = #game.vars.asteroids, 1, -1 do
|
||||
local ast = game.vars.asteroids[a]
|
||||
|
||||
local dx = bullet.x - ast.x
|
||||
local dy = bullet.y - ast.y
|
||||
local dist = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if dist < ast.size then
|
||||
-- Hit!
|
||||
table.remove(game.vars.bullets, b)
|
||||
table.remove(game.vars.asteroids, a)
|
||||
game.vars.score = game.vars.score + (4 - ast.size) * 50
|
||||
|
||||
-- Spawn smaller asteroids
|
||||
if ast.size > 1 then
|
||||
for _ = 1, 2 do
|
||||
spawn_asteroid(ast.x, ast.y, ast.size - 1)
|
||||
end
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Ship-asteroid collisions
|
||||
for i = 1, #game.vars.asteroids do
|
||||
local ast = game.vars.asteroids[i]
|
||||
|
||||
local dx = game.vars.ship_x - ast.x
|
||||
local dy = game.vars.ship_y - ast.y
|
||||
local dist = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if dist < ast.size + 6 then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function spawn_asteroid(x, y, size)
|
||||
if size < 1 then return end
|
||||
|
||||
local speed = ASTEROID_SPEEDS[size]
|
||||
local angle = math.random() * math.pi * 2
|
||||
|
||||
table.insert(game.vars.asteroids, {
|
||||
x = x,
|
||||
y = y,
|
||||
size = size,
|
||||
vel_x = math.cos(angle) * speed,
|
||||
vel_y = math.sin(angle) * speed
|
||||
})
|
||||
end
|
||||
|
||||
function reset_game()
|
||||
game.vars.score = 0
|
||||
game.vars.level = 1
|
||||
game.vars.ship_x = game.width() / 2
|
||||
game.vars.ship_y = game.height() / 2
|
||||
game.vars.ship_angle = 0
|
||||
game.vars.ship_vel_x = 0
|
||||
game.vars.ship_vel_y = 0
|
||||
game.vars.bullets = {}
|
||||
game.vars.asteroids = {}
|
||||
game.vars.frame_count = 0
|
||||
|
||||
spawn_asteroid(game.width() / 2 - 40, game.height() / 2 - 40, 3)
|
||||
spawn_asteroid(game.width() / 2 + 40, game.height() / 2 + 40, 3)
|
||||
end
|
||||
221
games/lua_examples/breakout.lua
Normal file
221
games/lua_examples/breakout.lua
Normal file
@@ -0,0 +1,221 @@
|
||||
-- NAME: Breakout
|
||||
-- DESC: Break bricks with bouncing ball and paddle
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
local STATE_LEVEL_COMPLETE = 3
|
||||
|
||||
-- Game constants
|
||||
local PADDLE_WIDTH = 40
|
||||
local PADDLE_HEIGHT = 6
|
||||
local BALL_RADIUS = 4
|
||||
local BRICK_WIDTH = 16
|
||||
local BRICK_HEIGHT = 6
|
||||
local BRICK_COLS = 16
|
||||
local BRICK_ROWS = 5
|
||||
local BRICK_START_Y = 20
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.lives = 3
|
||||
|
||||
-- Paddle
|
||||
game.vars.paddle_x = (game.width() / 2) - (PADDLE_WIDTH / 2)
|
||||
|
||||
-- Ball
|
||||
game.vars.ball_x = game.width() / 2
|
||||
game.vars.ball_y = game.height() - 30
|
||||
game.vars.ball_vel_x = 2
|
||||
game.vars.ball_vel_y = -3
|
||||
|
||||
-- Bricks (true = exists, false = broken)
|
||||
game.vars.bricks = {}
|
||||
game.vars.bricks_remaining = 0
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Breakout initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle paddle input
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.TOUCH_MOVE then
|
||||
game.vars.paddle_x = math.max(0, math.min(game.width() - PADDLE_WIDTH, event.x - PADDLE_WIDTH / 2))
|
||||
end
|
||||
|
||||
-- Update physics on frame tick
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
update_ball()
|
||||
check_brick_collisions()
|
||||
|
||||
-- Check win
|
||||
if game.vars.bricks_remaining <= 0 then
|
||||
game.vars.state = STATE_LEVEL_COMPLETE
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_LEVEL_COMPLETE then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2 - 30, "BREAKOUT", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Draw bricks
|
||||
for row = 1, BRICK_ROWS do
|
||||
for col = 1, BRICK_COLS do
|
||||
local idx = (row - 1) * BRICK_COLS + col
|
||||
if game.vars.bricks[idx] then
|
||||
local x = (col - 1) * BRICK_WIDTH
|
||||
local y = BRICK_START_Y + (row - 1) * BRICK_HEIGHT
|
||||
renderer.rect(x, y, BRICK_WIDTH, BRICK_HEIGHT, true, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw paddle
|
||||
renderer.rect(game.vars.paddle_x, game.height() - PADDLE_HEIGHT - 2, PADDLE_WIDTH, PADDLE_HEIGHT, true, true)
|
||||
|
||||
-- Draw ball
|
||||
renderer.circle(game.vars.ball_x, game.vars.ball_y, BALL_RADIUS, true, true)
|
||||
|
||||
-- Draw score and lives
|
||||
renderer.text(5, 5, "Score: " .. tostring(game.vars.score), true)
|
||||
renderer.text(game.width() - 50, 5, "Lives: " .. tostring(game.vars.lives), true)
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true)
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true)
|
||||
|
||||
elseif state == STATE_LEVEL_COMPLETE then
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 - 20, "LEVEL COMPLETE!", true)
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true)
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true)
|
||||
end
|
||||
end
|
||||
|
||||
function update_ball()
|
||||
-- Move ball
|
||||
game.vars.ball_x = game.vars.ball_x + game.vars.ball_vel_x
|
||||
game.vars.ball_y = game.vars.ball_y + game.vars.ball_vel_y
|
||||
|
||||
-- Bounce off walls
|
||||
if game.vars.ball_x - BALL_RADIUS < 0 or game.vars.ball_x + BALL_RADIUS > game.width() then
|
||||
game.vars.ball_vel_x = -game.vars.ball_vel_x
|
||||
game.vars.ball_x = math.max(BALL_RADIUS, math.min(game.width() - BALL_RADIUS, game.vars.ball_x))
|
||||
end
|
||||
|
||||
-- Bounce off top
|
||||
if game.vars.ball_y - BALL_RADIUS < 0 then
|
||||
game.vars.ball_vel_y = -game.vars.ball_vel_y
|
||||
game.vars.ball_y = BALL_RADIUS
|
||||
end
|
||||
|
||||
-- Check paddle collision
|
||||
if game.vars.ball_y + BALL_RADIUS > game.height() - PADDLE_HEIGHT - 2 then
|
||||
if game.vars.ball_x > game.vars.paddle_x and game.vars.ball_x < game.vars.paddle_x + PADDLE_WIDTH then
|
||||
if game.vars.ball_vel_y > 0 then
|
||||
game.vars.ball_vel_y = -game.vars.ball_vel_y
|
||||
|
||||
-- Add spin based on hit position
|
||||
local hit_pos = (game.vars.ball_x - game.vars.paddle_x) / PADDLE_WIDTH
|
||||
game.vars.ball_vel_x = (hit_pos - 0.5) * 4
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fall off bottom = lose life
|
||||
if game.vars.ball_y > game.height() then
|
||||
game.vars.lives = game.vars.lives - 1
|
||||
if game.vars.lives <= 0 then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
else
|
||||
reset_ball()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function check_brick_collisions()
|
||||
for row = 1, BRICK_ROWS do
|
||||
for col = 1, BRICK_COLS do
|
||||
local idx = (row - 1) * BRICK_COLS + col
|
||||
if game.vars.bricks[idx] then
|
||||
local brick_x = (col - 1) * BRICK_WIDTH
|
||||
local brick_y = BRICK_START_Y + (row - 1) * BRICK_HEIGHT
|
||||
|
||||
-- Simple AABB collision with ball
|
||||
if game.vars.ball_x + BALL_RADIUS > brick_x and
|
||||
game.vars.ball_x - BALL_RADIUS < brick_x + BRICK_WIDTH and
|
||||
game.vars.ball_y + BALL_RADIUS > brick_y and
|
||||
game.vars.ball_y - BALL_RADIUS < brick_y + BRICK_HEIGHT then
|
||||
|
||||
-- Destroy brick
|
||||
game.vars.bricks[idx] = false
|
||||
game.vars.bricks_remaining = game.vars.bricks_remaining - 1
|
||||
game.vars.score = game.vars.score + 10
|
||||
|
||||
-- Bounce ball (simple: vertical bounce)
|
||||
game.vars.ball_vel_y = -game.vars.ball_vel_y
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function reset_ball()
|
||||
game.vars.ball_x = game.vars.paddle_x + PADDLE_WIDTH / 2
|
||||
game.vars.ball_y = game.height() - 30
|
||||
game.vars.ball_vel_x = 2
|
||||
game.vars.ball_vel_y = -3
|
||||
end
|
||||
|
||||
function reset_game()
|
||||
game.vars.score = 0
|
||||
game.vars.lives = 3
|
||||
game.vars.paddle_x = (game.width() / 2) - (PADDLE_WIDTH / 2)
|
||||
|
||||
-- Create brick grid
|
||||
game.vars.bricks = {}
|
||||
game.vars.bricks_remaining = BRICK_ROWS * BRICK_COLS
|
||||
for i = 1, BRICK_ROWS * BRICK_COLS do
|
||||
game.vars.bricks[i] = true
|
||||
end
|
||||
|
||||
reset_ball()
|
||||
end
|
||||
168
games/lua_examples/flappy_bird.lua
Normal file
168
games/lua_examples/flappy_bird.lua
Normal file
@@ -0,0 +1,168 @@
|
||||
-- NAME: Flappy Bird
|
||||
-- DESC: Tap to flap, avoid pipes
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local BIRD_SIZE = 8
|
||||
local BIRD_GRAVITY = 0.3
|
||||
local BIRD_FLAP_POWER = -7
|
||||
local PIPE_WIDTH = 20
|
||||
local PIPE_GAP = 50
|
||||
local PIPE_SPEED = 3
|
||||
local SPAWN_RATE = 80 -- Frames between pipe spawns
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.frame_count = 0
|
||||
|
||||
-- Bird
|
||||
game.vars.bird_y = game.height() / 2
|
||||
game.vars.bird_vel = 0
|
||||
|
||||
-- Pipes (array of {x, gap_y})
|
||||
game.vars.pipes = {}
|
||||
game.vars.last_pipe_frame = 0
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Flappy Bird initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle flap input
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.bird_vel = BIRD_FLAP_POWER
|
||||
end
|
||||
|
||||
-- Update physics on frame tick
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
update_bird()
|
||||
update_pipes()
|
||||
check_collisions()
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 30, "FLAPPY BIRD", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Draw bird
|
||||
renderer.circle(20, game.vars.bird_y, BIRD_SIZE, true, true)
|
||||
|
||||
-- Draw pipes
|
||||
for i = 1, #game.vars.pipes do
|
||||
local pipe = game.vars.pipes[i]
|
||||
|
||||
-- Top pipe
|
||||
renderer.rect(pipe.x, 0, PIPE_WIDTH, pipe.gap_y, true, true)
|
||||
|
||||
-- Bottom pipe
|
||||
local bottom_start = pipe.gap_y + PIPE_GAP
|
||||
renderer.rect(pipe.x, bottom_start, PIPE_WIDTH, game.height() - bottom_start, true, true)
|
||||
end
|
||||
|
||||
-- Draw score
|
||||
renderer.text(10, 10, "Score: " .. tostring(game.vars.score), true)
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 30, "GAME OVER", true)
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true)
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Restart", true)
|
||||
end
|
||||
end
|
||||
|
||||
function update_bird()
|
||||
-- Apply gravity
|
||||
game.vars.bird_vel = game.vars.bird_vel + BIRD_GRAVITY
|
||||
game.vars.bird_y = game.vars.bird_y + game.vars.bird_vel
|
||||
|
||||
-- Clamp to screen (game over if hit top/bottom)
|
||||
if game.vars.bird_y - BIRD_SIZE < 0 or game.vars.bird_y + BIRD_SIZE > game.height() then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
end
|
||||
end
|
||||
|
||||
function update_pipes()
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
|
||||
-- Spawn new pipe
|
||||
if game.vars.frame_count - game.vars.last_pipe_frame >= SPAWN_RATE then
|
||||
local gap_y = math.random(30, game.height() - PIPE_GAP - 30)
|
||||
table.insert(game.vars.pipes, {x = game.width(), gap_y = gap_y})
|
||||
game.vars.last_pipe_frame = game.vars.frame_count
|
||||
end
|
||||
|
||||
-- Move and remove off-screen pipes
|
||||
for i = #game.vars.pipes, 1, -1 do
|
||||
local pipe = game.vars.pipes[i]
|
||||
pipe.x = pipe.x - PIPE_SPEED
|
||||
|
||||
-- Score when pipe passes bird
|
||||
if pipe.x == 20 then
|
||||
game.vars.score = game.vars.score + 1
|
||||
end
|
||||
|
||||
-- Remove if off-screen
|
||||
if pipe.x < -PIPE_WIDTH then
|
||||
table.remove(game.vars.pipes, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function check_collisions()
|
||||
-- Check collision with pipes
|
||||
for i = 1, #game.vars.pipes do
|
||||
local pipe = game.vars.pipes[i]
|
||||
|
||||
-- Bird hitbox: circle at (20, bird_y) with radius BIRD_SIZE
|
||||
-- Check if within pipe's X range
|
||||
if 20 + BIRD_SIZE > pipe.x and 20 - BIRD_SIZE < pipe.x + PIPE_WIDTH then
|
||||
-- Check Y collision
|
||||
if game.vars.bird_y - BIRD_SIZE < pipe.gap_y or
|
||||
game.vars.bird_y + BIRD_SIZE > pipe.gap_y + PIPE_GAP then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function reset_game()
|
||||
game.vars.score = 0
|
||||
game.vars.bird_y = game.height() / 2
|
||||
game.vars.bird_vel = 0
|
||||
game.vars.pipes = {}
|
||||
game.vars.frame_count = 0
|
||||
game.vars.last_pipe_frame = 0
|
||||
end
|
||||
198
games/lua_examples/memory_match.lua
Normal file
198
games/lua_examples/memory_match.lua
Normal file
@@ -0,0 +1,198 @@
|
||||
-- NAME: Memory Match
|
||||
-- DESC: Find matching pairs
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local GRID_COLS = 4
|
||||
local GRID_ROWS = 4
|
||||
local CARD_SIZE = 28
|
||||
local CARD_SPACING = 4
|
||||
local FLIP_DURATION = 15 -- Frames to show flip animation
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.moves = 0
|
||||
|
||||
-- Card grid
|
||||
game.vars.cards = {} -- {id, face_up, matched}
|
||||
game.vars.selected = {} -- Indices of selected cards
|
||||
game.vars.flip_frame = 0
|
||||
game.vars.waiting = false -- Waiting to flip back incorrect pair
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Memory Match initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle card selection
|
||||
if event.type == INPUT.TOUCH_DOWN and not game.vars.waiting then
|
||||
local card_idx = get_card_at(event.x, event.y)
|
||||
if card_idx and not game.vars.cards[card_idx].matched and not game.vars.cards[card_idx].face_up then
|
||||
-- Select card
|
||||
game.vars.cards[card_idx].face_up = true
|
||||
table.insert(game.vars.selected, card_idx)
|
||||
|
||||
if #game.vars.selected == 2 then
|
||||
game.vars.moves = game.vars.moves + 1
|
||||
|
||||
-- Check for match
|
||||
if game.vars.cards[game.vars.selected[1]].id == game.vars.cards[game.vars.selected[2]].id then
|
||||
-- Match!
|
||||
game.vars.cards[game.vars.selected[1]].matched = true
|
||||
game.vars.cards[game.vars.selected[2]].matched = true
|
||||
game.vars.score = game.vars.score + 1
|
||||
game.vars.selected = {}
|
||||
|
||||
-- Check win
|
||||
if game.vars.score == (GRID_COLS * GRID_ROWS) / 2 then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
end
|
||||
else
|
||||
-- No match, wait then flip back
|
||||
game.vars.waiting = true
|
||||
game.vars.flip_frame = 0
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle flip-back animation
|
||||
if game.vars.waiting and event.type == INPUT.FRAME_TICK then
|
||||
game.vars.flip_frame = game.vars.flip_frame + 1
|
||||
|
||||
if game.vars.flip_frame >= FLIP_DURATION then
|
||||
-- Flip cards back
|
||||
game.vars.cards[game.vars.selected[1]].face_up = false
|
||||
game.vars.cards[game.vars.selected[2]].face_up = false
|
||||
game.vars.selected = {}
|
||||
game.vars.waiting = false
|
||||
game.vars.flip_frame = 0
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 40, "MEMORY MATCH", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 - 10, "Find all pairs", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Start", true)
|
||||
|
||||
elseif state == STATE_PLAYING or state == STATE_GAME_OVER then
|
||||
-- Draw grid
|
||||
local start_x = (game.width() - (GRID_COLS * (CARD_SIZE + CARD_SPACING))) / 2
|
||||
local start_y = 30
|
||||
|
||||
for row = 0, GRID_ROWS - 1 do
|
||||
for col = 0, GRID_COLS - 1 do
|
||||
local idx = row * GRID_COLS + col + 1
|
||||
local card = game.vars.cards[idx]
|
||||
local x = start_x + col * (CARD_SIZE + CARD_SPACING)
|
||||
local y = start_y + row * (CARD_SIZE + CARD_SPACING)
|
||||
|
||||
if card.face_up or card.matched then
|
||||
-- Show card value
|
||||
renderer.rect(x, y, CARD_SIZE, CARD_SIZE, true, true)
|
||||
local text = tostring(card.id)
|
||||
renderer.text(x + CARD_SIZE / 2 - 2, y + CARD_SIZE / 2 - 2, text, false)
|
||||
else
|
||||
-- Face down (outline)
|
||||
renderer.rect(x, y, CARD_SIZE, CARD_SIZE, true, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw stats
|
||||
renderer.text(10, 10, "Pairs: " .. tostring(game.vars.score) .. "/" .. tostring((GRID_COLS * GRID_ROWS) / 2), true)
|
||||
renderer.text(10, 20, "Moves: " .. tostring(game.vars.moves), true)
|
||||
|
||||
if state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, 5, "YOU WIN!", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() - 20, "Tap to Menu", true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function get_card_at(x, y)
|
||||
local start_x = (game.width() - (GRID_COLS * (CARD_SIZE + CARD_SPACING))) / 2
|
||||
local start_y = 30
|
||||
|
||||
for row = 0, GRID_ROWS - 1 do
|
||||
for col = 0, GRID_COLS - 1 do
|
||||
local card_x = start_x + col * (CARD_SIZE + CARD_SPACING)
|
||||
local card_y = start_y + row * (CARD_SIZE + CARD_SPACING)
|
||||
|
||||
if x >= card_x and x < card_x + CARD_SIZE and
|
||||
y >= card_y and y < card_y + CARD_SIZE then
|
||||
return row * GRID_COLS + col + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function reset_game()
|
||||
game.vars.score = 0
|
||||
game.vars.moves = 0
|
||||
game.vars.selected = {}
|
||||
game.vars.flip_frame = 0
|
||||
game.vars.waiting = false
|
||||
|
||||
-- Create shuffled card pairs
|
||||
local card_ids = {}
|
||||
local num_pairs = (GRID_COLS * GRID_ROWS) / 2
|
||||
for i = 1, num_pairs do
|
||||
table.insert(card_ids, i)
|
||||
table.insert(card_ids, i)
|
||||
end
|
||||
|
||||
-- Shuffle
|
||||
for i = #card_ids, 2, -1 do
|
||||
local j = math.random(1, i)
|
||||
card_ids[i], card_ids[j] = card_ids[j], card_ids[i]
|
||||
end
|
||||
|
||||
-- Create card objects
|
||||
game.vars.cards = {}
|
||||
for i = 1, #card_ids do
|
||||
game.vars.cards[i] = {
|
||||
id = card_ids[i],
|
||||
face_up = false,
|
||||
matched = false
|
||||
}
|
||||
end
|
||||
end
|
||||
204
games/lua_examples/pong.lua
Normal file
204
games/lua_examples/pong.lua
Normal file
@@ -0,0 +1,204 @@
|
||||
-- NAME: Pong
|
||||
-- DESC: Classic two-player Pong game
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local PADDLE_WIDTH = 8
|
||||
local PADDLE_HEIGHT = 40
|
||||
local BALL_RADIUS = 5
|
||||
local MAX_SCORE = 5
|
||||
|
||||
-- Initialize game
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.frame_count = 0
|
||||
|
||||
-- Left paddle (player 1)
|
||||
game.vars.paddle_left_y = (game.height() / 2) - (PADDLE_HEIGHT / 2)
|
||||
game.vars.paddle_left_score = 0
|
||||
|
||||
-- Right paddle (player 2)
|
||||
game.vars.paddle_right_y = (game.height() / 2) - (PADDLE_HEIGHT / 2)
|
||||
game.vars.paddle_right_score = 0
|
||||
|
||||
-- Ball
|
||||
game.vars.ball_x = game.width() / 2
|
||||
game.vars.ball_y = game.height() / 2
|
||||
game.vars.ball_vel_x = 3
|
||||
game.vars.ball_vel_y = 2
|
||||
|
||||
-- Enable continuous frame updates for smooth animation
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Pong initialized")
|
||||
end
|
||||
|
||||
-- Update game logic
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
-- State: MENU
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
-- State: PLAYING
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle paddle input via touch
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.TOUCH_MOVE then
|
||||
-- Left side touch moves left paddle, right side touch moves right paddle
|
||||
if event.x < game.width() / 2 then
|
||||
-- Move left paddle (constrain within bounds)
|
||||
game.vars.paddle_left_y = math.max(0, math.min(game.height() - PADDLE_HEIGHT, event.y - PADDLE_HEIGHT / 2))
|
||||
else
|
||||
-- Move right paddle (constrain within bounds)
|
||||
game.vars.paddle_right_y = math.max(0, math.min(game.height() - PADDLE_HEIGHT, event.y - PADDLE_HEIGHT / 2))
|
||||
end
|
||||
end
|
||||
|
||||
-- Update physics on frame tick
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
update_ball()
|
||||
|
||||
-- Check win condition
|
||||
if game.vars.paddle_left_score >= MAX_SCORE or game.vars.paddle_right_score >= MAX_SCORE then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
end
|
||||
|
||||
return true -- Always redraw when playing
|
||||
end
|
||||
|
||||
-- State: GAME_OVER
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Draw game
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
-- Draw: MENU
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 15, game.height() / 2 - 30, "PONG", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
renderer.text(game.width() / 2 - 70, game.height() / 2 + 20, "First to " .. tostring(MAX_SCORE), true)
|
||||
|
||||
-- Draw: PLAYING
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Draw center line
|
||||
for y = 0, game.height(), 5 do
|
||||
renderer.pixel(game.width() / 2, y, true)
|
||||
end
|
||||
|
||||
-- Draw paddles
|
||||
renderer.rect(10, game.vars.paddle_left_y, PADDLE_WIDTH, PADDLE_HEIGHT, true, true)
|
||||
renderer.rect(game.width() - 10 - PADDLE_WIDTH, game.vars.paddle_right_y, PADDLE_WIDTH, PADDLE_HEIGHT, true, true)
|
||||
|
||||
-- Draw ball
|
||||
renderer.circle(game.vars.ball_x, game.vars.ball_y, BALL_RADIUS, true, true)
|
||||
|
||||
-- Draw scores
|
||||
local left_score_text = tostring(game.vars.paddle_left_score)
|
||||
local right_score_text = tostring(game.vars.paddle_right_score)
|
||||
renderer.text(game.width() / 2 - 30, 5, left_score_text, true)
|
||||
renderer.text(game.width() / 2 + 20, 5, right_score_text, true)
|
||||
|
||||
-- Draw: GAME_OVER
|
||||
elseif state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 30, "GAME OVER", true)
|
||||
|
||||
local winner = "Player 1 Wins!"
|
||||
if game.vars.paddle_right_score > game.vars.paddle_left_score then
|
||||
winner = "Player 2 Wins!"
|
||||
end
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, winner, true)
|
||||
|
||||
local final_text = game.vars.paddle_left_score .. " - " .. game.vars.paddle_right_score
|
||||
renderer.text(game.width() / 2 - 25, game.height() / 2 + 20, final_text, true)
|
||||
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 40, "Tap to Menu", true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: Update ball physics
|
||||
function update_ball()
|
||||
-- Move ball
|
||||
game.vars.ball_x = game.vars.ball_x + game.vars.ball_vel_x
|
||||
game.vars.ball_y = game.vars.ball_y + game.vars.ball_vel_y
|
||||
|
||||
-- Bounce off top/bottom
|
||||
if game.vars.ball_y - BALL_RADIUS < 0 or game.vars.ball_y + BALL_RADIUS > game.height() then
|
||||
game.vars.ball_vel_y = -game.vars.ball_vel_y
|
||||
game.vars.ball_y = math.max(BALL_RADIUS, math.min(game.height() - BALL_RADIUS, game.vars.ball_y))
|
||||
end
|
||||
|
||||
-- Check left paddle collision
|
||||
if game.vars.ball_x - BALL_RADIUS < 10 + PADDLE_WIDTH then
|
||||
if game.vars.ball_y > game.vars.paddle_left_y and game.vars.ball_y < game.vars.paddle_left_y + PADDLE_HEIGHT then
|
||||
if game.vars.ball_vel_x < 0 then
|
||||
game.vars.ball_vel_x = -game.vars.ball_vel_x
|
||||
|
||||
-- Add spin based on where ball hits paddle
|
||||
local hit_pos = (game.vars.ball_y - game.vars.paddle_left_y) / PADDLE_HEIGHT
|
||||
game.vars.ball_vel_y = (hit_pos - 0.5) * 6
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check right paddle collision
|
||||
if game.vars.ball_x + BALL_RADIUS > game.width() - 10 - PADDLE_WIDTH then
|
||||
if game.vars.ball_y > game.vars.paddle_right_y and game.vars.ball_y < game.vars.paddle_right_y + PADDLE_HEIGHT then
|
||||
if game.vars.ball_vel_x > 0 then
|
||||
game.vars.ball_vel_x = -game.vars.ball_vel_x
|
||||
|
||||
-- Add spin based on where ball hits paddle
|
||||
local hit_pos = (game.vars.ball_y - game.vars.paddle_right_y) / PADDLE_HEIGHT
|
||||
game.vars.ball_vel_y = (hit_pos - 0.5) * 6
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Score on left side miss
|
||||
if game.vars.ball_x < 0 then
|
||||
game.vars.paddle_right_score = game.vars.paddle_right_score + 1
|
||||
reset_ball()
|
||||
end
|
||||
|
||||
-- Score on right side miss
|
||||
if game.vars.ball_x > game.width() then
|
||||
game.vars.paddle_left_score = game.vars.paddle_left_score + 1
|
||||
reset_ball()
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: Reset ball to center
|
||||
function reset_ball()
|
||||
game.vars.ball_x = game.width() / 2
|
||||
game.vars.ball_y = game.height() / 2
|
||||
game.vars.ball_vel_x = 3 * (math.random() > 0.5 and 1 or -1)
|
||||
game.vars.ball_vel_y = 2 * (math.random() > 0.5 and 1 or -1)
|
||||
end
|
||||
|
||||
-- Helper: Reset game
|
||||
function reset_game()
|
||||
game.vars.paddle_left_y = (game.height() / 2) - (PADDLE_HEIGHT / 2)
|
||||
game.vars.paddle_left_score = 0
|
||||
game.vars.paddle_right_y = (game.height() / 2) - (PADDLE_HEIGHT / 2)
|
||||
game.vars.paddle_right_score = 0
|
||||
reset_ball()
|
||||
end
|
||||
193
games/lua_examples/simon_says.lua
Normal file
193
games/lua_examples/simon_says.lua
Normal file
@@ -0,0 +1,193 @@
|
||||
-- NAME: Simon Says
|
||||
-- DESC: Repeat the color sequence
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_SHOWING = 2
|
||||
local STATE_GAME_OVER = 3
|
||||
|
||||
-- Game constants
|
||||
local BUTTON_SIZE = 40
|
||||
local BUTTON_SPACING = 10
|
||||
local SHOW_DURATION = 30 -- Frames to show each button
|
||||
local WAIT_DURATION = 20 -- Frames between shows
|
||||
|
||||
-- Button positions (4 buttons in grid)
|
||||
local BUTTONS = {
|
||||
{x = 20, y = 20, color = 1}, -- Top-left
|
||||
{x = 80, y = 20, color = 2}, -- Top-right
|
||||
{x = 20, y = 80, color = 3}, -- Bottom-left
|
||||
{x = 80, y = 80, color = 4} -- Bottom-right
|
||||
}
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
|
||||
-- Sequence of button presses
|
||||
game.vars.sequence = {}
|
||||
game.vars.player_seq = {}
|
||||
|
||||
-- Animation state
|
||||
game.vars.showing_idx = 0
|
||||
game.vars.show_frame = 0
|
||||
game.vars.show_button = nil
|
||||
|
||||
-- Input state
|
||||
game.vars.waiting_for_input = false
|
||||
game.vars.input_idx = 0
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Simon Says initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Start showing sequence
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
game.vars.show_frame = game.vars.show_frame + 1
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
|
||||
elseif state == STATE_SHOWING then
|
||||
-- Animate sequence
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
game.vars.show_frame = game.vars.show_frame + 1
|
||||
|
||||
-- Move to next button in sequence
|
||||
if game.vars.show_frame > SHOW_DURATION + WAIT_DURATION then
|
||||
game.vars.showing_idx = game.vars.showing_idx + 1
|
||||
game.vars.show_frame = 0
|
||||
game.vars.show_button = nil
|
||||
|
||||
-- Done showing sequence, wait for player input
|
||||
if game.vars.showing_idx > #game.vars.sequence then
|
||||
game.vars.state = STATE_PLAYING
|
||||
game.vars.waiting_for_input = true
|
||||
game.vars.input_idx = 0
|
||||
end
|
||||
else
|
||||
-- Highlight button during show duration
|
||||
if game.vars.show_frame <= SHOW_DURATION then
|
||||
game.vars.show_button = game.vars.sequence[game.vars.showing_idx]
|
||||
else
|
||||
game.vars.show_button = nil
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle player input
|
||||
if game.vars.waiting_for_input and event.type == INPUT.TOUCH_DOWN then
|
||||
local button = get_button_at(event.x, event.y)
|
||||
if button then
|
||||
game.vars.player_seq[#game.vars.player_seq + 1] = button
|
||||
game.vars.input_idx = game.vars.input_idx + 1
|
||||
|
||||
-- Check if correct
|
||||
if game.vars.sequence[game.vars.input_idx] ~= button then
|
||||
-- Wrong! Game over
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if completed sequence
|
||||
if game.vars.input_idx == #game.vars.sequence then
|
||||
-- Advance to next round
|
||||
game.vars.sequence[#game.vars.sequence + 1] = math.random(1, 4)
|
||||
game.vars.player_seq = {}
|
||||
game.vars.waiting_for_input = false
|
||||
game.vars.showing_idx = 0
|
||||
game.vars.show_frame = 0
|
||||
game.vars.show_button = nil
|
||||
game.vars.state = STATE_SHOWING
|
||||
game.vars.score = game.vars.score + 1
|
||||
return true
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 40, "SIMON SAYS", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 - 10, "Repeat the sequence", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Start", true)
|
||||
|
||||
else
|
||||
-- Draw buttons with highlight
|
||||
for i = 1, 4 do
|
||||
local btn = BUTTONS[i]
|
||||
local filled = (game.vars.show_button == i)
|
||||
renderer.rect(btn.x, btn.y, BUTTON_SIZE, BUTTON_SIZE, true, filled)
|
||||
end
|
||||
|
||||
-- Draw score
|
||||
renderer.text(10, 10, "Level: " .. tostring(game.vars.score + 1), true)
|
||||
|
||||
if state == STATE_PLAYING and game.vars.waiting_for_input then
|
||||
renderer.text(game.width() / 2 - 40, game.height() - 20, "Your turn!", true)
|
||||
end
|
||||
|
||||
if state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
|
||||
renderer.text(game.width() / 2 - 30, game.height() / 2, "Level: " .. tostring(game.vars.score + 1), true)
|
||||
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Restart", true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function get_button_at(x, y)
|
||||
for i = 1, 4 do
|
||||
local btn = BUTTONS[i]
|
||||
if x >= btn.x and x < btn.x + BUTTON_SIZE and
|
||||
y >= btn.y and y < btn.y + BUTTON_SIZE then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function reset_game()
|
||||
game.vars.score = 0
|
||||
game.vars.sequence = {math.random(1, 4)}
|
||||
game.vars.player_seq = {}
|
||||
game.vars.showing_idx = 0
|
||||
game.vars.show_frame = 0
|
||||
game.vars.show_button = nil
|
||||
game.vars.waiting_for_input = false
|
||||
game.vars.input_idx = 0
|
||||
game.vars.state = STATE_SHOWING
|
||||
end
|
||||
314
games/lua_examples/tetris.lua
Normal file
314
games/lua_examples/tetris.lua
Normal file
@@ -0,0 +1,314 @@
|
||||
-- NAME: Tetris
|
||||
-- DESC: Stack falling blocks, clear lines
|
||||
|
||||
-- Game states
|
||||
local STATE_MENU = 0
|
||||
local STATE_PLAYING = 1
|
||||
local STATE_GAME_OVER = 2
|
||||
|
||||
-- Game constants
|
||||
local GRID_WIDTH = 10
|
||||
local GRID_HEIGHT = 20
|
||||
local CELL_SIZE = 8
|
||||
local SPAWN_RATE = 30 -- Frames before piece drops
|
||||
|
||||
-- Tetromino shapes (4 orientations each)
|
||||
local TETROMINOS = {
|
||||
-- I piece
|
||||
{
|
||||
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
|
||||
{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
|
||||
{{0, 0}, {1, 0}, {2, 0}, {3, 0}},
|
||||
{{0, 0}, {0, 1}, {0, 2}, {0, 3}}
|
||||
},
|
||||
-- O piece
|
||||
{
|
||||
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
|
||||
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
|
||||
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
|
||||
{{0, 0}, {1, 0}, {0, 1}, {1, 1}}
|
||||
},
|
||||
-- T piece
|
||||
{
|
||||
{{1, 0}, {0, 1}, {1, 1}, {2, 1}},
|
||||
{{1, 0}, {0, 1}, {1, 1}, {1, 2}},
|
||||
{{0, 1}, {1, 1}, {2, 1}, {1, 2}},
|
||||
{{1, 0}, {1, 1}, {2, 1}, {1, 2}}
|
||||
},
|
||||
-- S piece
|
||||
{
|
||||
{{1, 0}, {2, 0}, {0, 1}, {1, 1}},
|
||||
{{0, 0}, {0, 1}, {1, 1}, {1, 2}},
|
||||
{{1, 0}, {2, 0}, {0, 1}, {1, 1}},
|
||||
{{0, 0}, {0, 1}, {1, 1}, {1, 2}}
|
||||
},
|
||||
-- Z piece
|
||||
{
|
||||
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
|
||||
{{1, 0}, {0, 1}, {1, 1}, {0, 2}},
|
||||
{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
|
||||
{{1, 0}, {0, 1}, {1, 1}, {0, 2}}
|
||||
},
|
||||
-- J piece
|
||||
{
|
||||
{{0, 0}, {0, 1}, {1, 1}, {2, 1}},
|
||||
{{1, 0}, {2, 0}, {1, 1}, {1, 2}},
|
||||
{{0, 1}, {1, 1}, {2, 1}, {2, 0}},
|
||||
{{1, 0}, {1, 1}, {0, 2}, {1, 2}}
|
||||
},
|
||||
-- L piece
|
||||
{
|
||||
{{2, 0}, {0, 1}, {1, 1}, {2, 1}},
|
||||
{{1, 0}, {1, 1}, {1, 2}, {2, 2}},
|
||||
{{0, 1}, {1, 1}, {2, 1}, {0, 0}},
|
||||
{{0, 0}, {1, 0}, {1, 1}, {1, 2}}
|
||||
}
|
||||
}
|
||||
|
||||
function init()
|
||||
game.vars.state = STATE_MENU
|
||||
game.vars.score = 0
|
||||
game.vars.level = 1
|
||||
game.vars.lines = 0
|
||||
|
||||
-- Grid (0 = empty, 1 = filled)
|
||||
game.vars.grid = {}
|
||||
for y = 1, GRID_HEIGHT do
|
||||
game.vars.grid[y] = {}
|
||||
for x = 1, GRID_WIDTH do
|
||||
game.vars.grid[y][x] = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Current piece
|
||||
game.vars.piece = nil
|
||||
game.vars.piece_x = 0
|
||||
game.vars.piece_y = 0
|
||||
game.vars.piece_type = 0
|
||||
game.vars.piece_rotation = 0
|
||||
|
||||
-- Animation
|
||||
game.vars.frame_count = 0
|
||||
game.vars.clear_rows = {}
|
||||
game.vars.clearing = false
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
|
||||
print("Tetris initialized")
|
||||
end
|
||||
|
||||
function update(event)
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
reset_game()
|
||||
game.vars.state = STATE_PLAYING
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_PLAYING then
|
||||
-- Handle input
|
||||
if event.type == INPUT.TOUCH_DOWN then
|
||||
if event.x < game.width() / 2 then
|
||||
-- Left side: move left
|
||||
if can_move(game.vars.piece, game.vars.piece_x - 1, game.vars.piece_y, game.vars.piece_rotation) then
|
||||
game.vars.piece_x = game.vars.piece_x - 1
|
||||
end
|
||||
else
|
||||
-- Right side: move right
|
||||
if can_move(game.vars.piece, game.vars.piece_x + 1, game.vars.piece_y, game.vars.piece_rotation) then
|
||||
game.vars.piece_x = game.vars.piece_x + 1
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Update physics on frame tick
|
||||
if event.type == INPUT.FRAME_TICK then
|
||||
game.vars.frame_count = game.vars.frame_count + 1
|
||||
|
||||
if game.vars.clearing then
|
||||
-- Clear animation
|
||||
if game.vars.frame_count % 10 == 0 then
|
||||
clear_lines()
|
||||
game.vars.clearing = false
|
||||
end
|
||||
else
|
||||
-- Drop piece
|
||||
if game.vars.frame_count >= SPAWN_RATE then
|
||||
game.vars.frame_count = 0
|
||||
|
||||
if can_move(game.vars.piece, game.vars.piece_x, game.vars.piece_y + 1, game.vars.piece_rotation) then
|
||||
game.vars.piece_y = game.vars.piece_y + 1
|
||||
else
|
||||
-- Lock piece
|
||||
lock_piece()
|
||||
|
||||
-- Check for complete lines
|
||||
local complete = check_complete_lines()
|
||||
if #complete > 0 then
|
||||
game.vars.clear_rows = complete
|
||||
game.vars.clearing = true
|
||||
else
|
||||
spawn_piece()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
elseif state == STATE_GAME_OVER then
|
||||
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
|
||||
game.vars.state = STATE_MENU
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function draw()
|
||||
renderer.clear(false) -- Black background
|
||||
|
||||
local state = game.vars.state
|
||||
|
||||
if state == STATE_MENU then
|
||||
renderer.text(game.width() / 2 - 20, game.height() / 2 - 30, "TETRIS", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
|
||||
|
||||
elseif state == STATE_PLAYING or state == STATE_GAME_OVER then
|
||||
-- Draw grid
|
||||
local start_x = 20
|
||||
local start_y = 15
|
||||
|
||||
for y = 1, GRID_HEIGHT do
|
||||
for x = 1, GRID_WIDTH do
|
||||
if game.vars.grid[y][x] == 1 then
|
||||
renderer.rect(start_x + (x - 1) * CELL_SIZE, start_y + (y - 1) * CELL_SIZE, CELL_SIZE, CELL_SIZE, true, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw current piece
|
||||
if game.vars.piece then
|
||||
for _, block in ipairs(game.vars.piece) do
|
||||
local x = start_x + (game.vars.piece_x + block[1]) * CELL_SIZE
|
||||
local y = start_y + (game.vars.piece_y + block[2]) * CELL_SIZE
|
||||
renderer.rect(x, y, CELL_SIZE, CELL_SIZE, true, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw score
|
||||
renderer.text(game.width() - 50, 10, "Score: " .. tostring(game.vars.score), true)
|
||||
renderer.text(game.width() - 50, 20, "Lines: " .. tostring(game.vars.lines), true)
|
||||
|
||||
if state == STATE_GAME_OVER then
|
||||
renderer.text(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true)
|
||||
renderer.text(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function spawn_piece()
|
||||
game.vars.piece_type = math.random(1, #TETROMINOS)
|
||||
game.vars.piece_rotation = 1
|
||||
game.vars.piece = TETROMINOS[game.vars.piece_type][game.vars.piece_rotation]
|
||||
game.vars.piece_x = 3
|
||||
game.vars.piece_y = 0
|
||||
|
||||
-- Check if game over
|
||||
if not can_move(game.vars.piece, game.vars.piece_x, game.vars.piece_y, game.vars.piece_rotation) then
|
||||
game.vars.state = STATE_GAME_OVER
|
||||
end
|
||||
end
|
||||
|
||||
function lock_piece()
|
||||
if not game.vars.piece then return end
|
||||
|
||||
for _, block in ipairs(game.vars.piece) do
|
||||
local x = game.vars.piece_x + block[1] + 1
|
||||
local y = game.vars.piece_y + block[2] + 1
|
||||
|
||||
if y >= 1 and y <= GRID_HEIGHT and x >= 1 and x <= GRID_WIDTH then
|
||||
game.vars.grid[y][x] = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function can_move(piece, x, y, rotation)
|
||||
if not piece then return false end
|
||||
|
||||
for _, block in ipairs(piece) do
|
||||
local grid_x = x + block[1] + 1
|
||||
local grid_y = y + block[2] + 1
|
||||
|
||||
if grid_x < 1 or grid_x > GRID_WIDTH or grid_y < 1 or grid_y > GRID_HEIGHT then
|
||||
return false
|
||||
end
|
||||
|
||||
if game.vars.grid[grid_y][grid_x] == 1 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function check_complete_lines()
|
||||
local complete = {}
|
||||
|
||||
for y = 1, GRID_HEIGHT do
|
||||
local full = true
|
||||
for x = 1, GRID_WIDTH do
|
||||
if game.vars.grid[y][x] == 0 then
|
||||
full = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if full then
|
||||
table.insert(complete, y)
|
||||
end
|
||||
end
|
||||
|
||||
return complete
|
||||
end
|
||||
|
||||
function clear_lines()
|
||||
for _, y in ipairs(game.vars.clear_rows) do
|
||||
-- Remove line
|
||||
table.remove(game.vars.grid, y)
|
||||
-- Add empty line at top
|
||||
table.insert(game.vars.grid, 1, {})
|
||||
for x = 1, GRID_WIDTH do
|
||||
game.vars.grid[1][x] = 0
|
||||
end
|
||||
end
|
||||
|
||||
game.vars.score = game.vars.score + (#game.vars.clear_rows * 100)
|
||||
game.vars.lines = game.vars.lines + #game.vars.clear_rows
|
||||
game.vars.clear_rows = {}
|
||||
|
||||
spawn_piece()
|
||||
end
|
||||
|
||||
function reset_game()
|
||||
game.vars.score = 0
|
||||
game.vars.level = 1
|
||||
game.vars.lines = 0
|
||||
game.vars.frame_count = 0
|
||||
game.vars.clearing = false
|
||||
|
||||
-- Clear grid
|
||||
for y = 1, GRID_HEIGHT do
|
||||
for x = 1, GRID_WIDTH do
|
||||
game.vars.grid[y][x] = 0
|
||||
end
|
||||
end
|
||||
|
||||
spawn_piece()
|
||||
end
|
||||
Reference in New Issue
Block a user