--!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