mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-08-22 12:29:51 +00:00
apps: add 2048 to the Store (pisti87, #265)
Community submission, reviewed before publishing rather than taken on trust. The whole API surface it touches is ui.label / ui.canvas / ui.colors, sys.random, timer.every(100) and two store keys for the high score. No network, no mesh transmit, no filesystem, no dynamic code loading (load/loadstring/require/dofile), no _G, metatable, debug or coroutine tricks, and no long-string or numeric-escape obfuscation. Persistence is one bounded value. Nothing in it can reach past its own window, so it needs no permission grant. It also uses the real lifecycle contract — an app table with on_open/on_tick/ on_input/on_close, returned from the chunk — which is worth noting because the published SDK page told people to do it a different way until today. He got it right by reading the shipped apps. Parse-checked against the firmware's own vendored Lua 5.4.7 rather than a system interpreter, so the syntax is verified by the same parser the device runs. Not run on hardware here; pisti87 reports it working on a T-Deck Plus. deploy-apps.sh now also verifies that every apps.json entry has its <id>/<ver>/<id>.lua and .json on disk. An entry pointing at a missing file lists in the Store and then fails to install with nothing on screen to explain why — the same failure the language check already prevents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
de45b1b158
commit
8cc4a2b981
@@ -0,0 +1 @@
|
||||
{"id":"2048","name":"2048","ver":"1.2","desc":"The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87."}
|
||||
@@ -0,0 +1,837 @@
|
||||
-- 2048 — wada.* reference app
|
||||
-- Swipe (or trackball mapped swipe) to move tiles.
|
||||
-- Tap after win or game over to restart.
|
||||
|
||||
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
|
||||
local C = ui.colors
|
||||
|
||||
local app = {}
|
||||
|
||||
local cv, score_lbl
|
||||
|
||||
local CELL = 38
|
||||
|
||||
local grid = {}
|
||||
|
||||
local history = {
|
||||
grids = {},
|
||||
maxsize = 5
|
||||
}
|
||||
|
||||
local px_w, px_h
|
||||
local score = 0
|
||||
local hiscore = 0
|
||||
|
||||
local over = false
|
||||
local won = false
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- History
|
||||
--------------------------------------------------
|
||||
|
||||
function history:save()
|
||||
|
||||
local old = {
|
||||
grid = {},
|
||||
score = score,
|
||||
over = over,
|
||||
won = won
|
||||
}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
old.grid[x] = {}
|
||||
|
||||
for y = 1, 4 do
|
||||
old.grid[x][y] = grid[x][y]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
table.insert(self.grids, old)
|
||||
|
||||
if #self.grids > self.maxsize then
|
||||
table.remove(self.grids, 1)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function history:revert()
|
||||
|
||||
if #self.grids > 0 then
|
||||
|
||||
local old = table.remove(self.grids)
|
||||
|
||||
grid = old.grid
|
||||
score = old.score
|
||||
over = old.over
|
||||
won = old.won
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Game logic
|
||||
--------------------------------------------------
|
||||
|
||||
local function clear_grid()
|
||||
|
||||
grid = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
grid[x] = {}
|
||||
|
||||
for y = 1, 4 do
|
||||
grid[x][y] = 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Add random tile
|
||||
--------------------------------------------------
|
||||
|
||||
local function add_random_tile()
|
||||
|
||||
local free = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
if grid[x][y] == 0 then
|
||||
|
||||
table.insert(
|
||||
free,
|
||||
{
|
||||
x = x,
|
||||
y = y
|
||||
}
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Board is full.
|
||||
if #free == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local p
|
||||
|
||||
|
||||
-- If there is only one empty cell,
|
||||
-- use it directly.
|
||||
if #free == 1 then
|
||||
|
||||
p = free[1]
|
||||
|
||||
else
|
||||
|
||||
local index = sys.random(1, #free)
|
||||
|
||||
if index == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
p = free[index]
|
||||
|
||||
if p == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- 10% chance for 4,
|
||||
-- 90% chance for 2.
|
||||
if sys.random(0, 9) == 0 then
|
||||
grid[p.x][p.y] = 4
|
||||
else
|
||||
grid[p.x][p.y] = 2
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Reset
|
||||
--------------------------------------------------
|
||||
|
||||
local function reset()
|
||||
|
||||
clear_grid()
|
||||
|
||||
score = 0
|
||||
over = false
|
||||
won = false
|
||||
|
||||
history.grids = {}
|
||||
|
||||
add_random_tile()
|
||||
add_random_tile()
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Compress line
|
||||
--------------------------------------------------
|
||||
|
||||
local function compress(line)
|
||||
|
||||
local r = {}
|
||||
|
||||
for i = 1, 4 do
|
||||
|
||||
if line[i] ~= 0 then
|
||||
table.insert(r, line[i])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
while #r < 4 do
|
||||
table.insert(r, 0)
|
||||
end
|
||||
|
||||
return r
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Merge line
|
||||
--------------------------------------------------
|
||||
|
||||
local function merge(line)
|
||||
|
||||
local r = {}
|
||||
local i = 1
|
||||
|
||||
while i <= 4 do
|
||||
|
||||
if i < 4
|
||||
and line[i] ~= 0
|
||||
and line[i] == line[i + 1]
|
||||
then
|
||||
|
||||
local v = line[i] * 2
|
||||
|
||||
table.insert(r, v)
|
||||
|
||||
score = score + v
|
||||
|
||||
|
||||
-- Permanent high score.
|
||||
if score > hiscore then
|
||||
|
||||
hiscore = score
|
||||
|
||||
store.set(
|
||||
"2048_best",
|
||||
hiscore
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Reaching 2048 means victory.
|
||||
if v >= 2048 then
|
||||
won = true
|
||||
end
|
||||
|
||||
|
||||
i = i + 2
|
||||
|
||||
else
|
||||
|
||||
table.insert(r, line[i])
|
||||
|
||||
i = i + 1
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
while #r < 4 do
|
||||
table.insert(r, 0)
|
||||
end
|
||||
|
||||
return r
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Move left
|
||||
--------------------------------------------------
|
||||
|
||||
local function move_left()
|
||||
|
||||
local changed = false
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
local old = {}
|
||||
local line = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
old[x] = grid[x][y]
|
||||
line[x] = grid[x][y]
|
||||
|
||||
end
|
||||
|
||||
|
||||
line = compress(line)
|
||||
line = merge(line)
|
||||
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
grid[x][y] = line[x]
|
||||
|
||||
if old[x] ~= line[x] then
|
||||
changed = true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return changed
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Rotate
|
||||
--------------------------------------------------
|
||||
|
||||
local function rotate()
|
||||
|
||||
local n = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
n[x] = {}
|
||||
end
|
||||
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
n[y][5 - x] = grid[x][y]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
grid = n
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Game over
|
||||
--------------------------------------------------
|
||||
|
||||
local function game_over()
|
||||
|
||||
-- Any empty cell means the game can continue.
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
if grid[x][y] == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Check horizontal and vertical merges.
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
if x < 4
|
||||
and grid[x][y] == grid[x + 1][y]
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
if y < 4
|
||||
and grid[x][y] == grid[x][y + 1]
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Board is full and no merge is possible.
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Move
|
||||
--------------------------------------------------
|
||||
|
||||
local function move(dir)
|
||||
|
||||
-- Do nothing after victory or defeat.
|
||||
if over or won then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
history:save()
|
||||
|
||||
local changed = false
|
||||
|
||||
|
||||
if dir == "left" then
|
||||
|
||||
changed = move_left()
|
||||
|
||||
|
||||
elseif dir == "right" then
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
changed = move_left()
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
|
||||
elseif dir == "up" then
|
||||
|
||||
rotate()
|
||||
|
||||
changed = move_left()
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
|
||||
elseif dir == "down" then
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
changed = move_left()
|
||||
|
||||
rotate()
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Nothing moved.
|
||||
--------------------------------------------------
|
||||
|
||||
if not changed then
|
||||
|
||||
history:revert()
|
||||
|
||||
return
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Successful move.
|
||||
-- Exactly one new tile must be created.
|
||||
--------------------------------------------------
|
||||
|
||||
local added = add_random_tile()
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- This should only happen if the board was already
|
||||
-- completely full. Normally a successful move on a
|
||||
-- full board creates space, so added should be true.
|
||||
--------------------------------------------------
|
||||
|
||||
if not added then
|
||||
|
||||
over = true
|
||||
|
||||
return
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Victory.
|
||||
--
|
||||
-- merge() sets won when a 2048 tile is created.
|
||||
--------------------------------------------------
|
||||
|
||||
if won then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- IMPORTANT:
|
||||
--
|
||||
-- Game-over is checked AFTER the new random tile
|
||||
-- has been inserted.
|
||||
--------------------------------------------------
|
||||
|
||||
if game_over() then
|
||||
over = true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Tile colors
|
||||
--------------------------------------------------
|
||||
|
||||
local function tile_color(v)
|
||||
|
||||
if v == 0 then
|
||||
return 0xCDC1B4
|
||||
|
||||
elseif v == 2 then
|
||||
return 0xEEE4DA
|
||||
|
||||
elseif v == 4 then
|
||||
return 0xEDE0C8
|
||||
|
||||
elseif v == 8 then
|
||||
return 0xF2B179
|
||||
|
||||
elseif v == 16 then
|
||||
return 0xF59563
|
||||
|
||||
elseif v == 32 then
|
||||
return 0xF67C5F
|
||||
|
||||
elseif v == 64 then
|
||||
return 0xF65E3B
|
||||
|
||||
elseif v == 128 then
|
||||
return 0xEDCF72
|
||||
|
||||
elseif v == 256 then
|
||||
return 0xEDCC61
|
||||
|
||||
elseif v == 512 then
|
||||
return 0xEDC850
|
||||
|
||||
elseif v == 1024 then
|
||||
return 0xEDC53F
|
||||
|
||||
elseif v == 2048 then
|
||||
return 0xEDC22E
|
||||
|
||||
end
|
||||
|
||||
-- Values above 2048.
|
||||
return 0x3C3A32
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Text colors
|
||||
--------------------------------------------------
|
||||
|
||||
local function text_color(v)
|
||||
|
||||
if v <= 4 then
|
||||
return 0x776E65
|
||||
else
|
||||
return 0xFFFFFF
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Text size
|
||||
--------------------------------------------------
|
||||
|
||||
local function text_size(v)
|
||||
|
||||
if v < 100 then
|
||||
return 16
|
||||
|
||||
elseif v < 1000 then
|
||||
return 14
|
||||
|
||||
elseif v < 10000 then
|
||||
return 11
|
||||
|
||||
else
|
||||
return 9
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Text positioning
|
||||
--------------------------------------------------
|
||||
|
||||
local function text_x(px, v, size)
|
||||
|
||||
local width
|
||||
|
||||
if v < 100 then
|
||||
width = size * 1.1
|
||||
|
||||
elseif v < 1000 then
|
||||
width = size * 1.7
|
||||
|
||||
elseif v < 10000 then
|
||||
width = size * 2.3
|
||||
|
||||
else
|
||||
width = size * 2.9
|
||||
end
|
||||
|
||||
|
||||
return px + math.floor(
|
||||
(CELL - width) / 2
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
local function text_y(py, size)
|
||||
|
||||
return py + math.floor(
|
||||
(CELL - size) / 2
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Score line
|
||||
--------------------------------------------------
|
||||
|
||||
local function scoreline()
|
||||
|
||||
local status = ""
|
||||
|
||||
if won then
|
||||
|
||||
status = " - tap retry"
|
||||
|
||||
elseif over then
|
||||
|
||||
status = " - tap retry"
|
||||
|
||||
end
|
||||
|
||||
|
||||
score_lbl:set(
|
||||
string.format(
|
||||
"Score %d Best %d%s",
|
||||
score,
|
||||
hiscore,
|
||||
status
|
||||
)
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Draw
|
||||
--------------------------------------------------
|
||||
|
||||
local function draw()
|
||||
|
||||
cv:fill(0x101418)
|
||||
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
local px = (x - 1) * CELL
|
||||
local py = (y - 1) * CELL
|
||||
|
||||
local v = grid[x][y]
|
||||
|
||||
|
||||
cv:rect(
|
||||
px + 2,
|
||||
py + 2,
|
||||
CELL - 4,
|
||||
CELL - 4,
|
||||
tile_color(v),
|
||||
true,
|
||||
4
|
||||
)
|
||||
|
||||
|
||||
if v > 0 then
|
||||
|
||||
local size = text_size(v)
|
||||
|
||||
cv:text(
|
||||
text_x(px, v, size),
|
||||
text_y(py, size),
|
||||
tostring(v),
|
||||
text_color(v),
|
||||
size
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- End messages
|
||||
--------------------------------------------------
|
||||
|
||||
if won then
|
||||
|
||||
cv:text(
|
||||
30,
|
||||
px_h / 2,
|
||||
"YOU WIN!",
|
||||
C.good,
|
||||
18
|
||||
)
|
||||
|
||||
elseif over then
|
||||
|
||||
cv:text(
|
||||
30,
|
||||
px_h / 2,
|
||||
"GAME OVER",
|
||||
C.bad,
|
||||
18
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- WADAMESH API
|
||||
--------------------------------------------------
|
||||
|
||||
function app.on_open(w, h)
|
||||
|
||||
hiscore = store.get(
|
||||
"2048_best",
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
px_w = CELL * 4
|
||||
px_h = CELL * 4
|
||||
|
||||
|
||||
score_lbl = ui.label(
|
||||
"",
|
||||
4,
|
||||
4,
|
||||
14,
|
||||
C.text
|
||||
)
|
||||
|
||||
|
||||
cv = ui.canvas(
|
||||
px_w,
|
||||
px_h
|
||||
)
|
||||
|
||||
|
||||
cv:pos(
|
||||
math.floor(
|
||||
(w - px_w) / 2
|
||||
),
|
||||
28
|
||||
)
|
||||
|
||||
|
||||
reset()
|
||||
|
||||
scoreline()
|
||||
draw()
|
||||
|
||||
|
||||
timer.every(100)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function app.on_input(ev)
|
||||
|
||||
if ev.type == "swipe" then
|
||||
|
||||
move(ev.dir)
|
||||
|
||||
scoreline()
|
||||
draw()
|
||||
|
||||
|
||||
elseif ev.type == "down"
|
||||
and (over or won)
|
||||
then
|
||||
|
||||
reset()
|
||||
|
||||
scoreline()
|
||||
draw()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function app.on_tick(dt)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function app.on_close()
|
||||
|
||||
end
|
||||
|
||||
|
||||
return app
|
||||
+34
-6
@@ -1,6 +1,34 @@
|
||||
{"apps":[
|
||||
{"id":"monitor","name":"RF Monitor","ver":"1.3","desc":"Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."},
|
||||
{"id":"airtime","name":"Airtime","ver":"1.3","desc":"Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."},
|
||||
{"id":"snake","name":"Snake","ver":"1.0","desc":"The classic, in Lua. Swipe to steer."},
|
||||
{"id":"sdktest","name":"SDK Test","ver":"1.1","desc":"Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, file access and its limits, keyboard input. Has a button that TRANSMITS a test message to Public (asks permission first)."}
|
||||
]}
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"id": "monitor",
|
||||
"name": "RF Monitor",
|
||||
"ver": "1.3",
|
||||
"desc": "Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."
|
||||
},
|
||||
{
|
||||
"id": "airtime",
|
||||
"name": "Airtime",
|
||||
"ver": "1.3",
|
||||
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."
|
||||
},
|
||||
{
|
||||
"id": "snake",
|
||||
"name": "Snake",
|
||||
"ver": "1.0",
|
||||
"desc": "The classic, in Lua. Swipe to steer."
|
||||
},
|
||||
{
|
||||
"id": "sdktest",
|
||||
"name": "SDK Test",
|
||||
"ver": "1.1",
|
||||
"desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, file access and its limits, keyboard input. Has a button that TRANSMITS a test message to Public (asks permission first)."
|
||||
},
|
||||
{
|
||||
"id": "2048",
|
||||
"name": "2048",
|
||||
"ver": "1.2",
|
||||
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+13
-2
@@ -44,8 +44,19 @@ missing = [f"{l['code']} v{l['ver']}" for l in langs
|
||||
if not os.path.isfile(os.path.join(apps_dir, "lang", str(l["ver"]), l["code"] + ".lang"))]
|
||||
if missing:
|
||||
sys.exit("ABORT: langs.json points at files that do not exist: " + ", ".join(missing))
|
||||
json.load(open(os.path.join(apps_dir, "apps.json"))) # parse check
|
||||
print(f"catalogs ok: {len(langs)} languages, every referenced .lang present")
|
||||
|
||||
# Same check for apps: an entry whose <id>/<ver>/<id>.lua is absent shows up in the
|
||||
# Store and then fails to install, with nothing on screen to say why.
|
||||
apps = json.load(open(os.path.join(apps_dir, "apps.json")))["apps"]
|
||||
bad = []
|
||||
for a in apps:
|
||||
base = os.path.join(apps_dir, a["id"], a["ver"])
|
||||
for f in (a["id"] + ".lua", a["id"] + ".json"):
|
||||
if not os.path.isfile(os.path.join(base, f)):
|
||||
bad.append(f'{a["id"]} v{a["ver"]}: {f}')
|
||||
if bad:
|
||||
sys.exit("ABORT: apps.json points at files that do not exist: " + ", ".join(bad))
|
||||
print(f"catalogs ok: {len(langs)} languages and {len(apps)} apps, every referenced file present")
|
||||
PY
|
||||
|
||||
# --delete mirrors the repo: the store is generated content, the repo is the source
|
||||
|
||||
Reference in New Issue
Block a user