--!name FreeCell
--!icon cards
--!category Games
-- FreeCell with snail.cards() faces. Copy to /apps/freecell.lua

local R = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"}
local S = {"S","H","D","C"}

local function red(c) local s = math.floor(c / 13); return s == 1 or s == 2 end
local function rank(c) return c % 13 end
local function suit(c) return math.floor(c / 13) end
local function face(c) return R[rank(c) + 1] .. S[suit(c) + 1] end

local tab, free, found
local hold
local cursor -- 0..3 free, 4..7 found, 8..15 tab
local won, msg

local function shuffle()
  local d = {}
  for i = 0, 51 do d[#d + 1] = i end
  for i = #d, 2, -1 do
    local j = math.random(i)
    d[i], d[j] = d[j], d[i]
  end
  return d
end

local function deal()
  local d = shuffle()
  tab = {{}, {}, {}, {}, {}, {}, {}, {}}
  for i = 1, 52 do
    local col = ((i - 1) % 8) + 1
    tab[col][#tab[col] + 1] = d[i]
  end
  free, found = {-1, -1, -1, -1}, {{}, {}, {}, {}}
  hold, cursor, won = nil, 8, false
  msg = "OK pick/drop  L/R"
end

local function can_found(c, fi)
  local p = found[fi]
  if #p == 0 then return rank(c) == 0 end
  local t = p[#p]
  return suit(c) == suit(t) and rank(c) == rank(t) + 1
end

local function can_tab(c, ti)
  local p = tab[ti]
  if #p == 0 then return true end
  local t = p[#p]
  return red(c) ~= red(t) and rank(c) == rank(t) - 1
end

local function empty_free()
  local n = 0
  for i = 1, 4 do if free[i] < 0 then n = n + 1 end end
  return n
end

local function empty_tab()
  local n = 0
  for i = 1, 8 do if #tab[i] == 0 then n = n + 1 end end
  return n
end

local function max_move()
  local f, e = empty_free(), empty_tab()
  if hold and hold.src == "t" and #tab[hold.i] == 0 then e = e - 1 end
  local m = f + 1
  for _ = 1, math.min(math.max(e, 0), 4) do m = m * 2 end
  return m
end

local function check_won()
  local n = 0
  for i = 1, 4 do n = n + #found[i] end
  won = (n == 52)
  if won then msg = "You win! OK new" end
end

local function pick()
  if cursor <= 3 then
    local fi = cursor + 1
    if free[fi] < 0 then msg = "empty free"; return end
    hold = {src = "f", i = fi, cards = {free[fi]}}
    free[fi] = -1
    msg = "hold " .. face(hold.cards[1])
    return
  end
  if cursor <= 7 then
    local fi = cursor - 3
    local p = found[fi]
    if #p == 0 then msg = "empty found"; return end
    hold = {src = "d", i = fi, cards = {table.remove(p)}}
    msg = "hold " .. face(hold.cards[1])
    return
  end
  local ti = cursor - 7
  local p = tab[ti]
  if #p == 0 then msg = "empty col"; return end
  local start = #p
  while start > 1 do
    local a, b = p[start - 1], p[start]
    if not (red(a) ~= red(b) and rank(a) == rank(b) + 1) then break end
    start = start - 1
  end
  local n = #p - start + 1
  if n > max_move() then start = #p - max_move() + 1; n = max_move() end
  local cards = {}
  for i = start, #p do cards[#cards + 1] = p[i] end
  for i = #p, start, -1 do p[i] = nil end
  hold = {src = "t", i = ti, cards = cards}
  msg = "hold " .. n
end

local function put_back()
  local c, s, i = hold.cards, hold.src, hold.i
  if s == "f" then free[i] = c[1]
  elseif s == "d" then found[i][#found[i] + 1] = c[1]
  else
    local p = tab[i]
    for j = 1, #c do p[#p + 1] = c[j] end
  end
  hold = nil
end

local function drop()
  if not hold then return end
  local cards, first = hold.cards, hold.cards[1]
  if cursor <= 3 then
    if #cards ~= 1 or free[cursor + 1] >= 0 then
      put_back(); msg = "free busy"; return
    end
    free[cursor + 1] = first
    hold = nil; msg = "free"; return
  end
  if cursor <= 7 then
    if #cards ~= 1 or not can_found(first, cursor - 3) then
      put_back(); msg = "bad found"; return
    end
    found[cursor - 3][#found[cursor - 3] + 1] = first
    hold = nil; msg = "found"; check_won(); return
  end
  local ti = cursor - 7
  if #cards > max_move() or not can_tab(first, ti) then
    put_back(); msg = "illegal"; return
  end
  local p = tab[ti]
  for j = 1, #cards do p[#p + 1] = cards[j] end
  hold = nil; msg = "ok"; check_won()
end

local function tok_free(c)
  if c == nil or c < 0 then return "-" end
  return face(c)
end

local function top_spec()
  local parts = {}
  for i = 1, 4 do parts[#parts + 1] = tok_free(free[i]) end
  parts[#parts + 1] = "-"
  for i = 1, 4 do
    local p = found[i]
    parts[#parts + 1] = (#p == 0) and "-" or face(p[#p])
  end
  return table.concat(parts, " ")
end

local function col_tops()
  local parts = {}
  for i = 1, 8 do
    local p = tab[i]
    parts[#parts + 1] = (#p == 0) and "-" or face(p[#p])
  end
  return table.concat(parts, " ")
end

local function col_spec(ti)
  local p = tab[ti]
  if #p == 0 then return "-" end
  local parts = {}
  local start = math.max(1, #p - 7)
  for i = start, #p do parts[#parts + 1] = face(p[i]) end
  return table.concat(parts, " ")
end

function start()
  local n = tonumber(snail.load()) or 1
  math.randomseed(n % 2147483646 + 1)
  snail.save(tostring(n + 1))
  snail.tick(0)
  snail.ink("fast")
  deal()
end

function key(k)
  if won then
    if k == "ok" then deal() end
    return
  end
  if k == "left" then cursor = (cursor + 15) % 16
  elseif k == "right" then cursor = (cursor + 1) % 16
  elseif k == "down" then
    if hold then put_back(); msg = "cancel" end
  elseif k == "up" then deal()
  elseif k == "ok" then
    if hold then drop() else pick() end
  end
end

function draw()
  snail.title("FreeCell")
  snail.status(msg or "")
  snail.center(true)

  if cursor <= 3 then snail.small("> FREE " .. (cursor + 1) .. "          foundations")
  elseif cursor <= 7 then snail.small("  free cells     > FOUND " .. (cursor - 3))
  else snail.small("FREE CELLS          FOUNDATIONS")
  end
  snail.cards(top_spec())

  snail.gap()
  if cursor >= 8 then
    snail.small("> COL " .. (cursor - 7))
  else
    snail.small("COLUMNS (tops)")
  end
  snail.cards(col_tops())

  if cursor >= 8 then
    snail.small("column " .. (cursor - 7))
    snail.cards(col_spec(cursor - 7))
  end

  if hold then
    snail.gap()
    snail.small("HOLD")
    local hs = {}
    for i = 1, #hold.cards do hs[#hs + 1] = face(hold.cards[i]) end
    snail.cards(table.concat(hs, " "))
  end

  snail.hint("L/R OK  D cancel  U new")
end
