233 lines
6.4 KiB
Lua
233 lines
6.4 KiB
Lua
-- 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
|