315 lines
9.0 KiB
Lua
315 lines
9.0 KiB
Lua
-- 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_scaled(game.width() / 2 - 20, game.height() / 2 - 30, "TETRIS", true, 2)
|
|
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
|
|
|
|
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_scaled(game.width() - 50, 10, "Score: " .. tostring(game.vars.score), true, 2)
|
|
renderer.text_scaled(game.width() - 50, 20, "Lines: " .. tostring(game.vars.lines), true, 2)
|
|
|
|
if state == STATE_GAME_OVER then
|
|
renderer.text_scaled(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true, 2)
|
|
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true, 2)
|
|
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
|