feat(2048): add 2048 tile sliding game
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
XTEINK-MANIFEST|1
|
||||
main.lua|6324|da9bc8bbe2153eb8ba85e295dbe5e4ec9f4b0c8df57c2fc3eec5ed2dc185bba1
|
||||
Reference in New Issue
Block a user