- 2048: Grid merging, directional movement, score tracking, win/draw detection - Tic-Tac-Toe: Minimax AI opponent, perfect play, win detection - Lunar Lander: Gravity + thrust physics, fuel management, landing validation - Air Hockey: Refined paddle physics, puck acceleration, goal detection All games tested for state transitions, collision logic, and win conditions. Suite now complete with 10 classic games ready for SD card deployment.
286 lines
8.5 KiB
Lua
286 lines
8.5 KiB
Lua
-- NAME: Tic-Tac-Toe
|
|
-- DESC: Play vs AI opponent
|
|
|
|
-- Game states
|
|
local STATE_MENU = 0
|
|
local STATE_PLAYING = 1
|
|
local STATE_GAME_OVER = 2
|
|
|
|
-- Game constants
|
|
local GRID_SIZE = 3
|
|
local CELL_SIZE = 30
|
|
local CELL_SPACING = 2
|
|
|
|
function init()
|
|
game.vars.state = STATE_MENU
|
|
game.vars.grid = {}
|
|
game.vars.player = 1 -- 1 = X, 2 = O
|
|
game.vars.ai = 2
|
|
game.vars.game_over = false
|
|
game.vars.winner = 0 -- 0 = ongoing, 1 = player wins, 2 = ai wins, 3 = draw
|
|
|
|
-- Enable continuous updates
|
|
game.set_frame_updates(true)
|
|
|
|
print("Tic-Tac-Toe 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
|
|
if event.type == INPUT.TOUCH_DOWN then
|
|
local cell = get_cell_at(event.x, event.y)
|
|
if cell and game.vars.grid[cell] == 0 then
|
|
-- Player move
|
|
game.vars.grid[cell] = game.vars.player
|
|
|
|
-- Check win
|
|
local winner = check_winner()
|
|
if winner ~= 0 then
|
|
if winner == game.vars.player then
|
|
game.vars.winner = 1
|
|
else
|
|
game.vars.winner = 2
|
|
end
|
|
game.vars.state = STATE_GAME_OVER
|
|
return true
|
|
end
|
|
|
|
-- Check draw
|
|
if is_board_full() then
|
|
game.vars.winner = 3
|
|
game.vars.state = STATE_GAME_OVER
|
|
return true
|
|
end
|
|
|
|
-- AI move
|
|
local ai_move = find_best_move()
|
|
game.vars.grid[ai_move] = game.vars.ai
|
|
|
|
-- Check win
|
|
winner = check_winner()
|
|
if winner ~= 0 then
|
|
if winner == game.vars.ai then
|
|
game.vars.winner = 2
|
|
end
|
|
game.vars.state = STATE_GAME_OVER
|
|
return true
|
|
end
|
|
|
|
-- Check draw
|
|
if is_board_full() then
|
|
game.vars.winner = 3
|
|
game.vars.state = STATE_GAME_OVER
|
|
return true
|
|
end
|
|
|
|
return true
|
|
end
|
|
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)
|
|
|
|
local state = game.vars.state
|
|
|
|
if state == STATE_MENU then
|
|
renderer.text(game.width() / 2 - 50, game.height() / 2 - 30, "TIC-TAC-TOE", true)
|
|
renderer.text(game.width() / 2 - 50, game.height() / 2, "Play vs AI", 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_SIZE * (CELL_SIZE + CELL_SPACING))) / 2
|
|
local start_y = 30
|
|
|
|
for row = 0, GRID_SIZE - 1 do
|
|
for col = 0, GRID_SIZE - 1 do
|
|
local cell_idx = row * GRID_SIZE + col + 1
|
|
local cell_x = start_x + col * (CELL_SIZE + CELL_SPACING)
|
|
local cell_y = start_y + row * (CELL_SIZE + CELL_SPACING)
|
|
|
|
-- Draw cell
|
|
renderer.rect(cell_x, cell_y, CELL_SIZE, CELL_SIZE, true, false)
|
|
|
|
-- Draw X or O
|
|
local value = game.vars.grid[cell_idx]
|
|
if value == 1 then
|
|
-- Draw X
|
|
renderer.line(cell_x + 2, cell_y + 2, cell_x + CELL_SIZE - 2, cell_y + CELL_SIZE - 2, true, 1)
|
|
renderer.line(cell_x + CELL_SIZE - 2, cell_y + 2, cell_x + 2, cell_y + CELL_SIZE - 2, true, 1)
|
|
elseif value == 2 then
|
|
-- Draw O
|
|
renderer.circle(cell_x + CELL_SIZE / 2, cell_y + CELL_SIZE / 2, CELL_SIZE / 2 - 2, true, false)
|
|
end
|
|
end
|
|
end
|
|
|
|
if state == STATE_GAME_OVER then
|
|
if game.vars.winner == 1 then
|
|
renderer.text(game.width() / 2 - 30, 10, "YOU WIN!", true)
|
|
elseif game.vars.winner == 2 then
|
|
renderer.text(game.width() / 2 - 30, 10, "AI WINS!", true)
|
|
else
|
|
renderer.text(game.width() / 2 - 20, 10, "DRAW!", true)
|
|
end
|
|
|
|
renderer.text(game.width() / 2 - 60, game.height() - 15, "Tap to Menu", true)
|
|
end
|
|
end
|
|
end
|
|
|
|
function get_cell_at(x, y)
|
|
local start_x = (game.width() - (GRID_SIZE * (CELL_SIZE + CELL_SPACING))) / 2
|
|
local start_y = 30
|
|
|
|
for row = 0, GRID_SIZE - 1 do
|
|
for col = 0, GRID_SIZE - 1 do
|
|
local cell_x = start_x + col * (CELL_SIZE + CELL_SPACING)
|
|
local cell_y = start_y + row * (CELL_SIZE + CELL_SPACING)
|
|
|
|
if x >= cell_x and x < cell_x + CELL_SIZE and
|
|
y >= cell_y and y < cell_y + CELL_SIZE then
|
|
return row * GRID_SIZE + col + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function check_winner()
|
|
-- Check rows
|
|
for row = 0, GRID_SIZE - 1 do
|
|
local val = game.vars.grid[row * GRID_SIZE + 1]
|
|
if val ~= 0 then
|
|
local match = true
|
|
for col = 1, GRID_SIZE - 1 do
|
|
if game.vars.grid[row * GRID_SIZE + col + 1] ~= val then
|
|
match = false
|
|
break
|
|
end
|
|
end
|
|
if match then return val end
|
|
end
|
|
end
|
|
|
|
-- Check columns
|
|
for col = 0, GRID_SIZE - 1 do
|
|
local val = game.vars.grid[col + 1]
|
|
if val ~= 0 then
|
|
local match = true
|
|
for row = 1, GRID_SIZE - 1 do
|
|
if game.vars.grid[row * GRID_SIZE + col + 1] ~= val then
|
|
match = false
|
|
break
|
|
end
|
|
end
|
|
if match then return val end
|
|
end
|
|
end
|
|
|
|
-- Check diagonals
|
|
local val = game.vars.grid[1]
|
|
if val ~= 0 then
|
|
if game.vars.grid[5] == val and game.vars.grid[9] == val then
|
|
return val
|
|
end
|
|
end
|
|
|
|
val = game.vars.grid[3]
|
|
if val ~= 0 then
|
|
if game.vars.grid[5] == val and game.vars.grid[7] == val then
|
|
return val
|
|
end
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
function is_board_full()
|
|
for i = 1, 9 do
|
|
if game.vars.grid[i] == 0 then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function find_best_move()
|
|
local best_score = -1000
|
|
local best_move = nil
|
|
|
|
for i = 1, 9 do
|
|
if game.vars.grid[i] == 0 then
|
|
game.vars.grid[i] = game.vars.ai
|
|
local score = minimax(0, false)
|
|
game.vars.grid[i] = 0
|
|
|
|
if score > best_score then
|
|
best_score = score
|
|
best_move = i
|
|
end
|
|
end
|
|
end
|
|
|
|
return best_move or 5 -- Fallback to center
|
|
end
|
|
|
|
function minimax(depth, is_maximizing)
|
|
local winner = check_winner()
|
|
|
|
if winner == game.vars.ai then return 10 - depth end
|
|
if winner == game.vars.player then return depth - 10 end
|
|
if is_board_full() then return 0 end
|
|
|
|
if is_maximizing then
|
|
local best_score = -1000
|
|
for i = 1, 9 do
|
|
if game.vars.grid[i] == 0 then
|
|
game.vars.grid[i] = game.vars.ai
|
|
local score = minimax(depth + 1, false)
|
|
game.vars.grid[i] = 0
|
|
best_score = math.max(best_score, score)
|
|
end
|
|
end
|
|
return best_score
|
|
else
|
|
local best_score = 1000
|
|
for i = 1, 9 do
|
|
if game.vars.grid[i] == 0 then
|
|
game.vars.grid[i] = game.vars.player
|
|
local score = minimax(depth + 1, true)
|
|
game.vars.grid[i] = 0
|
|
best_score = math.min(best_score, score)
|
|
end
|
|
end
|
|
return best_score
|
|
end
|
|
end
|
|
|
|
function reset_game()
|
|
game.vars.grid = {}
|
|
for i = 1, 9 do
|
|
game.vars.grid[i] = 0
|
|
end
|
|
game.vars.winner = 0
|
|
end
|