AHABKD5VEK5RSTM3CME4XJAHCVTHYV2D2WAWUGSJ6PBUCUI7CB3AC
CQHZPQEOQ3W3TPJTJWOZGA3P5AYOZJO67TKNQCPL5FKGHLRJP3AAC
PHCL6PQZG5UGDFSXGSG3LVOEQHQWOQQMH2DWBVFN5O3DQ7YK3IWAC
YK4G5INACWFTBIEAFL4DVMRYG3MGO7Y5VDT2FVCN7OW3YTHTLC4QC
G335UZH5POREKDAOCCRJZYAGY7F4LO7VQ476GF6OWKYUO2GEKXGAC
SJGHLCLGIENGSEL3H4EVSW7VJT2WJRGJNIR3BURZJR5RCE2T3YWAC
LUYWR2DLKU5HA5LEY5ZW6BKL5WEWJREBN5WL4C7XPYECD7LUP3VQC
MUADNESBQPELEGUUCGMPUZKVQZPUSSNALDW3MWCBJ42MRFWMQF7QC
GVX7YSQYURPWFSUWVUAORZJTQBJURWWNBNUGEZYFAUMX3X5LSACQC
YYVOTWROJFENEAQ4M4VJPWLODTTX7BTAL3NQSTL2UKJ5VSOEH4JQC
KCUJIKQ6POLGKI26YHZWTD242B7JOSGI4VO4XBSKAU66ARM3EIBQC
LYLOM4SPCNQX64J76E7WDGCXPJOJ2GTSLJCDREJJFZY46HGMCR3QC
FKENDSMEJXEZPAT6K5TYJC2KZBVYKMXA7LN7ZVPZMYIBODWIB64AC
VZTOJS52BORPI34JWUWQFQEENTMZDLQHCCNZMFBQ6UWFLHE6X5GAC
4IBBQBYGBZDCXQO5C7H6UTWT67YJUJ7R6FH6UNHX4QGXOO47UUBQC
TEOLZ25FHVXJ3DF3LRXEBUIAH76FUHA24U7F3FZFKZVIQBSF3VZAC
WMYD2MLJHHONHGA2M2K7G5DYZ47A7ZEZ7M3CJAMLFQK7GRG4UE3AC
RRYPAAZARIX5ZIF5PDYPUDJP3VFMNYXIEGTUK4XCTZKIJRDPOD5AC
TWOCOTZPRAI2JQH6QY2ZBUZGYA53G7MCOSO64LBIO7OCVEHK7AGQC
IMDZUX5WFBYCKZPYGVTSJPONI3ORUDNALX5A2BMUFK3JOZ6DHJWQC
TRZPDEYAE6S3FP7NKTNKQLECPVV2LRKGC7PJCC2MHPXDLU7CAABQC
73FZRRIBGT2F6MK4C2JIK4M4BXVKLF5YWRWEERBB7BZ7X4XZCWTQC
KWMGPU6WTJORKCZAVAFGBM2C5X7NDWR44HHJOZDVLUOBOVULSB2QC
-- game logic
-- sprites
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 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
return result
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 draw_level_state()
for r,row in ipairs(level_state) do
for c,cell in ipairs(row) do
draw_sprite(cell, left+(c-1)*side, top+(r-1)*side)
end end end
function draw_sprite(cell, x,y)
if cell >= CELL_VACANT then
color(0,0,0)
rect('fill', x,y, side,side)
return
end
local sprite = sprites[cell+1]
for r,row in ipairs(sprite) do
for c,clr in ipairs(row) do
color(unpack(clr))
rect('fill', x+(c-1)*pxside, y+(r-1)*pxside, pxside, pxside)
end end end
function move_down()
if player.y >= #level_state then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y+1][player.x]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x, y=player.y+1, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.y >= #level_state-1 then return end
local dest2 = level_state[player.y+2][player.x]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x, y=player.y+2, cell=dest2})
move_crate_to(player.y+2, player.x)
remove_crate_from(player.y+1, player.x)
end
move_player_to(player.y+1, player.x)
remove_player_from(player.y, player.x)
player.y = player.y+1
table.insert(undo_history, u)
end
function move_up()
if player.y <= 1 then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y-1][player.x]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x, y=player.y-1, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.y <= 2 then return end
local dest2 = level_state[player.y-2][player.x]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x, y=player.y-2, cell=dest2})
move_crate_to(player.y-2, player.x)
remove_crate_from(player.y-1, player.x)
end
move_player_to(player.y-1, player.x)
remove_player_from(player.y, player.x)
player.y = player.y-1
table.insert(undo_history, u)
end
function move_left()
if player.x <= 1 then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y][player.x-1]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x-1, y=player.y, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.x <= 2 then return end
local dest2 = level_state[player.y][player.x-2]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x-2, y=player.y, cell=dest2})
move_crate_to(player.y, player.x-2)
remove_crate_from(player.y, player.x-1)
end
move_player_to(player.y, player.x-1)
remove_player_from(player.y, player.x)
player.x = player.x-1
table.insert(undo_history, u)
end
function move_right()
if player.x >= #level_state[1] then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y][player.x+1]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x+1, y=player.y, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.x >= #level_state[1]-1 then return end
local dest2 = level_state[player.y][player.x+2]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x+2, y=player.y, cell=dest2})
move_crate_to(player.y, player.x+2)
remove_crate_from(player.y, player.x+1)
end
move_player_to(player.y, player.x+1)
remove_player_from(player.y, player.x)
player.x = player.x+1
table.insert(undo_history, u)
end
function move_player_to(y, x)
local old = level_state[y][x]
if old == CELL_VACANT then
level_state[y][x] = CELL_PLAYER
elseif old == CELL_TARGET then
level_state[y][x] = CELL_PLAYER_ON_TARGET
else
error('cannot move player to '..old)
end
end
function remove_player_from(y, x)
local src = level_state[y][x]
if src == CELL_PLAYER then
level_state[y][x] = CELL_VACANT
elseif src == CELL_PLAYER_ON_TARGET then
level_state[y][x] = CELL_TARGET
end
end
function move_crate_to(y, x)
local old = level_state[y][x]
if old == CELL_VACANT then
level_state[y][x] = CELL_CRATE
elseif old == CELL_TARGET then
level_state[y][x] = CELL_CRATE_ON_TARGET
else
error('cannot move crate to '..old)
end
end
function remove_crate_from(y, x)
local src = level_state[y][x]
if src == CELL_CRATE then
level_state[y][x] = CELL_VACANT
elseif src == CELL_CRATE_ON_TARGET then
level_state[y][x] = CELL_TARGET
end
end
function undo_move()
if undo_state == nil then return end
for _,sq in ipairs(undo_state) do
level_state[sq.y][sq.x] = sq.cell
end
player = player_state(level_state)
end
function any_state_in_level(needle)
for r,row in ipairs(level_state) do
for c,cell in ipairs(row) do
if cell == needle then
return true
end end end end
local undo_state = table.remove(undo_history)
function move_to_empty_space(y, x)
local path = unwind_path(find_path(level_state, player, {y=y, x=x}))
if #path == 0 then return end
local src = level_state[player.y][player.x]
local dest = level_state[y][x]
if dest == CELL_WALL or dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
return
end
local u = {
{x=player.x, y=player.y, cell=src},
{x=x, y=y, cell=dest}}
move_player_to(y, x)
remove_player_from(player.y, player.x)
player.x, player.y = x, y
table.insert(undo_history, u)
end
function move_down()
if player.y >= #level_state then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y+1][player.x]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x, y=player.y+1, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.y >= #level_state-1 then return end
local dest2 = level_state[player.y+2][player.x]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x, y=player.y+2, cell=dest2})
move_crate_to(player.y+2, player.x)
remove_crate_from(player.y+1, player.x)
end
move_player_to(player.y+1, player.x)
remove_player_from(player.y, player.x)
player.y = player.y+1
table.insert(undo_history, u)
end
function move_up()
if player.y <= 1 then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y-1][player.x]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x, y=player.y-1, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.y <= 2 then return end
local dest2 = level_state[player.y-2][player.x]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x, y=player.y-2, cell=dest2})
move_crate_to(player.y-2, player.x)
remove_crate_from(player.y-1, player.x)
end
move_player_to(player.y-1, player.x)
remove_player_from(player.y, player.x)
player.y = player.y-1
table.insert(undo_history, u)
end
function move_left()
if player.x <= 1 then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y][player.x-1]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x-1, y=player.y, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.x <= 2 then return end
local dest2 = level_state[player.y][player.x-2]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x-2, y=player.y, cell=dest2})
move_crate_to(player.y, player.x-2)
remove_crate_from(player.y, player.x-1)
end
move_player_to(player.y, player.x-1)
remove_player_from(player.y, player.x)
player.x = player.x-1
table.insert(undo_history, u)
end
function move_right()
if player.x >= #level_state[1] then return end
local src = level_state[player.y][player.x]
local dest = level_state[player.y][player.x+1]
if dest == CELL_WALL then return end
local u = {
{x=player.x, y=player.y, cell=src},
{x=player.x+1, y=player.y, cell=dest}}
if dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
if player.x >= #level_state[1]-1 then return end
local dest2 = level_state[player.y][player.x+2]
if dest2 ~= CELL_VACANT and dest2 ~= CELL_TARGET then return end
table.insert(u, {x=player.x+2, y=player.y, cell=dest2})
move_crate_to(player.y, player.x+2)
remove_crate_from(player.y, player.x+1)
end
move_player_to(player.y, player.x+1)
remove_player_from(player.y, player.x)
player.x = player.x+1
table.insert(undo_history, u)
end
function move_to_empty_space(y, x)
local path = unwind_path(find_path(level_state, player, {y=y, x=x}))
if #path == 0 then return end
local src = level_state[player.y][player.x]
local dest = level_state[y][x]
if dest == CELL_WALL or dest == CELL_CRATE or dest == CELL_CRATE_ON_TARGET then
return
end
local u = {
{x=player.x, y=player.y, cell=src},
{x=x, y=y, cell=dest}}
move_player_to(y, x)
remove_player_from(player.y, player.x)
player.x, player.y = x, y
table.insert(undo_history, u)
end
function move_player_to(y, x)
local old = level_state[y][x]
if old == CELL_VACANT then
level_state[y][x] = CELL_PLAYER
elseif old == CELL_TARGET then
level_state[y][x] = CELL_PLAYER_ON_TARGET
else
error('cannot move player to '..old)
end
end
function remove_player_from(y, x)
local src = level_state[y][x]
if src == CELL_PLAYER then
level_state[y][x] = CELL_VACANT
elseif src == CELL_PLAYER_ON_TARGET then
level_state[y][x] = CELL_TARGET
end
end
function move_crate_to(y, x)
local old = level_state[y][x]
if old == CELL_VACANT then
level_state[y][x] = CELL_CRATE
elseif old == CELL_TARGET then
level_state[y][x] = CELL_CRATE_ON_TARGET
else
error('cannot move crate to '..old)
end
end
function remove_crate_from(y, x)
local src = level_state[y][x]
if src == CELL_CRATE then
level_state[y][x] = CELL_VACANT
elseif src == CELL_CRATE_ON_TARGET then
level_state[y][x] = CELL_TARGET
end
end
function undo_move()
local undo_state = table.remove(undo_history)
if undo_state == nil then return end
for _,sq in ipairs(undo_state) do
level_state[sq.y][sq.x] = sq.cell
end
player = player_state(level_state)
end
end
function car.draw()
-- the sprites are pretty dark; a dark background accentuates contrast between their colors
g.setBackgroundColor(0.4,0.4, 0.4)
ui_state.button_handlers = {}
draw_level_state()
draw_buttons()
draw_level_number()
draw_win_state()
draw_hud()
end
-- some constants for draw_level_number
level_color = {0.8,0.8,0.8}
level_width = App.width('MMM')+10
function draw_level_number()
local lx, ly = left-level_width, top+10
if Safe_height > Safe_width then
lx, ly = (Safe_width - level_width)/2, top-side-Line_height-10
end
color(unpack(level_color))
g.print(curr_level, lx,ly)
end
function draw_win_state()
if not any_state_in_level(CELL_CRATE) then
color(0,1,0)
g.print('success!', left-2*side, top+side)
end
function draw_buttons()
local bg = {r=0.4,g=0.2,b=0.4}
button(ui_state, 'left', {x=0, y=bside, w=bside, h=Safe_height-bside*2,
bg=bg, onpress1=move_left})
button(ui_state, 'right', {x=Safe_width-bside, y=bside, w=bside, h=Safe_height-bside*2,
bg=bg, onpress1=move_right})
button(ui_state, 'up', {x=bside, y=0, w=Safe_width-bside*2, h=bside,
bg=bg, onpress1=move_up})
button(ui_state, 'down', {x=bside, y=Safe_height-bside, w=Safe_width-bside*2, h=bside,
bg=bg, onpress1=move_down})
-- level navigation
local ns, ps = tostring('>>'), tostring('<<')
local nw, pw = App.width(ns)+10, App.width(ps)+10
local h = Line_height+10
local nx,px, y
if Safe_width > Safe_height then
px, nx = left-side-pw-10, left+lw*side+side+10
y = top + lh*side/2
else
px, nx = left+side+10, left+lw*side-side-nw-10
y = top-side-h
end
if curr_level > 1 then
button(ui_state, 'previous level', {x=px, y=y, w=pw, h=h,
bg=bg, onpress1=previous_level,
icon=function(p)
color(unpack(level_color))
g.print(ps, p.x+5, p.y+5)
end,
})
end
if curr_level < #levels then
button(ui_state, 'next level', {x=nx, y=y, w=nw, h=h,
bg=bg, onpress1=next_level,
icon=function(p)
color(unpack(level_color))
g.print(ns, p.x+5, p.y+5)
end,
})
end
if #undo_history > 0 then
local w = App.width('undo')+10
local ux, uy = left+lw*side+10, top+10
if Safe_width < Safe_height then
ux, uy = left+lw*side-w-10, top+lh*side+10
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)
button(ui_state, 'undo', {x=ux, y=uy, w=w, h=Line_height+10,
bg=bg, onpress1=undo_move,
icon=function(p)
color(0,0,0)
g.print('undo', p.x+5, p.y+5)
end,
})
end
end
-- ensure we can fit the above layout into the available window area
-- leave 6 tiles worth of margin on all sides for the nav buttons
function car.resize()
local w,h = Safe_width, Safe_height
local w1, w2 = landscape_button_offsets()
local h1, h2 = portrait_button_offsets()
if Safe_width > Safe_height then
side = min((w-w1-w2)/(1+lw+1+6), h/(lh+6)) -- leave 1 side between board and buttons
else
side = min(w/(lw+6), (h-h1-h2)/(1+lh+1+6)) -- leave 1 side between board and buttons
end
pxside = side/num_tile_px
left = (w-side*lw)/2
top = (h-side*lh)/2
if Safe_width > Safe_height then
bside = min((w-w1-w2-side*(1+lw+1))/2, (h-side*lh)/2)
else
bside = min((h-h1-h2-side*(1+lh+1))/2, (w-side*lw)/2)
table.insert(result, dest)
end
-- space needed for buttons to the left and right
function landscape_button_offsets()
local ns, ps = tostring('>>'), tostring('<<')
local nw, pw = App.width(ns)+10, App.width(ps)+10
return 10+pw+10, 10+nw+10
-- we assume the level name and undo button will be within this width
end
-- space needed for buttons to the top and bottom
function portrait_button_offsets()
return 10+Line_height+10, 10+Line_height+10
return result
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
-- routines for drawing and sizing to available space
function car.draw()
-- the sprites are pretty dark; a dark background accentuates contrast between their colors
g.setBackgroundColor(0.4,0.4, 0.4)
ui_state.button_handlers = {}
draw_level_state()
draw_buttons()
draw_level_number()
draw_win_state()
draw_hud()
end
function draw_level_state()
for r,row in ipairs(level_state) do
for c,cell in ipairs(row) do
draw_sprite(cell, left+(c-1)*side, top+(r-1)*side)
end end end
function draw_sprite(cell, x,y)
if cell >= CELL_VACANT then
color(0,0,0)
rect('fill', x,y, side,side)
return
end
local sprite = sprites[cell+1]
for r,row in ipairs(sprite) do
for c,clr in ipairs(row) do
color(unpack(clr))
rect('fill', x+(c-1)*pxside, y+(r-1)*pxside, pxside, pxside)
end end end
-- some constants for draw_level_number
level_color = {0.8,0.8,0.8}
level_width = App.width('MMM')+10
function draw_level_number()
local lx, ly = left-level_width, top+10
if Safe_height > Safe_width then
lx, ly = (Safe_width - level_width)/2, top-side-Line_height-10
end
color(unpack(level_color))
g.print(curr_level, lx,ly)
end
function draw_win_state()
if not any_state_in_level(CELL_CRATE) then
color(0,1,0)
g.print('success!', left-2*side, top+side)
end
end
function any_state_in_level(needle)
for r,row in ipairs(level_state) do
for c,cell in ipairs(row) do
if cell == needle then
return true
end end end end
function draw_buttons()
local bg = {r=0.4,g=0.2,b=0.4}
button(ui_state, 'left', {x=0, y=bside, w=bside, h=Safe_height-bside*2,
bg=bg, onpress1=move_left})
button(ui_state, 'right', {x=Safe_width-bside, y=bside, w=bside, h=Safe_height-bside*2,
bg=bg, onpress1=move_right})
button(ui_state, 'up', {x=bside, y=0, w=Safe_width-bside*2, h=bside,
bg=bg, onpress1=move_up})
button(ui_state, 'down', {x=bside, y=Safe_height-bside, w=Safe_width-bside*2, h=bside,
bg=bg, onpress1=move_down})
-- level navigation
local ns, ps = tostring('>>'), tostring('<<')
local nw, pw = App.width(ns)+10, App.width(ps)+10
local h = Line_height+10
local nx,px, y
if Safe_width > Safe_height then
px, nx = left-side-pw-10, left+lw*side+side+10
y = top + lh*side/2
else
px, nx = left+side+10, left+lw*side-side-nw-10
y = top-side-h
end
if curr_level > 1 then
button(ui_state, 'previous level', {x=px, y=y, w=pw, h=h,
bg=bg, onpress1=previous_level,
icon=function(p)
color(unpack(level_color))
g.print(ps, p.x+5, p.y+5)
end,
})
end
if curr_level < #levels then
button(ui_state, 'next level', {x=nx, y=y, w=nw, h=h,
bg=bg, onpress1=next_level,
icon=function(p)
color(unpack(level_color))
g.print(ns, p.x+5, p.y+5)
end,
})
end
if #undo_history > 0 then
local w = App.width('undo')+10
local ux, uy = left+lw*side+10, top+10
if Safe_width < Safe_height then
ux, uy = left+lw*side-w-10, top+lh*side+10
end
button(ui_state, 'undo', {x=ux, y=uy, w=w, h=Line_height+10,
bg=bg, onpress1=undo_move,
icon=function(p)
color(0,0,0)
g.print('undo', p.x+5, p.y+5)
end,
})
end
end
-- leave 6 tiles worth of margin on all sides for the nav buttons
function car.resize()
local w,h = Safe_width, Safe_height
local w1, w2 = landscape_button_offsets()
local h1, h2 = portrait_button_offsets()
if Safe_width > Safe_height then
side = min((w-w1-w2)/(1+lw+1+6), h/(lh+6)) -- leave 1 side between board and buttons
else
side = min(w/(lw+6), (h-h1-h2)/(1+lh+1+6)) -- leave 1 side between board and buttons
end
pxside = side/num_tile_px
left = (w-side*lw)/2
top = (h-side*lh)/2
if Safe_width > Safe_height then
bside = min((w-w1-w2-side*(1+lw+1))/2, (h-side*lh)/2)
else
bside = min((h-h1-h2-side*(1+lh+1))/2, (w-side*lw)/2)
end
end
-- space needed for buttons to the left and right
function landscape_button_offsets()
local ns, ps = tostring('>>'), tostring('<<')
local nw, pw = App.width(ns)+10, App.width(ps)+10
return 10+pw+10, 10+nw+10
-- we assume the level name and undo button will be within this width
end
-- space needed for buttons to the top and bottom
function portrait_button_offsets()
return 10+Line_height+10, 10+Line_height+10
end