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