Client for playing 300 publicly available Sokoban puzzles on a computer or phone.
-- some commonly modified settings
dark_theme = true
curr_level = 1
-- levels available:
-- 1-59: Sokoban Jr 1
-- 60-106: Sokoban Jr 2
-- 107-149: Sasquatch
-- 150-187: Sasquatch II
-- 188-226: Sasquatch III
-- 227-267: Microcosmos
-- 268-300: Nabocosmos
-- 301-454: Microban I
-- 455-588: Microban II
-- 589-688: Microban III
-- 689-789: Microban IV

ui_state = {}  -- for buttons; recreated each frame

-- level dimensions in cells
lw, lh = nil, nil
-- each cell is a 6x6 square (or multiple thereof)
num_tile_px = 6

-- data structures for current level
level_state = nil  -- 2D array of lh*lw sprite ids
crate_id = nil  -- 2D array of lh*lw crate ids; only a debugging aid
player = nil  -- {x=,y=} coordinate of player in current level
undo_history = {} -- an array of undo states, each an array of {x=,y=,cell=} square states.
pending_moves = {}  -- moves already made, but need to be animated
next_pending_move = nil  -- timestamp for next frame of animation
crate_to_move = nil

-- sprite ids
CELL_PLAYER = 0
CELL_PLAYER_ON_TARGET = 1
CELL_CRATE = 2
CELL_CRATE_ON_TARGET = 3
CELL_TARGET = 4
CELL_WALL = 5
CELL_GRASS = 6  -- vestigial?
CELL_VACANT = 7

function car.load()
  love.keyboard.setTextInput(false)
  level_state = load_level(levels[curr_level])
  player = player_state(level_state)
  crate_id = load_crate_id(level_state)
  -- some constants for draw_level_number
  if dark_theme then
    level_color = {0.8,0.8,0.8}
  else
    level_color = {0,0,0}
  end
  level_width = App.width('MMM')+10
end

function load_level(level)
  local result = {}
  for _,row in ipairs(level) do
    local dest = {}
    for _,pair in ipairs(row) do
      table.insert(dest, floor(pair/16))
      table.insert(dest, pair%16)
    end
    table.insert(result, dest)
  end
  lw = math.max(#level[1]*2, 8)
  lh = math.max(#level, 6)
  car.resize()
  return result
end

function car.mouse_press(x,y, b)
  if #pending_moves > 0 then
    make_all_pending_moves()
    return
  end
  if mouse_press_consumed_by_any_button(ui_state, x,y, b) then
    crate_to_move = nil
    return
  end
  if x > left and x < left+lw*side and y > top and y < top+lh*side then
    local y, x = 1+floor((y-top)/side), 1+floor((x-left)/side)
    if crate_to_move == nil and (level_state[y][x] == CELL_VACANT or level_state[y][x] == CELL_TARGET) then
      plan_move_to_empty_space(y, x)
    elseif level_state[y][x] == CELL_CRATE or level_state[y][x] == CELL_CRATE_ON_TARGET then
      assert(crate_id[y][x])
      crate_to_move = {x=x, y=y, id=crate_id[y][x]}
    elseif crate_to_move and level_state[y][x] ~= CELL_WALL and level_state[y][x] ~= CELL_CRATE and level_state[y][x] ~= CELL_CRATE_ON_TARGET then
      plan_move_crate(y, x)
      crate_to_move = nil
    end
  end
end

function car.keychord_press(chord)
  if chord == 'f1' then
    stop_app()
  elseif chord == 'left' or chord == 'right' or chord == 'up' or chord == 'down' then
    move(chord, --[[add to undo]] true)
  elseif chord == 'C-z' then
    undo_move()
  elseif chord == 'C-right' then
    next_level()
  elseif chord == 'C-left' then
    previous_level()
  end
end

function move(dir, add_to_undo)
  if dir == 'left' then
    move_left(add_to_undo)
    crate_to_move = nil
  elseif dir == 'right' then
    move_right(add_to_undo)
    crate_to_move = nil
  elseif dir == 'up' then
    move_up(add_to_undo)
    crate_to_move = nil
  elseif dir == 'down' then
    move_down(add_to_undo)
    crate_to_move = nil
  end
end

function next_level()
  if curr_level >= #levels then return end
  curr_level = curr_level+1
  level_state = load_level(levels[curr_level])
  player = player_state(level_state)
  crate_id = load_crate_id(level_state)
  undo_history = {}
end

function previous_level()
  if curr_level <= 1 then return end
  curr_level = curr_level-1
  level_state = load_level(levels[curr_level])
  player = player_state(level_state)
  crate_id = load_crate_id(level_state)
  undo_history = {}
end

function player_state(level_state)
  for r,row in ipairs(level_state) do
    for c,cell in ipairs(row) do
      if cell == CELL_PLAYER or cell == CELL_PLAYER_ON_TARGET then
        return {x=c, y=r}
      end end end end

function load_crate_id(level_state)
  local next_crate_id = 1
  local result = {}
  for r,row in ipairs(level_state) do
    local dest = {}
    for c,cell in ipairs(row) do
      if cell == CELL_CRATE or cell == CELL_CRATE_ON_TARGET then
        table.insert(dest, next_crate_id)
        next_crate_id = next_crate_id+1
      else
        table.insert(dest, false)
      end
    end
    table.insert(result, dest)
  end
  return result
end