-- Lógica pura do Sokoban: sem DOM, sem I/O, sem dependência nenhuma além da
-- biblioteca padrão do Lua. Roda igual num `lua` de linha de comando e num
-- navegador via fengari (ver web/ui.lua). Estado nunca é mutado: toda função
-- devolve uma tabela nova (ou a mesma, sem alteração, quando o movimento é
-- inválido — mesmo padrão de "decisão ignorada" dos outros jogos do
-- portfólio).

Sokoban = {}

local DIRECOES = {
  cima = { dx = 0, dy = -1 },
  baixo = { dx = 0, dy = 1 },
  esquerda = { dx = -1, dy = 0 },
  direita = { dx = 1, dy = 0 },
}

local function chave(x, y)
  return x .. "," .. y
end

local function copiarConjunto(conjunto)
  local copia = {}
  for k in pairs(conjunto) do
    copia[k] = true
  end
  return copia
end

--- Carrega um nível a partir de um texto multi-linha (notação clássica do
--- Sokoban): `#` parede, `.` alvo, `$` caixa, `*` caixa já no alvo,
--- `@` jogador, `+` jogador em cima de um alvo, espaço = chão vazio.
function Sokoban.carregarNivel(texto)
  local paredes, alvos, caixas = {}, {}, {}
  local jogador = nil
  local largura, altura = 0, 0

  local y = 0
  for linha in (texto .. "\n"):gmatch("(.-)\n") do
    y = y + 1
    if #linha > largura then largura = #linha end
    for x = 1, #linha do
      local c = linha:sub(x, x)
      local k = chave(x, y)
      if c == "#" then
        paredes[k] = true
      elseif c == "." then
        alvos[k] = true
      elseif c == "$" then
        caixas[k] = true
      elseif c == "*" then
        caixas[k] = true
        alvos[k] = true
      elseif c == "@" then
        jogador = { x = x, y = y }
      elseif c == "+" then
        jogador = { x = x, y = y }
        alvos[k] = true
      end
    end
  end
  altura = y

  assert(jogador, "nível sem jogador (@)")

  return {
    largura = largura,
    altura = altura,
    paredes = paredes,
    alvos = alvos,
    caixas = caixas,
    jogador = jogador,
    movimentos = 0,
    historico = {},
    vencido = false,
  }
end

local function todosAlvosCobertos(alvos, caixas)
  for k in pairs(alvos) do
    if not caixas[k] then
      return false
    end
  end
  return true
end

--- Move o jogador numa direção ('cima', 'baixo', 'esquerda', 'direita').
--- Empurra uma caixa se houver uma no caminho e o espaço atrás dela estiver
--- livre. Movimento inválido (parede, ou caixa bloqueada) devolve o mesmo
--- estado, sem alteração nenhuma.
function Sokoban.mover(estado, direcao)
  if estado.vencido then
    return estado
  end

  local d = DIRECOES[direcao]
  assert(d, "direção desconhecida: " .. tostring(direcao))

  local novoX, novoY = estado.jogador.x + d.dx, estado.jogador.y + d.dy
  local kDestino = chave(novoX, novoY)

  if estado.paredes[kDestino] then
    return estado
  end

  local novasCaixas = estado.caixas
  if estado.caixas[kDestino] then
    local caixaX, caixaY = novoX + d.dx, novoY + d.dy
    local kCaixaDestino = chave(caixaX, caixaY)
    if estado.paredes[kCaixaDestino] or estado.caixas[kCaixaDestino] then
      return estado -- caixa bloqueada, movimento inteiro é ignorado
    end
    novasCaixas = copiarConjunto(estado.caixas)
    novasCaixas[kDestino] = nil
    novasCaixas[kCaixaDestino] = true
  end

  local historico = {}
  for i = 1, #estado.historico do
    historico[i] = estado.historico[i]
  end
  table.insert(historico, { jogador = estado.jogador, caixas = estado.caixas, movimentos = estado.movimentos })

  local novoEstado = {
    largura = estado.largura,
    altura = estado.altura,
    paredes = estado.paredes,
    alvos = estado.alvos,
    caixas = novasCaixas,
    jogador = { x = novoX, y = novoY },
    movimentos = estado.movimentos + 1,
    historico = historico,
    vencido = false,
  }
  novoEstado.vencido = todosAlvosCobertos(novoEstado.alvos, novoEstado.caixas)

  return novoEstado
end

--- Desfaz o último movimento. Sem histórico (início do nível), devolve o
--- mesmo estado sem alteração.
function Sokoban.desfazer(estado)
  if #estado.historico == 0 then
    return estado
  end

  local historico = {}
  for i = 1, #estado.historico - 1 do
    historico[i] = estado.historico[i]
  end
  local ultimo = estado.historico[#estado.historico]

  return {
    largura = estado.largura,
    altura = estado.altura,
    paredes = estado.paredes,
    alvos = estado.alvos,
    caixas = ultimo.caixas,
    jogador = ultimo.jogador,
    movimentos = ultimo.movimentos,
    historico = historico,
    vencido = false,
  }
end

function Sokoban.venceu(estado)
  return estado.vencido
end

-- Níveis fixos do jogo, em dificuldade crescente. Todos verificados como
-- solucionáveis por um solver BFS à parte (não incluído no jogo) — as
-- soluções mais curtas encontradas estão documentadas em logica/testes.lua.
Sokoban.NIVEIS = {
  {
    nome = "Primeiro empurrão",
    texto = [[
#####
#@$.#
#####]],
  },
  {
    nome = "Duas caixas",
    texto = [[
#######
#     #
# $ $ #
#@ . .#
#     #
#######]],
  },
  {
    nome = "O desvio",
    texto = [[
########
#      #
#  $   #
# @#.  #
#  $   #
#  .   #
#      #
########]],
  },
  {
    nome = "Aperta o passo",
    texto = [[
########
#   .  #
# $ $  #
#  @   #
#.     #
#      #
########]],
  },
}
