Snail OS · Store
Every app here is a file you can read.
The Store app on the device installs these with one press. Or download a file, drop it in /apps on the SD card, and the launcher lists it. The full source of each is below.
Each of these is one .lua file, read by an interpreter compiled into the firmware. It cannot write to the card and it never runs on the CPU. An app may carry one more file beside it — an .art sprite pack — and the Store downloads the pair or neither.
The device fetches the same index.json this page is built from, so the two cannot drift. To write your own, start with the developer documentation.
Connected
Four apps are built into Snail OS. One pairing with an eight-character code covers them all, and every screen opens from the SD card before the radio is touched. RIGHT asks a-gnt.com again.
GitHub
Your repositories, their directories, and a file set as a page.
Analytics
Users, views, top pages and where the readers came from, for today, 7 days or 28.
Search Console
Clicks, impressions, CTR and average position, plus the top queries and pages.
The Calendar reads the same account: its month grid opens a day, and a day opens an event. Sync for the connected apps requires Snail Connect, $2.99 a month. Snail OS buyers get six months free.
Coming soon: Google Drive.
Games
One deck and a dealer who talks. Double down on the first two cards; blackjack pays three to two.
Read the sourceHide the source
--!name Billy's Blackjack
--!icon game
local BANK_START = 500
local MIN_BET, MAX_BET, BET_STEP = 5, 500, 5
local phase, deck, you, billy = "bet", {}, {}, {}
local bank, bet, message, outcome = BANK_START, 25, "", ""
local doubled, wins, losses, pushes = false, 0, 0, 0
local net = 0 -- what the last hand moved, for the settled screen
local RANKS = {
{"A", 11}, {"2", 2}, {"3", 3}, {"4", 4}, {"5", 5}, {"6", 6}, {"7", 7},
{"8", 8}, {"9", 9}, {"10", 10}, {"J", 10}, {"Q", 10}, {"K", 10},
}
local SUITS = {"S", "H", "D", "C"}
local function shuffle()
deck = {}
for s = 1, 4 do
for r = 1, 13 do
deck[#deck + 1] = {face = RANKS[r][1] .. SUITS[s], v = RANKS[r][2], rank = r}
end
end
for i = #deck, 2, -1 do
local j = math.random(i)
deck[i], deck[j] = deck[j], deck[i]
end
end
local function draw_card(hand)
if #deck == 0 then shuffle() end
hand[#hand + 1] = table.remove(deck)
end
local function total(hand)
local t, aces = 0, 0
for _, c in ipairs(hand) do
t = t + c.v
if c.rank == 1 then aces = aces + 1 end
end
while t > 21 and aces > 0 do t, aces = t - 10, aces - 1 end
return t
end
local function soft(hand)
local t, aces = 0, 0
for _, c in ipairs(hand) do
t = t + c.v
if c.rank == 1 then aces = aces + 1 end
end
while t > 21 and aces > 0 do t, aces = t - 10, aces - 1 end
return aces > 0
end
local function is_blackjack(hand)
return #hand == 2 and total(hand) == 21
end
local function hand_spec(hand, hide)
local out = {}
for i, c in ipairs(hand) do
out[i] = (hide and i == 2) and "?" or c.face
end
return table.concat(out, " ")
end
local function save()
snail.save(string.format("%d %d %d %d", bank, wins, losses, pushes))
end
local function load()
local s = snail.load()
if not s then return end
local b, w, l, p = s:match("(%-?%d+) (%d+) (%d+) (%d+)")
if b then
bank, wins, losses, pushes = tonumber(b), tonumber(w), tonumber(l), tonumber(p)
end
if bank < MIN_BET then bank = BANK_START end
end
local function clamp_bet()
bet = math.max(MIN_BET, math.min(bet, MAX_BET, bank))
end
local function billy_says(kind)
local lines = {
deal = {"Place it and we'll see.", "Cards are cards.", "Let's have a look."},
hit = {"Bold.", "Another, then.", "You want it, you got it."},
close = {"That's a working hand.", "I'd sit on that.", "Now we're talking."},
bust = {"Over. Happens.", "Twenty-two is still twenty-two.",
"The house thanks you."},
bbust = {"Over. Yours.", "I've overdone it.", "The deck turned on me."},
dealbj = {"Blackjack. Don't look at me like that.",
"Twenty-one on the deal. That's the game."},
yourbj = {"Blackjack. Pays three to two.", "Well. Look at that."},
win = {"You take it.", "Fair and square.", "Good hand."},
lose = {"Mine.", "That's the house.", "Better luck next one."},
push = {"Nobody's hand.", "Push. Try again.", "A tie is a rest."},
broke = {"That's the bankroll. I'll spot you a fresh one.",
"Cleaned out. Sit, I'll deal you back in."},
}
local set = lines[kind] or lines.deal
return set[math.random(#set)]
end
local function settle()
local yt, bt = total(you), total(billy)
local ybj, bbj = is_blackjack(you), is_blackjack(billy)
local stake = doubled and bet * 2 or bet
if yt > 21 then
outcome, net, losses = "BUST", -stake, losses + 1
message = billy_says("bust")
elseif ybj and not bbj then
outcome, net, wins = "BLACKJACK", math.floor(stake * 3 / 2), wins + 1
message = billy_says("yourbj")
elseif bbj and not ybj then
outcome, net, losses = "BILLY HAS IT", -stake, losses + 1
message = billy_says("dealbj")
elseif bt > 21 then
outcome, net, wins = "BILLY BUSTS", stake, wins + 1
message = billy_says("bbust")
elseif yt > bt then
outcome, net, wins = "YOU WIN", stake, wins + 1
message = billy_says("win")
elseif bt > yt then
outcome, net, losses = "BILLY WINS", -stake, losses + 1
message = billy_says("lose")
else
outcome, net, pushes = "PUSH", 0, pushes + 1
message = billy_says("push")
end
bank = bank + net
if bank < MIN_BET then
bank = BANK_START
message = billy_says("broke")
end
clamp_bet()
phase = "over"
snail.hint("OK deal again BACK menu")
save()
end
local function dealer_plays()
while total(billy) < 17 do draw_card(billy) end
settle()
end
local function play_hint()
snail.hint(#you == 2 and bank >= bet * 2
and "OK hit DOWN stand LEFT double BACK menu"
or "OK hit DOWN stand BACK menu")
end
local function deal()
shuffle()
you, billy, doubled = {}, {}, false
draw_card(you); draw_card(billy); draw_card(you); draw_card(billy)
phase = "play"
message = billy_says("deal")
if is_blackjack(you) or is_blackjack(billy) then settle() end
end
function start()
snail.ink("fast") -- solid tiles, so the short waveform is honest here
load()
clamp_bet()
phase = "bet"
message = "Sit down."
snail.hint("UP/DOWN bet OK deal BACK menu")
end
function key(k)
if phase == "bet" then
if k == "up" then bet = math.min(math.min(MAX_BET, bank), bet + BET_STEP)
elseif k == "down" then bet = math.max(MIN_BET, bet - BET_STEP)
elseif k == "left" then bet = MIN_BET
elseif k == "right" then bet = math.min(MAX_BET, bank)
elseif k == "ok" then
deal()
if phase == "play" then play_hint() end
end
elseif phase == "play" then
if k == "ok" then
draw_card(you)
local t = total(you)
if t > 21 then
settle()
elseif t == 21 then
dealer_plays()
else
message = billy_says(t >= 17 and "close" or "hit")
play_hint() -- double left the table with the third card
end
elseif k == "down" then
dealer_plays()
elseif k == "left" and #you == 2 and bank >= bet * 2 then
doubled = true
draw_card(you)
if total(you) > 21 then settle() else dealer_plays() end
end
else -- over
if k == "ok" then
phase = "bet"
clamp_bet()
message = "Again?"
snail.hint("UP/DOWN bet OK deal BACK menu")
end
end
end
function draw()
if phase == "bet" then
snail.center(true)
snail.image("billy")
snail.title("$" .. bank)
snail.small(string.format("%d won %d lost %d pushed", wins, losses, pushes))
snail.gap()
snail.title("BET $" .. bet)
snail.gap()
snail.text(message)
return
end
snail.center(true)
snail.small("BILLY")
snail.cards(hand_spec(billy, phase == "play"))
snail.small(phase == "play"
and ("showing " .. billy[1].v)
or ("total " .. total(billy)))
snail.gap()
snail.small("YOU" .. (doubled and " (doubled)" or ""))
snail.cards(hand_spec(you, false))
snail.small(string.format("total %d%s", total(you),
soft(you) and " soft" or ""))
snail.gap()
if phase == "over" then
snail.title(outcome .. (net > 0 and (" +$" .. net)
or net < 0 and (" -$" .. -net) or ""))
snail.text(message)
snail.gap()
snail.small("bank $" .. bank)
else
snail.text(message)
end
end
Snake
snake.lua · 6,660 bytes
The snake crawls on its own and the arrows steer. Eat, grow, and keep off the walls and your tail.
Read the sourceHide the source
--!name Snake
--!icon game
local N = 17 -- board is N x N cells
local CELL_EMPTY = "." -- ground, drawn as a dot so an empty board
local CELL_BODY = "#" -- still reads as a field rather than a void
local CELL_HEAD = "@" -- a block with the centre punched out
local CELL_FOOD = "*" -- a disc: food has its own silhouette
local START_LEN = 3
local FOOD_SCORE = 10
local PACE_MAX = 600
local PACE_MIN = 450
local ALPHA = "0123456789abcdefg"
local DX = {0, 0, -1, 1}
local DY = {-1, 1, 0, 0}
local OPPOSITE = {2, 1, 4, 3}
local DIR_OF_KEY = {up = 1, down = 2, left = 3, right = 4}
local DIR_NAME = {"NORTH", "SOUTH", "WEST", "EAST"}
local body, dir, food = {}, 4, 0
local q, qn = {}, 0
local scratch = {}
local function spec_index(cell)
return (cell // N) * (N + 1) + (cell % N) + 1
end
local score, best, alive, note = 0, 0, true, ""
local hi = false
local running = false
local function place_food()
local occ = scratch
for i = 1, N * N do occ[i] = false end
for i = 1, #body do occ[body[i] + 1] = true end
local free = 0
for i = 1, N * N do if not occ[i] then free = free + 1 end end
if free == 0 then
alive, note = false, "PERFECT BOARD"
return
end
local pick, seen = math.random(free), 0
for c = 0, N * N - 1 do
if not occ[c + 1] then
seen = seen + 1
if seen == pick then food = c return end
end
end
end
local function new_game()
body = {}
local cy = N // 2
for i = 1, START_LEN do
body[i] = cy * N + (N // 2) - i + 1
end
dir, qn, score, alive, note = 4, 0, 0, true, "GO"
hi = false
running = false
place_food()
end
local function encode(from, to)
local buf, n = scratch, 0
for i = from, to do
local x, y = body[i] % N, body[i] // N
n = n + 1 buf[n] = ALPHA:sub(x + 1, x + 1)
n = n + 1 buf[n] = ALPHA:sub(y + 1, y + 1)
end
return table.concat(buf, "", 1, n)
end
local function save()
local half = #body // 2
snail.save(string.format("%d %d %d %d ;%d %d %d %s",
best, score, #body, alive and 1 or 0,
dir, food % N, food // N, encode(1, half) .. encode(half + 1, #body)))
end
local function load()
local s = snail.load()
if not s then return end
local b, sc, ln, al, d, fx, fy, segs =
s:match("^(%d+) (%d+) (%d+) (%d+) ;(%d+) (%d+) (%d+) (%w*)$")
if not b then return end
best = tonumber(b)
ln = tonumber(ln)
if #segs ~= ln * 2 or ln < 1 then return end
local rebuilt, occ = {}, scratch
for i = 1, N * N do occ[i] = false end
local px, py
for i = 1, ln do
local x = ALPHA:find(segs:sub(i * 2 - 1, i * 2 - 1), 1, true)
local y = ALPHA:find(segs:sub(i * 2, i * 2), 1, true)
if not x or not y then return end
local c = (y - 1) * N + (x - 1)
if occ[c + 1] then return end
occ[c + 1] = true
if i > 1 and math.abs(x - px) + math.abs(y - py) ~= 1 then return end
px, py = x, y
rebuilt[i] = c
end
body, score, alive = rebuilt, tonumber(sc), tonumber(al) == 1
if score > best then best = score end
dir = tonumber(d)
if dir < 1 or dir > 4 then dir = 4 end
local fx2, fy2 = tonumber(fx), tonumber(fy)
if fx2 >= N or fy2 >= N then
place_food()
else
food = fy2 * N + fx2
if occ[food + 1] then place_food() end
end
qn = 0
if #body > 1 then
local gx = body[1] % N - body[2] % N
local gy = body[1] // N - body[2] // N
dir = (gy < 0 and 1) or (gy > 0 and 2) or (gx < 0 and 3) or 4
end
note = alive and "RESUMED" or "GAME OVER"
end
local function die(why)
alive, note = false, why
if score > best then best, hi = score, true end
end
local function queue_turn(turn)
local ref = qn > 0 and q[qn] or dir
if turn == ref or turn == OPPOSITE[ref] then return end
if qn >= 2 then return end
qn = qn + 1
q[qn] = turn
end
local function step()
if not alive then return end
if qn > 0 then -- pop one queued turn per step
dir = q[1]
q[1] = q[2]
qn = qn - 1
end
local head = body[1]
local nx, ny = head % N + DX[dir], head // N + DY[dir]
if nx < 0 or nx >= N or ny < 0 or ny >= N then
die("HIT THE WALL")
return
end
local cell = ny * N + nx
local grow = (cell == food)
local last = grow and #body or #body - 1
for i = 1, last do
if body[i] == cell then
die("ATE ITSELF")
return
end
end
table.insert(body, 1, cell)
if grow then
score = score + FOOD_SCORE
if score > best then best, hi = score, true end
place_food()
else
body[#body] = nil
end
end
local function set_hint()
if not alive then
snail.hint("OK plays again BACK menu")
elseif running then
snail.hint("ARROWS steer OK pause 2x SIDE new BACK menu")
else
snail.hint("ARROWS start and steer OK starts BACK menu")
end
end
local function set_pace()
if alive and running then
local pace = PACE_MAX - 10 * ((#body - START_LEN))
if pace < PACE_MIN then pace = PACE_MIN end
snail.tick(pace)
else
snail.tick(0)
end
end
function start()
snail.ink("fast") -- solid tiles, so the short waveform is honest here
new_game()
load()
set_hint()
set_pace()
end
function key(k)
if k == "top" then
new_game()
save()
elseif not alive then
if k == "ok" then
new_game()
save()
end
elseif k == "ok" then
running = not running
qn = 0
else
local turn = DIR_OF_KEY[k]
if turn then queue_turn(turn) end
running = true
end
set_hint()
set_pace()
end
function tick()
local before = score
step()
if not alive then running = false end
set_hint()
set_pace()
if not alive or score ~= before then save() end
end
local function board_spec()
local buf, n = scratch, 0
for y = 0, N - 1 do
if y > 0 then n = n + 1 buf[n] = "/" end
for x = 0, N - 1 do n = n + 1 buf[n] = CELL_EMPTY end
end
buf[spec_index(food)] = CELL_FOOD
for i = 1, #body do buf[spec_index(body[i])] = CELL_BODY end
buf[spec_index(body[1])] = CELL_HEAD
return table.concat(buf, "", 1, n)
end
function draw()
snail.center(true)
if not alive then
snail.title("GAME OVER")
snail.title(string.format("%d", score))
if hi then
snail.small("NEW BEST")
else
snail.small(string.format("BEST %d", best))
end
snail.gap()
snail.board(board_spec(), nil, note)
return
end
snail.title("SNAKE")
snail.small(string.format("SCORE %d BEST %d LEN %d", score, best, #body))
if running then
snail.small("HEADING " .. DIR_NAME[qn > 0 and q[qn] or dir])
else
snail.small("STOPPED -- FACING " .. DIR_NAME[dir])
end
snail.gap()
snail.board(board_spec())
end
Cascade
cascade.lua · 8,016 bytes
Seven tetrominoes, line clears and a next-piece preview. Gravity takes a row every three seconds.
Read the sourceHide the source
--!name Cascade
--!icon game
local COLS, ROWS = 10, 16
local CELL_EMPTY = "." -- a lattice, so an empty well still has depth
local CELL_LOCKED = "#"
local CELL_LIVE = "@" -- the piece you are still holding
local LINE_SCORE = {100, 300, 500, 800}
local GRAVITY = 2000 -- milliseconds per row of fall
local PIECES = {
{name = "I", n = 4, c = {{0,1},{1,1},{2,1},{3,1}}},
{name = "O", n = 2, c = {{0,0},{1,0},{0,1},{1,1}}},
{name = "T", n = 3, c = {{1,0},{0,1},{1,1},{2,1}}},
{name = "S", n = 3, c = {{1,0},{2,0},{0,1},{1,1}}},
{name = "Z", n = 3, c = {{0,0},{1,0},{1,1},{2,1}}},
{name = "J", n = 3, c = {{0,0},{0,1},{1,1},{2,1}}},
{name = "L", n = 3, c = {{2,0},{0,1},{1,1},{2,1}}},
}
local well = {} -- well[y][x], 0 empty, 1..7 a locked piece
local kind, rot, px, py = 1, 0, 3, 0
local nextkind = 1
local score, lines, best, dead, note = 0, 0, 0, false, ""
local falling = false
local ROT = {}
for k = 1, #PIECES do
local p = PIECES[k]
ROT[k] = {}
for r = 0, 3 do
local out = {}
for i = 1, 4 do
local x, y = p.c[i][1], p.c[i][2]
for _ = 1, r do x, y = p.n - 1 - y, x end
out[i * 2 - 1], out[i * 2] = x, y
end
ROT[k][r] = out
end
end
local function cells_of(k, r) return ROT[k][r % 4] end
local wellgen, gkey, ggy = 0, -1, 0
local function fits(k, r, ox, oy)
local c = cells_of(k, r)
for i = 1, 8, 2 do
local x, y = ox + c[i], oy + c[i + 1]
if x < 0 or x >= COLS or y >= ROWS then return false end
if y >= 0 and well[y][x] ~= 0 then return false end
end
return true
end
local function ghost_y()
local k = (((wellgen * 8 + kind) * 4 + rot % 4) * 16 + px + 2) * 32 + py
if k ~= gkey then
local gy = py
while fits(kind, rot, px, gy + 1) do gy = gy + 1 end
gkey, ggy = k, gy
end
return ggy
end
local rowspec, rowdig = {}, {}
local function refresh_row(y)
local rc, rd = {}, {}
for x = 0, COLS - 1 do
local v = well[y][x]
rc[x + 1] = v ~= 0 and CELL_LOCKED or CELL_EMPTY
rd[x + 1] = string.char(48 + v)
end
rowspec[y] = table.concat(rc)
rowdig[y] = table.concat(rd)
end
local function refresh_rows()
for y = 0, ROWS - 1 do refresh_row(y) end
end
local function clear_well()
for y = 0, ROWS - 1 do
well[y] = {}
for x = 0, COLS - 1 do well[y][x] = 0 end
end
end
local function spawn()
kind, rot, px, py = nextkind, 0, 3, 0
nextkind = math.random(#PIECES)
if not fits(kind, rot, px, py) then
dead = true
if score > best then best = score end
end
end
local function clear_lines()
local kept, n = {}, 0
for y = ROWS - 1, 0, -1 do
local full = true
for x = 0, COLS - 1 do
if well[y][x] == 0 then full = false break end
end
if not full then
n = n + 1
kept[n] = well[y]
end
end
local cleared = ROWS - n
if cleared == 0 then return 0 end
clear_well()
for i = 1, n do well[ROWS - i] = kept[i] end
lines = lines + cleared
score = score + LINE_SCORE[cleared]
return cleared
end
local function say_state()
if dead then
note = (score > 0 and score >= best) and "NEW BEST" or "GAME OVER"
else note = "IN HAND: " .. PIECES[kind].name end
end
local function lock()
wellgen = wellgen + 1
local c = cells_of(kind, rot)
for i = 1, 8, 2 do
local x, y = px + c[i], py + c[i + 1]
if y >= 0 then well[y][x] = kind end
end
local cleared = clear_lines()
if cleared > 0 then
refresh_rows()
else
for i = 1, 8, 2 do
local y = py + c[i + 1]
if y >= 0 then refresh_row(y) end
end
end
if score > best then best = score end
spawn()
say_state()
if cleared > 0 and not dead then
note = string.format("%s +%d", cleared == 4 and "CASCADE!"
or (cleared .. " LINE" .. (cleared > 1 and "S" or "")), LINE_SCORE[cleared])
end
end
local function new_game()
wellgen = wellgen + 1
clear_well()
refresh_rows()
score, lines, dead, falling = 0, 0, false, false
nextkind = math.random(#PIECES)
spawn()
say_state()
end
local function save()
local rows = {}
for y = 0, ROWS - 1 do rows[y + 1] = rowdig[y] end
snail.save(string.format("%d %d %d %d ;%d %d %d %d %d %s",
best, score, lines, dead and 1 or 0,
kind, rot, px, py, nextkind, table.concat(rows)))
end
local function load()
local s = snail.load()
if not s then return end
local b, sc, ln, dd, k, r, x, y, nk, grid =
s:match("^(%d+) (%d+) (%d+) (%d+) ;(%d+) (%d+) (%-?%d+) (%-?%d+) (%d+) (%d*)$")
if not b then return end
best = tonumber(b)
if #grid ~= ROWS * COLS then return end
for gy = 0, ROWS - 1 do
for gx = 0, COLS - 1 do
local v = tonumber(grid:sub(gy * COLS + gx + 1, gy * COLS + gx + 1))
well[gy][gx] = (v and v >= 0 and v <= #PIECES) and v or 0
end
end
score, lines, dead = tonumber(sc), tonumber(ln), tonumber(dd) == 1
kind, rot, px, py = tonumber(k), tonumber(r), tonumber(x), tonumber(y)
nextkind = tonumber(nk)
if kind < 1 or kind > #PIECES then kind = 1 end
if nextkind < 1 or nextkind > #PIECES then nextkind = 1 end
if not fits(kind, rot, px, py) then
rot, px, py = 0, 3, 0
if not fits(kind, rot, px, py) then dead = true end
end
wellgen = wellgen + 1
refresh_rows()
say_state()
end
local function set_hint()
if dead then
snail.hint("OK plays again BACK menu")
else
snail.hint("L/R move UP turn DOWN step OK drop 2x SIDE new")
end
end
local function set_pace()
snail.tick((not dead and falling) and GRAVITY or 0)
end
function start()
snail.ink("fast") -- solid tiles, so the short waveform is honest here
new_game()
load()
set_hint()
set_pace()
end
function key(k)
local restart = k == "top" or (dead and k == "ok")
if restart then
new_game()
elseif dead then
elseif k == "left" then
if fits(kind, rot, px - 1, py) then px = px - 1 end
elseif k == "right" then
if fits(kind, rot, px + 1, py) then px = px + 1 end
elseif k == "up" then
local r = (rot + 1) % 4
if fits(kind, r, px, py) then rot = r
elseif fits(kind, r, px - 1, py) then rot, px = r, px - 1
elseif fits(kind, r, px + 1, py) then rot, px = r, px + 1 end
elseif k == "down" then
if fits(kind, rot, px, py + 1) then
py, score = py + 1, score + 1
else
lock()
end
elseif k == "ok" then
local gy = ghost_y()
score = score + 2 * (gy - py)
py = gy
lock()
end
if not dead and not restart then falling = true end
if score > best then best = score end
set_hint()
set_pace()
save()
end
function tick()
if not dead and fits(kind, rot, px, py + 1) then
py = py + 1
elseif not dead then
lock()
save()
end
if dead then falling = false end
set_hint()
set_pace()
end
local TRAYS = {}
local function tray_spec()
local s = TRAYS[nextkind]
if s then return s end
local c = cells_of(nextkind, 0)
local hit = {}
for i = 1, 8, 2 do hit[c[i + 1] * 4 + c[i]] = true end
local out, n = {}, 0
for y = 0, 1 do
if y > 0 then n = n + 1 out[n] = "/" end
for x = 0, 3 do
n = n + 1
out[n] = hit[y * 4 + x] and CELL_LOCKED or CELL_EMPTY
end
end
s = table.concat(out)
TRAYS[nextkind] = s
return s
end
local function well_spec()
local out = {}
for y = 1, ROWS do out[y] = rowspec[y - 1] end
if not dead then
local c = cells_of(kind, rot)
local gy = ghost_y()
local function put(x, y, ch)
local r = out[y + 1]
out[y + 1] = r:sub(1, x) .. ch .. r:sub(x + 2)
end
if gy > py then
for i = 1, 8, 2 do put(px + c[i], gy + c[i + 1], "o") end
end
for i = 1, 8, 2 do put(px + c[i], py + c[i + 1], CELL_LIVE) end
end
return table.concat(out, "/")
end
function draw()
snail.center(true)
snail.small("SCORE")
snail.title(tostring(score))
snail.small(string.format("LINES %d BEST %d", lines, best))
snail.small(note)
snail.small("NEXT")
snail.board(tray_spec(), "small")
snail.board(well_spec(), nil, dead and ("GAME OVER " .. score) or nil)
end
Coming soon
Stocks
A watchlist you edit on the device, then one symbol a screen: price, day range, volume, averages and RSI.
Publishing
Wrote something worth sharing? Send the file — reply to your receipt or use the address on the main page. Everything published here is readable in full before anyone installs it. A card app cannot write to the card and does not run on the CPU, so reading the file is the whole audit.