-- DESCRIPTION: Slide and merge tiles to reach 2048 local SIZE = 4 local CELLS = SIZE * SIZE local BEST_PATH = "/.apps/2048/best.txt" local board = {} local score = 0 local best = 0 local message = "" local won = false local gameOver = false local firstDraw = true local needsDraw = true local function loadBest() local text = fs.readFile(BEST_PATH) best = tonumber(text and text:match("%d+") or "") or 0 end local function saveBest() if score <= best then return end best = score fs.writeFile(BEST_PATH, tostring(best)) end local function spawnTile() local empty = {} for index = 1, CELLS do if board[index] == 0 then empty[#empty + 1] = index end end if #empty == 0 then return end board[empty[math.random(#empty)]] = math.random(10) == 1 and 4 or 2 end local function startGame() for index = 1, CELLS do board[index] = 0 end score = 0 won = false gameOver = false message = "Slide to merge" firstDraw = true needsDraw = true spawnTile() spawnTile() end -- Line Indices - Every move is the same left-slide over a row or column read in the right order. local function lineIndices(direction, line) local vertical = direction == "up" or direction == "down" local reversed = direction == "right" or direction == "down" local indices = {} for step = 1, SIZE do local offset = reversed and SIZE + 1 - step or step if vertical then indices[step] = (offset - 1) * SIZE + line else indices[step] = (line - 1) * SIZE + offset end end return indices end local function slide(values) local packed = {} for _, value in ipairs(values) do if value ~= 0 then packed[#packed + 1] = value end end local out, gained, index = {}, 0, 1 while index <= #packed do if packed[index] == packed[index + 1] then local merged = packed[index] * 2 out[#out + 1] = merged gained = gained + merged index = index + 2 else out[#out + 1] = packed[index] index = index + 1 end end for position = #out + 1, SIZE do out[position] = 0 end return out, gained end local function move(direction) local moved = false for line = 1, SIZE do local indices = lineIndices(direction, line) local values = {} for step = 1, SIZE do values[step] = board[indices[step]] end local slid, gained = slide(values) score = score + gained for step = 1, SIZE do if board[indices[step]] ~= slid[step] then board[indices[step]] = slid[step] moved = true end end end return moved end local function hasMoves() for index = 1, CELLS do if board[index] == 0 then return true end local column = (index - 1) % SIZE + 1 if column < SIZE and board[index] == board[index + 1] then return true end if index + SIZE <= CELLS and board[index] == board[index + SIZE] then return true end end return false end local function step(direction) if move(direction) then spawnTile() message = "Slide to merge" if not won then for index = 1, CELLS do if board[index] >= 2048 then won = true message = "2048! Keep going" end end end else message = "No room that way" end if not hasMoves() then gameOver = true message = "No moves - OK for a new game" saveBest() end needsDraw = true end local function tileStyle(value) if value >= 256 then return COLOR_BLACK, COLOR_WHITE end if value >= 32 then return COLOR_DARK_GRAY, COLOR_WHITE end if value >= 8 then return COLOR_LIGHT_GRAY, COLOR_BLACK end return COLOR_WHITE, COLOR_BLACK end local function drawTile(value, x, y, size) local fill, textColor = tileStyle(value) if value == 0 then gui.drawRoundedRect(x, y, size, size, 1, 6, COLOR_LIGHT_GRAY) return end gui.fillRoundedRect(x, y, size, size, 6, fill) gui.drawRoundedRect(x, y, size, size, 2, 6, COLOR_BLACK) local label = tostring(value) local font = FONT_NOTOSANS_16 if gui.getTextWidth(font, label, STYLE_BOLD) > size - 10 then font = FONT_NOTOSANS_12 end local textWidth = gui.getTextWidth(font, label, STYLE_BOLD) gui.drawText(font, x + math.floor((size - textWidth) / 2), y + math.floor(size / 2) - 10, label, textColor, STYLE_BOLD) end local function render() local width, height = gui.width(), gui.height() local headerBottom = 132 local hintsTop = height - 56 local gap = 10 local size = math.min( math.floor((width - 32 - gap * (SIZE - 1)) / SIZE), math.floor((hintsTop - headerBottom - gap * (SIZE - 1)) / SIZE)) local gridWidth = size * SIZE + gap * (SIZE - 1) local gridHeight = size * SIZE + gap * (SIZE - 1) local startX = math.floor((width - gridWidth) / 2) local top = headerBottom + math.floor((hintsTop - headerBottom - gridHeight) / 2) gui.clear() gui.drawCenteredText(FONT_NOTOSANS_16, 24, "2048", COLOR_BLACK, STYLE_BOLD) gui.drawCenteredText(FONT_NOTOSANS_14, 66, "Score " .. score .. " Best " .. best, COLOR_BLACK, STYLE_BOLD) gui.drawCenteredText(FONT_SMALL, 100, message, COLOR_BLACK, STYLE_REGULAR) for row = 1, SIZE do for column = 1, SIZE do drawTile(board[(row - 1) * SIZE + column], startX + (column - 1) * (size + gap), top + (row - 1) * (size + gap), size) end end gui.drawButtonHints("<<", "New", "<", ">") gui.refresh(firstDraw and REFRESH_HALF or REFRESH_FAST) firstDraw = false needsDraw = false end function init() math.randomseed(sys.millis()) loadBest() startGame() end function draw() if input.wasPressed("back") then saveBest() sys.exit() return end if input.wasPressed("confirm") then saveBest() startGame() elseif not gameOver then if input.wasPressed("left") then step("left") elseif input.wasPressed("right") then step("right") elseif input.wasPressed("up") then step("up") elseif input.wasPressed("down") then step("down") end end if needsDraw then render() end end