Files
basic1/games/lua_examples/tic_tac_toe.lua
Adolfo Reyna b22170b62c Fix Lua game float-to-int conversion errors for renderer.circle()
All games had the same issue: renderer.circle() requires integer arguments,
but float calculations produced non-integer coordinates.

Fixed in all games using math.floor(x + 0.5) for proper rounding:
- pong.lua: Ball position
- air_hockey.lua: Puck position
- asteroids.lua: Asteroid positions
- ball.lua: Ball and trail positions, velocity line
- breakout.lua: Ball position
- flappy_bird.lua: Bird Y position
- counter.lua: Last touch marker position
- snake.lua: Food position (center calculation)
- tic_tac_toe.lua: O circle center position

Also fixed floating-point coordinate calculations in ball and line
drawing to ensure all coordinates are integers.
2026-02-12 20:51:26 -05:00

286 lines
8.6 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 (convert center to integers)
renderer.circle(math.floor(cell_x + CELL_SIZE / 2 + 0.5), math.floor(cell_y + CELL_SIZE / 2 + 0.5), 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