diff --git a/games/lua_examples/2048.lua b/games/lua_examples/2048.lua new file mode 100644 index 0000000..8b04f3f --- /dev/null +++ b/games/lua_examples/2048.lua @@ -0,0 +1,378 @@ +-- NAME: 2048 +-- DESC: Merge tiles to reach 2048 + +-- Game states +local STATE_MENU = 0 +local STATE_PLAYING = 1 +local STATE_GAME_OVER = 2 +local STATE_WIN = 3 + +-- Game constants +local GRID_SIZE = 4 +local TILE_SIZE = 20 +local TILE_SPACING = 2 + +function init() + game.vars.state = STATE_MENU + game.vars.score = 0 + + -- Grid (4x4, 0 = empty) + game.vars.grid = {} + for y = 1, GRID_SIZE do + game.vars.grid[y] = {} + for x = 1, GRID_SIZE do + game.vars.grid[y][x] = 0 + end + end + + game.vars.moved = false + game.vars.won = false + + -- Enable continuous updates + game.set_frame_updates(true) + + print("2048 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 + -- Determine swipe direction + local direction = get_swipe_direction(event.x, event.y) + if direction then + game.vars.moved = false + move_tiles(direction) + + if game.vars.moved then + spawn_tile() + + -- Check win/lose + if has_tile(2048) then + if not game.vars.won then + game.vars.state = STATE_WIN + game.vars.won = true + end + end + + if not can_move() then + game.vars.state = STATE_GAME_OVER + end + + return true + end + end + end + + elseif state == STATE_WIN then + if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then + -- Can continue playing or go to menu + if event.y < game.height() / 2 then + game.vars.state = STATE_PLAYING + else + game.vars.state = STATE_MENU + 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) + + local state = game.vars.state + + if state == STATE_MENU then + renderer.text(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true) + renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true) + + elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then + -- Draw grid + local start_x = (game.width() - (GRID_SIZE * (TILE_SIZE + TILE_SPACING))) / 2 + local start_y = 20 + + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + local tile_x = start_x + (x - 1) * (TILE_SIZE + TILE_SPACING) + local tile_y = start_y + (y - 1) * (TILE_SIZE + TILE_SPACING) + local value = game.vars.grid[y][x] + + if value == 0 then + -- Empty tile + renderer.rect(tile_x, tile_y, TILE_SIZE, TILE_SIZE, true, false) + else + -- Filled tile + renderer.rect(tile_x, tile_y, TILE_SIZE, TILE_SIZE, true, true) + + -- Draw value (simplified) + local text = tostring(value) + if string.len(text) <= 2 then + renderer.text(tile_x + 2, tile_y + 2, text, false) + else + renderer.text(tile_x + 1, tile_y + 2, text, false) + end + end + end + end + + -- Draw score + renderer.text(10, 10, "Score: " .. tostring(game.vars.score), true) + + if state == STATE_WIN then + renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "YOU WIN!", true) + renderer.text(game.width() / 2 - 60, game.height() / 2, "Tap up: Continue", true) + renderer.text(game.width() / 2 - 50, game.height() / 2 + 15, "Tap down: Menu", true) + end + + if state == STATE_GAME_OVER then + renderer.text(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true) + renderer.text(game.width() / 2 - 30, game.height() / 2 + 15, "Score: " .. tostring(game.vars.score), true) + renderer.text(game.width() / 2 - 60, game.height() / 2 + 30, "Tap to Menu", true) + end + end +end + +function get_swipe_direction(x, y) + -- Simple tap-based direction (could be enhanced with swipe tracking) + local mid_x = game.width() / 2 + local mid_y = game.height() / 2 + + local dx = x - mid_x + local dy = y - mid_y + + if math.abs(dx) > math.abs(dy) then + if dx > 0 then return "right" end + return "left" + else + if dy > 0 then return "down" end + return "up" + end +end + +function move_tiles(direction) + local old_grid = {} + for y = 1, GRID_SIZE do + old_grid[y] = {} + for x = 1, GRID_SIZE do + old_grid[y][x] = game.vars.grid[y][x] + end + end + + if direction == "left" then + move_left() + elseif direction == "right" then + move_right() + elseif direction == "up" then + move_up() + elseif direction == "down" then + move_down() + end + + -- Check if grid changed + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + if old_grid[y][x] ~= game.vars.grid[y][x] then + game.vars.moved = true + return + end + end + end +end + +function move_left() + for y = 1, GRID_SIZE do + -- Compact + local row = {} + for x = 1, GRID_SIZE do + if game.vars.grid[y][x] ~= 0 then + table.insert(row, game.vars.grid[y][x]) + end + end + + -- Merge + local i = 1 + while i < #row do + if row[i] == row[i + 1] then + row[i] = row[i] * 2 + game.vars.score = game.vars.score + row[i] + table.remove(row, i + 1) + end + i = i + 1 + end + + -- Fill back + for x = 1, GRID_SIZE do + if x <= #row then + game.vars.grid[y][x] = row[x] + else + game.vars.grid[y][x] = 0 + end + end + end +end + +function move_right() + for y = 1, GRID_SIZE do + local row = {} + for x = GRID_SIZE, 1, -1 do + if game.vars.grid[y][x] ~= 0 then + table.insert(row, game.vars.grid[y][x]) + end + end + + local i = 1 + while i < #row do + if row[i] == row[i + 1] then + row[i] = row[i] * 2 + game.vars.score = game.vars.score + row[i] + table.remove(row, i + 1) + end + i = i + 1 + end + + for x = GRID_SIZE, 1, -1 do + if GRID_SIZE - x + 1 <= #row then + game.vars.grid[y][x] = row[GRID_SIZE - x + 1] + else + game.vars.grid[y][x] = 0 + end + end + end +end + +function move_up() + for x = 1, GRID_SIZE do + local col = {} + for y = 1, GRID_SIZE do + if game.vars.grid[y][x] ~= 0 then + table.insert(col, game.vars.grid[y][x]) + end + end + + local i = 1 + while i < #col do + if col[i] == col[i + 1] then + col[i] = col[i] * 2 + game.vars.score = game.vars.score + col[i] + table.remove(col, i + 1) + end + i = i + 1 + end + + for y = 1, GRID_SIZE do + if y <= #col then + game.vars.grid[y][x] = col[y] + else + game.vars.grid[y][x] = 0 + end + end + end +end + +function move_down() + for x = 1, GRID_SIZE do + local col = {} + for y = GRID_SIZE, 1, -1 do + if game.vars.grid[y][x] ~= 0 then + table.insert(col, game.vars.grid[y][x]) + end + end + + local i = 1 + while i < #col do + if col[i] == col[i + 1] then + col[i] = col[i] * 2 + game.vars.score = game.vars.score + col[i] + table.remove(col, i + 1) + end + i = i + 1 + end + + for y = GRID_SIZE, 1, -1 do + if GRID_SIZE - y + 1 <= #col then + game.vars.grid[y][x] = col[GRID_SIZE - y + 1] + else + game.vars.grid[y][x] = 0 + end + end + end +end + +function spawn_tile() + local empty = {} + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + if game.vars.grid[y][x] == 0 then + table.insert(empty, {x = x, y = y}) + end + end + end + + if #empty > 0 then + local pos = empty[math.random(1, #empty)] + game.vars.grid[pos.y][pos.x] = math.random() > 0.9 and 4 or 2 + end +end + +function has_tile(value) + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + if game.vars.grid[y][x] == value then + return true + end + end + end + return false +end + +function can_move() + -- Check if any empty tiles + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + if game.vars.grid[y][x] == 0 then + return true + end + end + end + + -- Check if any merges possible + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + local val = game.vars.grid[y][x] + if x < GRID_SIZE and game.vars.grid[y][x + 1] == val then return true end + if y < GRID_SIZE and game.vars.grid[y + 1][x] == val then return true end + end + end + + return false +end + +function reset_game() + game.vars.score = 0 + game.vars.won = false + + for y = 1, GRID_SIZE do + for x = 1, GRID_SIZE do + game.vars.grid[y][x] = 0 + end + end + + spawn_tile() + spawn_tile() +end diff --git a/games/lua_examples/air_hockey.lua b/games/lua_examples/air_hockey.lua new file mode 100644 index 0000000..ed22db0 --- /dev/null +++ b/games/lua_examples/air_hockey.lua @@ -0,0 +1,189 @@ +-- NAME: Air Hockey +-- DESC: Fast-paced 2-player hockey + +-- Game states +local STATE_MENU = 0 +local STATE_PLAYING = 1 +local STATE_GAME_OVER = 2 + +-- Game constants +local PADDLE_WIDTH = 6 +local PADDLE_HEIGHT = 35 +local PUCK_RADIUS = 3 +local MAX_SCORE = 7 +local PUCK_SPEED = 4 + +function init() + game.vars.state = STATE_MENU + + -- 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 + + -- Puck + game.vars.puck_x = game.width() / 2 + game.vars.puck_y = game.height() / 2 + game.vars.puck_vel_x = PUCK_SPEED + game.vars.puck_vel_y = 1 + + -- Enable continuous updates + game.set_frame_updates(true) + + print("Air Hockey 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 via touch + if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.TOUCH_MOVE then + if event.x < game.width() / 2 then + -- Left paddle + game.vars.paddle_left_y = math.max(0, math.min(game.height() - PADDLE_HEIGHT, event.y - PADDLE_HEIGHT / 2)) + else + -- Right paddle + 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_puck() + check_collisions() + + -- Check win + 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 + 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, "AIR HOCKEY", true) + renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true) + renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "First to " .. tostring(MAX_SCORE), true) + + elseif state == STATE_PLAYING or state == STATE_GAME_OVER then + -- Draw center line + for y = 0, game.height(), 4 do + renderer.pixel(game.width() / 2, y, true) + end + + -- Draw goal areas (top/bottom highlights) + renderer.line(0, 5, game.width(), 5, true, 1) + renderer.line(0, game.height() - 5, game.width(), game.height() - 5, true, 1) + + -- Draw paddles + renderer.rect(5, game.vars.paddle_left_y, PADDLE_WIDTH, PADDLE_HEIGHT, true, true) + renderer.rect(game.width() - 5 - PADDLE_WIDTH, game.vars.paddle_right_y, PADDLE_WIDTH, PADDLE_HEIGHT, true, true) + + -- Draw puck + renderer.circle(game.vars.puck_x, game.vars.puck_y, PUCK_RADIUS, true, true) + + -- Draw scores + renderer.text(game.width() / 2 - 30, 5, tostring(game.vars.paddle_left_score), true) + renderer.text(game.width() / 2 + 20, 5, tostring(game.vars.paddle_right_score), true) + + if state == STATE_GAME_OVER then + local winner = game.vars.paddle_left_score > game.vars.paddle_right_score and "Player 1" or "Player 2" + renderer.text(game.width() / 2 - 50, game.height() / 2 - 20, "GAME OVER", true) + renderer.text(game.width() / 2 - 40, game.height() / 2, winner .. " Wins!", true) + renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) + end + end +end + +function update_puck() + -- Move puck + game.vars.puck_x = game.vars.puck_x + game.vars.puck_vel_x + game.vars.puck_y = game.vars.puck_y + game.vars.puck_vel_y + + -- Bounce off top/bottom + if game.vars.puck_y - PUCK_RADIUS < 0 or game.vars.puck_y + PUCK_RADIUS > game.height() then + game.vars.puck_vel_y = -game.vars.puck_vel_y + game.vars.puck_y = math.max(PUCK_RADIUS, math.min(game.height() - PUCK_RADIUS, game.vars.puck_y)) + end + + -- Goal: left side + if game.vars.puck_x < 0 then + game.vars.paddle_right_score = game.vars.paddle_right_score + 1 + reset_puck() + end + + -- Goal: right side + if game.vars.puck_x > game.width() then + game.vars.paddle_left_score = game.vars.paddle_left_score + 1 + reset_puck() + end +end + +function check_collisions() + -- Left paddle collision + if game.vars.puck_x - PUCK_RADIUS < 5 + PADDLE_WIDTH then + if game.vars.puck_y > game.vars.paddle_left_y and game.vars.puck_y < game.vars.paddle_left_y + PADDLE_HEIGHT then + if game.vars.puck_vel_x < 0 then + game.vars.puck_vel_x = -game.vars.puck_vel_x + 0.5 -- Speed up slightly + + -- Add spin + local hit_pos = (game.vars.puck_y - game.vars.paddle_left_y) / PADDLE_HEIGHT + game.vars.puck_vel_y = (hit_pos - 0.5) * 6 + end + end + end + + -- Right paddle collision + if game.vars.puck_x + PUCK_RADIUS > game.width() - 5 - PADDLE_WIDTH then + if game.vars.puck_y > game.vars.paddle_right_y and game.vars.puck_y < game.vars.paddle_right_y + PADDLE_HEIGHT then + if game.vars.puck_vel_x > 0 then + game.vars.puck_vel_x = -game.vars.puck_vel_x - 0.5 -- Speed up slightly + + -- Add spin + local hit_pos = (game.vars.puck_y - game.vars.paddle_right_y) / PADDLE_HEIGHT + game.vars.puck_vel_y = (hit_pos - 0.5) * 6 + end + end + end +end + +function reset_puck() + game.vars.puck_x = game.width() / 2 + game.vars.puck_y = game.height() / 2 + game.vars.puck_vel_x = PUCK_SPEED * (math.random() > 0.5 and 1 or -1) + game.vars.puck_vel_y = (math.random() - 0.5) * 2 +end + +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_puck() +end diff --git a/games/lua_examples/lunar_lander.lua b/games/lua_examples/lunar_lander.lua new file mode 100644 index 0000000..8d753ae --- /dev/null +++ b/games/lua_examples/lunar_lander.lua @@ -0,0 +1,173 @@ +-- NAME: Lunar Lander +-- DESC: Land safely with limited fuel + +-- Game states +local STATE_MENU = 0 +local STATE_PLAYING = 1 +local STATE_LANDED = 2 +local STATE_CRASHED = 3 + +-- Game constants +local GRAVITY = 0.15 +local THRUST_POWER = 0.3 +local MAX_FUEL = 100 +local SAFE_LANDING_SPEED = 1.5 + +function init() + game.vars.state = STATE_MENU + game.vars.score = 0 + + -- Lander + game.vars.lander_x = game.width() / 2 + game.vars.lander_y = 10 + game.vars.lander_vel_y = 0 + game.vars.fuel = MAX_FUEL + game.vars.thrusting = false + + -- Terrain + game.vars.landing_zone_x = game.width() / 2 - 20 + game.vars.landing_zone_w = 40 + game.vars.terrain_y = game.height() - 15 + + -- Enable continuous updates + game.set_frame_updates(true) + + print("Lunar Lander 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 thrust input + if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.TOUCH_MOVE then + if game.vars.fuel > 0 then + game.vars.thrusting = true + game.vars.fuel = game.vars.fuel - 1 + else + game.vars.thrusting = false + end + else + game.vars.thrusting = false + end + + -- Update physics on frame tick + if event.type == INPUT.FRAME_TICK then + update_lander() + check_landing() + return true + end + + elseif state == STATE_LANDED or state == STATE_CRASHED 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, "LUNAR LANDER", true) + renderer.text(game.width() / 2 - 70, game.height() / 2 - 5, "Land in the zone safely", true) + renderer.text(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Start", true) + + elseif state == STATE_PLAYING or state == STATE_LANDED or state == STATE_CRASHED then + -- Draw terrain + renderer.rect(0, game.vars.terrain_y, game.width(), game.height() - game.vars.terrain_y, true, true) + + -- Draw landing zone (outline) + renderer.rect(game.vars.landing_zone_x, game.vars.terrain_y - 2, game.vars.landing_zone_w, 2, true, true) + + -- Draw lander (triangle) + local lander_w = 8 + local lander_h = 6 + renderer.line(game.vars.lander_x - lander_w / 2, game.vars.lander_y + lander_h, + game.vars.lander_x, game.vars.lander_y, true, 1) + renderer.line(game.vars.lander_x, game.vars.lander_y, + game.vars.lander_x + lander_w / 2, game.vars.lander_y + lander_h, true, 1) + renderer.line(game.vars.lander_x - lander_w / 2, game.vars.lander_y + lander_h, + game.vars.lander_x + lander_w / 2, game.vars.lander_y + lander_h, true, 1) + + -- Draw thrust flame + if game.vars.thrusting then + renderer.line(game.vars.lander_x - 2, game.vars.lander_y + lander_h, + game.vars.lander_x - 1, game.vars.lander_y + lander_h + 3, true, 1) + renderer.line(game.vars.lander_x + 2, game.vars.lander_y + lander_h, + game.vars.lander_x + 1, game.vars.lander_y + lander_h + 3, true, 1) + end + + -- Draw stats + renderer.text(5, 5, "Fuel: " .. tostring(math.floor(game.vars.fuel)), true) + renderer.text(5, 15, "Speed: " .. tostring(math.floor(game.vars.lander_vel_y * 10)), true) + + if state == STATE_LANDED then + renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "LANDED!", 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 + + if state == STATE_CRASHED then + renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "CRASHED!", true) + renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) + end + end +end + +function update_lander() + -- Apply gravity + game.vars.lander_vel_y = game.vars.lander_vel_y + GRAVITY + + -- Apply thrust + if game.vars.thrusting then + game.vars.lander_vel_y = game.vars.lander_vel_y - THRUST_POWER + end + + -- Update position + game.vars.lander_y = game.vars.lander_y + game.vars.lander_vel_y +end + +function check_landing() + -- Check if lander reached terrain + if game.vars.lander_y + 6 >= game.vars.terrain_y then + local lander_left = game.vars.lander_x - 4 + local lander_right = game.vars.lander_x + 4 + local zone_left = game.vars.landing_zone_x + local zone_right = game.vars.landing_zone_x + game.vars.landing_zone_w + + -- Check if in landing zone + if lander_left >= zone_left and lander_right <= zone_right then + -- Check landing speed + if game.vars.lander_vel_y <= SAFE_LANDING_SPEED then + game.vars.state = STATE_LANDED + game.vars.score = 100 + math.floor(game.vars.fuel) + else + game.vars.state = STATE_CRASHED + end + else + game.vars.state = STATE_CRASHED + end + end +end + +function reset_game() + game.vars.lander_x = game.width() / 2 + game.vars.lander_y = 10 + game.vars.lander_vel_y = 0 + game.vars.fuel = MAX_FUEL + game.vars.thrusting = false + game.vars.score = 0 +end diff --git a/games/lua_examples/tic_tac_toe.lua b/games/lua_examples/tic_tac_toe.lua new file mode 100644 index 0000000..ae599a6 --- /dev/null +++ b/games/lua_examples/tic_tac_toe.lua @@ -0,0 +1,285 @@ +-- 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