A little game integrated with its tools

A week ago, my son (hearing that I was at loose ends) asked for "a game where you drag the player around the screen". This set some wheels in motion inside my head, resulting in this 3-screen app: one screen for a sprite editor, a second screen for a level/maze editor, and the final screen putting them together into a weird little game where you try to solve a maze without letting your player touch the walls. The screens exchange information using global variables for the player sprite and the maze. To try it out after downloading Lua Carousel:
1. Run the following abbreviations:
g = love.graphics pt, line, rect = g.point, g.line, g.rectangle color = g.setColor min, max = math.min, math.max floor = math.floor
2. In a new screen, run the following sprite editor (100 lines):
palette = {{1,0,0}, {0,0,1}}
W,H = 8,8
sprite = {}
for i=1,H do
local row = {}
for j=1,W do
table.insert(row, (i+j)%#palette + 1)
end
table.insert(sprite, row)
end
Ezoom = 32 -- pixels
-- top-left of sprite
EX,EY = 40, 90
Ewidgets = {}
-- one button per pixel of sprite
function sprite_pixel(X,Y)
local x = EX + (X-1)*Ezoom
local y = EY + (Y-1)*Ezoom
local draw = function()
color(unpack(palette[sprite[Y][X]]))
rect('fill', x,y, Ezoom-1,Ezoom-1)
end
local ispress = function(x2,y2)
return x2 >= x and x2 <= x+Ezoom and y2 >= y and y2 <= y+Ezoom
end
local press = function()
sprite[Y][X] = sprite[Y][X]%#palette + 1
end
return {draw=draw, ispress=ispress, press=press}
end
for y=1,H do
for x=1,W do
table.insert(Ewidgets, sprite_pixel(x,y))
end
end
-- one slider per color component in palette
function palette_component(p, c)
local x0, x1 = EX+20, 200 -- left and right limit
local lo, hi = 0, 1 -- what left and right map to
local x = x0 + (x1-x0)*(palette[p][c]-lo)/(hi-lo)
local top = EY+Ezoom*H + 30
local y = top + (p-1)*100 + (c-1)*30
local w,h = 20,20
local selected = false
local slider_color = {0,0,0}
slider_color[c] = 1
local draw = function()
color(unpack(slider_color))
line(x0,y, x1,y)
rect('fill', x-w/2, y-h/2, w,h)
end
local ispress = function(x2,y2)
return x2 >= x-w/2 and x2 <= x+w/2 and y2 >= y-h/2 and y2 <= y+h/2
end
local press = function() selected = true end
local update = function(x2,y2)
if selected then
x = min(max(x2, x0), x1)
palette[p][c] = lo + (x-x0)*(hi-lo)/(x1-x0)
end
end
local release = function() selected = false end
return {draw=draw, ispress=ispress, press=press, update=update, release=release}
end
for p=1,#palette do
for c=1,#palette[1] do
table.insert(Ewidgets, palette_component(p, c))
end
end
function car.draw()
for name,w in pairs(Ewidgets) do
w.draw()
end
end
function car.mousepressed(x,y, b)
for name,w in pairs(Ewidgets) do
if w.ispress(x,y) then
return w.press()
end
end
end
function car.update(dt)
for name,w in pairs(Ewidgets) do
if w.update then w.update(App.mouse_x(), App.mouse_y()) end
end
end
function car.mousereleased(x,y, b)
for name,w in pairs(Ewidgets) do
if w.release then w.release() end
end
end
(Many globals here start with 'E' to avoid conflicting with the other screens.)
3. In a new screen, run the following maze editor (100 lines):
Aw = Safe_width-60 -- playing area
Ah = Safe_height-Menu_bottom
Pzoom = 4
local Pw = W*Pzoom -- player width
local Psx = floor(Pw*1.5) -- player x space
local Mcw = floor(Aw/Psx) -- maze width in cells
local Mw = Psx*Mcw -- Maze width
local Mx = floor((Safe_width-Mw)/2)
local Ph = H*Pzoom
local Psy = floor(Ph*1.5)
local Mch = floor(Ah/Psy) -- maze height in cells
local Mh = Psy*Mch -- Maze height
local My = Menu_bottom + floor((Safe_height-Menu_bottom-Mh)/2)
-- maze consists of Mcw*Mch rooms
-- each room has 4 walls around it
-- but the outer walls are implicitly built and so not represented
-- so we have (Mch-1)*Mcw horizontal walls and Mch*(Mcw-1) vertical walls
-- cell x,y has horizontal walls x,y-1 and x,y, and vertical walls x-1,y and x,y
-- walls that are out of bounds of Mhw and Mvw are always solid
Mhw, Mvw = {}, {}
for i=1,Mch-1 do
local mhwr = {}, {}
for j=1,Mcw do
table.insert(mhwr, 0)
end
table.insert(Mhw, mhwr)
end
for i=1,Mch do
local mvwr = {}
for j=1,Mcw-1 do
table.insert(mvwr, 0)
end
table.insert(Mvw, mvwr)
end
widgets = {}
-- one button per horizontal wall
function maze_hor_wall(X,Y)
local x = Mx + (X-1)*Psx+10
local y = My + Y*Psy-10
local w = Psx-20
local h = 20
local draw = function()
color(0.5,0.5,0.5)
if Mhw[Y][X] == 1 then
rect('fill', x,y,w,h, 5)
else
rect('line', x,y,w,h, 5)
end
end
local ispress = function(x2,y2)
return x2 >= x and x2 <= x+w and y2 >= y and y2 <= y+h
end
local press = function()
Mhw[Y][X] = 1-Mhw[Y][X]
end
return {draw=draw, ispress=ispress, press=press}
end
for y=1,Mch-1 do
for x=1,Mcw do
table.insert(widgets, maze_hor_wall(x,y))
end
end
-- one button per vertical wall
function maze_vert_wall(X,Y)
local x = Mx + X*Psx-10
local y = My + (Y-1)*Psy+10
local w = 20
local h = Psy-20
local draw = function()
color(0.5,0.5,0.5)
if Mvw[Y][X] == 1 then
rect('fill', x,y,w,h, 5)
else
rect('line', x,y,w,h, 5)
end
end
local ispress = function(x2,y2)
return x2 >= x and x2 <= x+w and y2 >= y and y2 <= y+h
end
local press = function()
Mvw[Y][X] = 1-Mvw[Y][X]
end
return {draw=draw, ispress=ispress, press=press}
end
for y=1,Mch do
for x=1,Mcw-1 do
table.insert(widgets, maze_vert_wall(x,y))
end
end
function car.draw()
color(0.8,0.8,0.8)
rect('line', Mx, My, Mcw*Psx, Mch*Psy)
for name,w in pairs(widgets) do
w.draw()
end
end
function car.mousepressed(x,y, b)
for name,w in pairs(widgets) do
if w.ispress(x,y) then
return w.press()
end
end
end
function car.update(dt)
for name,w in pairs(widgets) do
if w.update then w.update(App.mouse_x(), App.mouse_y()) end
end
end
function car.mousereleased(x,y, b)
for name,w in pairs(widgets) do
if w.release then w.release() end
end
end
(The names got a bit cryptic here as I worked on my phone. Hopefully the comments are clear. Definitely comment if something doesn't make sense.)
4. In a new screen, run the following code for the game itself (100 lines) which uses the sprite and maze defined in the previous screens.
Game_over = nil
Aw = Safe_width-60 -- playing area
Ah = Safe_height-Menu_bottom
Pzoom = 4
local Pw = W*Pzoom -- player width
local Psx = floor(Pw*1.5) -- player x space
local Mcw = floor(Aw/Psx) -- maze width in cells
local Mw = Psx*Mcw -- Maze width
local Mx = floor((Safe_width-Mw)/2)
local Ph = H*Pzoom
local Psy = floor(Ph*1.5)
local Mch = floor(Ah/Psy) -- maze height in cells
local Mh = Psy*Mch -- Maze height
local My = Menu_bottom + floor((Safe_height-Menu_bottom-Mh)/2)
Px,Py = Mx+5, My+5
move = nil
function car.draw()
draw_board()
--draw_game_over()
draw_sprite(sprite, Pzoom, Px,Py, palette)
color(0.5,0.5,0.5)
rect('fill', Px, Py+#sprite*Pzoom, #sprite[1]*Pzoom, 30)
if Game_over then
color(1,0,0)
g.print('game over!', 30,30)
end
end
function car.mousepressed(x,y, b)
if Game_over then return end
if x >= Px and x <= Px+#sprite[1]*Pzoom and
y >= Py+#sprite*Pzoom and y <= Py+#sprite*Pzoom+30 then
move = {x=x-Px, y=y-Py}
end
end
function car.mousereleased()
if Game_over then return end
move = nil
end
function car.update()
if Game_over then return end
if move then
Px = App.mouse_x() - move.x
Py = App.mouse_y() - move.y
Game_over = game_over(Px, Py)
end
end
function game_over(x, y)
-- is Px, Py colliding with a solid wall?
-- borders
local mcx = floor((x-Mx)/Psx)+1
local mcy = floor((y-My)/Psy)+1
if mcx < 1 or mcy < 1 then return true end
if mcx > Mcw or mcy > Mch then return true end
local ox = (x-Mx)%Psx
local oy = (y-My)%Psy
if ox > Pw/2 then
if mcx == Mcw then return true end
if Mvw[mcy][mcx] > 0 then return true end
end
if oy > Ph/2 then
if mcy == Mch then return true end
if Mhw[mcy][mcx] > 0 then return true end
end
end
function draw_game_over()
for x=30,Safe_width do
for y=Menu_bottom,Safe_height do
if game_over(x,y) then
color(1,0,0, 0.2)
else
color(0,1,0, 0.2)
end
pt(x,y)
end end end
function draw_board()
color(0.8,0.8,0.8)
rect('line', Mx, My, Mcw*Psx, Mch*Psy)
for y=1,Mch-1 do
for x=1,Mcw do
if Mhw[y][x] > 0 then
line(Mx+(x-1)*Psx, My+y*Psy, Mx+x*Psx, My+y*Psy)
end
end
end
for y=1,Mch do
for x=1,Mcw-1 do
if Mvw[y][x] > 0 then
line(Mx+x*Psx, My+(y-1)*Psy, Mx+x*Psx, My+y*Psy)
end
end
end
end
function draw_sprite(sprite, zoom, x,y, palette)
for j,row in ipairs(sprite) do
for i,cell in ipairs(row) do
color(unpack(palette[cell]))
rect('fill', x+(i-1)*zoom, y+(j-1)*zoom, zoom, zoom)
end end end
That's it. Once you've pasted and run these screens once, you can bounce between them editing the maze or the player sprite, and see the changes immediately in the game.
One little thing that has frustrated me on touch-screen devices is that my finger often hinders my view of the gestures its performing. In this game I interact with the player using a little handle (akin to a tab or flap you might find in a pop-up book) that is otherwise invisible to the rest of the game.
Get Lua Carousel
Lua Carousel
Write programs on desktop and mobile
| Status | In development |
| Category | Tool |
| Author | Kartik Agaram |
| Tags | LÖVE |
More posts
- Programming on your device with your preferred language63 days ago
- Lua Carousel: program on the device you have, with docs at your fingertipsMay 12, 2025
- Pong Wars, MMO editionFeb 16, 2025
- New version after 41 days, and stop-motion animationFeb 15, 2025
- Drawing with a pen on a pendulumJan 11, 2025
- New version after 16 daysJan 04, 2025
- New version after 9 daysDec 19, 2024
- New version after 3 daysNov 17, 2024
- New version after 40 daysNov 14, 2024
- Turn your phone or tablet into a chess clockNov 01, 2024
Leave a comment
Log in with itch.io to leave a comment.