From 53a2fb046b6dacfb3bde3c033ae77e01f59a2b1c Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Thu, 12 Feb 2026 19:18:51 -0500 Subject: [PATCH] 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. --- games/lua_examples/asteroids.lua | 284 +++++++++++++++++++++++++ games/lua_examples/breakout.lua | 221 ++++++++++++++++++++ games/lua_examples/flappy_bird.lua | 168 +++++++++++++++ games/lua_examples/memory_match.lua | 198 ++++++++++++++++++ games/lua_examples/pong.lua | 204 ++++++++++++++++++ games/lua_examples/simon_says.lua | 193 +++++++++++++++++ games/lua_examples/tetris.lua | 314 ++++++++++++++++++++++++++++ 7 files changed, 1582 insertions(+) create mode 100644 games/lua_examples/asteroids.lua create mode 100644 games/lua_examples/breakout.lua create mode 100644 games/lua_examples/flappy_bird.lua create mode 100644 games/lua_examples/memory_match.lua create mode 100644 games/lua_examples/pong.lua create mode 100644 games/lua_examples/simon_says.lua create mode 100644 games/lua_examples/tetris.lua diff --git a/games/lua_examples/asteroids.lua b/games/lua_examples/asteroids.lua new file mode 100644 index 0000000..fefcc1d --- /dev/null +++ b/games/lua_examples/asteroids.lua @@ -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 diff --git a/games/lua_examples/breakout.lua b/games/lua_examples/breakout.lua new file mode 100644 index 0000000..904e8d7 --- /dev/null +++ b/games/lua_examples/breakout.lua @@ -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 diff --git a/games/lua_examples/flappy_bird.lua b/games/lua_examples/flappy_bird.lua new file mode 100644 index 0000000..99a3b36 --- /dev/null +++ b/games/lua_examples/flappy_bird.lua @@ -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 diff --git a/games/lua_examples/memory_match.lua b/games/lua_examples/memory_match.lua new file mode 100644 index 0000000..cb19e80 --- /dev/null +++ b/games/lua_examples/memory_match.lua @@ -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 diff --git a/games/lua_examples/pong.lua b/games/lua_examples/pong.lua new file mode 100644 index 0000000..e52f100 --- /dev/null +++ b/games/lua_examples/pong.lua @@ -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 diff --git a/games/lua_examples/simon_says.lua b/games/lua_examples/simon_says.lua new file mode 100644 index 0000000..efcd144 --- /dev/null +++ b/games/lua_examples/simon_says.lua @@ -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 diff --git a/games/lua_examples/tetris.lua b/games/lua_examples/tetris.lua new file mode 100644 index 0000000..88b83ed --- /dev/null +++ b/games/lua_examples/tetris.lua @@ -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