feat(2048): add 2048 tile sliding game

This commit is contained in:
2026-07-26 21:53:09 -04:00
parent 2af8d8fdae
commit 4f558bc75a
4 changed files with 261 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# 2048
Slide tiles with the direction buttons; equal tiles merge and the score grows. A new 2 (or
occasionally 4) appears after every move that changes the board. The game ends when the grid is full
with no equal neighbours.
## Controls
| Button | Action |
|---|---|
| Up / Down / Left / Right | Slide the board |
| Confirm | New game |
| Back | Exit |
## Notes
- The best score is kept in `/.apps/2048/best.txt` and written on game over, new game, and exit.
- Tile shade tracks magnitude (white → light gray → dark gray → black), which is the one place the
panel renders true grayscale via `gui.fillRoundedRect`.
- Every move repaints the whole grid with `REFRESH_FAST`; the panel diffs frames, so only changed
tiles actually move.
## Testing
Logic runs off-device with stubbed firmware globals:
```bash
nix run nixpkgs#lua -- _scratch/test_2048.lua
```
+229
View File
@@ -0,0 +1,229 @@
-- 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 top = 86
local gap = 8
local size = math.min(
math.floor((width - 48 - gap * (SIZE - 1)) / SIZE),
math.floor((height - top - 80 - gap * (SIZE - 1)) / SIZE))
local gridWidth = size * SIZE + gap * (SIZE - 1)
local startX = math.floor((width - gridWidth) / 2)
gui.clear()
gui.drawCenteredText(FONT_NOTOSANS_16, 16, "2048", COLOR_BLACK, STYLE_BOLD)
gui.drawCenteredText(FONT_SMALL, 46, "Score " .. score .. " Best " .. best, COLOR_BLACK, STYLE_REGULAR)
gui.drawCenteredText(FONT_SMALL, 64, 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
+2
View File
@@ -0,0 +1,2 @@
XTEINK-MANIFEST|1
main.lua|6324|da9bc8bbe2153eb8ba85e295dbe5e4ec9f4b0c8df57c2fc3eec5ed2dc185bba1
+1
View File
@@ -1,4 +1,5 @@
XTEINK-CATALOG|1
2048|Slide and merge tiles to reach 2048|apps/2048/manifest.txt|97|44ea59e862e09bb4ed034fc3fe9c280a13bb814bd6cc16670482bb3e34db0180
AppStore|Install apps from public Xteink repositories|apps/AppStore/manifest.txt|192|2eb608b92de6bdeff4fc8ed447b9319f45c0d79d5d73b14db1ab5a92f91e42d4
BinaryKeyboard|Two-button recursive keyboard experiment|apps/BinaryKeyboard/manifest.txt|97|893867dba0ae2725caca1eb56375c98c4dd00d351134aa129698c51f97e25690
HomeAssistant|Home Assistant status cards|apps/HomeAssistant/manifest.txt|177|b38ca49804444fd383a505ce9c51680ad9b7443950d682ca1b9ef3047bff98a5