-- 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 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 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 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 end function init() math.randomseed(sys.millis()) loadBest() startGame() render() end function on_button(name, event) if event ~= "down" then return end if name == "back" then saveBest() sys.exit() return end if name == "confirm" then saveBest() startGame() render() elseif not gameOver then if name == "left" then step("left") elseif name == "right" then step("right") elseif name == "page_back" then step("up") elseif name == "page_forward" then step("down") else return end render() end end