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

Billy's Blackjack

billy.lua · 7,228 bytes + billy.art · 5,990 bytes

One deck and a dealer who talks. Double down on the first two cards; blackjack pays three to two.

Read 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 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 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.