A little programming game
The kids recently got a little paper computer and, perhaps ill-advisedly, I've been building it on my touchscreen computer. The way I rationalize it is, the paper computer gives no feedback on whether a program works, and checking instructions seems like a tedious, error-prone task that is ideal to be automated away.
My app is "responsive", i.e. it adapts to the size of the screen. If you rotate your device, it adjusts gracefully. It's amazing how few lines of code it takes to obtain responsiveness if you build your "page" in an imperative way rather than atop layers of ostensibly "declarative" notation.
Unfortunately, in the process of making it responsive I found something missing in Carousel: a `car.resize` function akin to `love.resize` that is called when you rotate your device. So I had to ship a new version with this function. Upgrade to try out this program.
This is the largest program I've shown so far, and to keep it manageable I've split it up into 3 screens. Here's the first, copy it into a screen called 'rabbot':
ui_state = {} -- button state recreated each frame -- level is 4x4 tiles -- each tile is 2 digits, connects at most 2 sides -- up=1 right=2 down=3 left=4 -- examples: -- 12 from upper margin to right -- 13 straight from top to bottom of tile board = { {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, } board_color = {0, 0.8, 0} r = 4 -- road width road_color = {0.4, 0, 0.4} code = {} -- up to 20 steps ('forward', 'left' or 'right') -- rabbot initially starts out outside the board, above the top-left tile, going down rabbot_state = {x=1,y=0, dir=3, status=nil} animation_frame_duration = 0.5 -- seconds -- at most one tool will be dragged at a time -- examples: {'board', 42}, {'code', 'forward'}, {'rabbot'} drag = nil -- 16x16 rabbot sprite authored elsewhere rabbot = json.decode([=[ [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,1,1,1,1,1,2,2,1,1,1,2,2,1,1,1], [1,1,1,1,1,1,1,2,2,1,1,1,2,2,1,1], [1,1,1,1,1,1,1,2,2,2,2,1,2,2,1,1], [1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1,1,2,1,2,2,1,2,1], [1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1,1,2,2,1,2,1,2,1], [1,1,1,1,1,1,1,1,1,1,2,2,1,2,2,1], [1,1,1,1,1,1,1,1,1,1,1,2,2,2,1,1], [1,1,2,2,2,2,2,2,2,2,2,2,2,2,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,1,2,2,1,1,2,2,1,2,2,1,1,2,2,1], [1,1,2,2,1,1,2,2,1,2,2,1,1,2,2,1], [1,1,1,2,2,2,2,1,1,1,2,2,2,2,1,1]] ]=]) rabbot_palette = {board_color, {1,1,1}} function car.draw() ui_state.button_handlers = {} if Safe_width > Safe_height then draw_landscape() else draw_portrait() end draw_drag() draw_rabbot() draw_rabbot_path() end -- initialize viewport now and any time device rotates function car.resize(w,h) W = Safe_width-30*2 -- next/prev screen buttons H = Safe_height - Menu_bottom end car.resize() function draw_landscape() -- width: 2 toolbar + 1 gutter + 4 board + 1 gutter + 1 toolbar + 1 gutter + 5 code = 15 units -- height: 1 status + 4 code + 1 gutter + 1 run button = 7 units side = floor(min(W/15, H/7)) local left = floor((Safe_width-side*15)/2) local top = floor((Safe_height-side*7)/2) top = top + side draw_board_toolbar(left, top) left = left + side*3 draw_board(left, top) left = left + side*5 draw_code_toolbar(left, top) left = left + side*2 draw_code(left, top) end function draw_portrait() -- width: 2 toolbar + 1 gutter + 4 board + 1 drop zone = 8 units -- height: 1 status + 4 board + 1 gutter + 4 code + 1 gutter + 1 run button = 12 units side = floor(min(W/8, H/12)) local left = floor((Safe_width-side*8)/2) local top = floor((Safe_height-side*12)/2) top = top+side draw_board_toolbar(left, top) draw_board(left+side*3, top) top = top+side*5 draw_code_toolbar(left, top) draw_code(left+side*2, top) end function car.mouse_press(x,y, b) rabbot_state.status = nil rabbot_state.path = {} rabbot_state.ip = nil if mouse_press_consumed_by_any_button(ui_state, x,y, b) then return end end function car.mouse_release(x,y) if drag == nil then return end if drag[1] == 'board' then local b = ui_state.board local left, top = b.left, b.top if x >= left and x < left+4*side and y >= top and y < top+4*side then board[floor((y-top)/side)+1][floor((x-left)/side)+1] = drag[2] end elseif drag[1] == 'code' then local c = ui_state.code local left, top = c.left, c.top if x >= left and x < left+5*side and y >= top and y < top+4*side then local i = floor((y-top)/side)*5 + floor((x-left)/side)+1 if i <= #code or drag[2] == nil then code[i] = drag[2] else table.insert(code, drag[2]) end end elseif drag[1] == 'rabbot' then drop_rabbot() rabbot_state.path = {} rabbot_state.ip = 1 rabbot_state.next_update = Current_time + animation_frame_duration end drag = nil end function car.update(dt) step_rabbot() end function draw_drag() if drag == nil then return end local x,y = App.mouse_x()-side/2, App.mouse_y()-side/2 if drag[1] == 'board' then draw_tile_bg(x, y) draw_tile_fg(drag[2], x,y) elseif drag[1] == 'code' then draw_code_step('', drag[2], x,y) 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
Here's the second screen, save it as `rabbot-draw`:
-- board function draw_board_toolbar(left, top) draw_tile_tool(0, left+side/2, top) draw_tile_tool(13, left, top+side) draw_tile_tool(24, left+side, top+side) draw_tile_tool(23, left, top+2*side) draw_tile_tool(34, left+side, top+2*side) draw_tile_tool(12, left, top+3*side) draw_tile_tool(14, left+side, top+3*side) end function draw_tile_tool(t, left, top) local b = board_color button(ui_state, t, { x=left, y=top, w=side-1, h=side-1, bg={r=b[1], g=b[2], b=b[3]}, icon = function(p) draw_tile_fg(t, p.x, p.y) end, onpress1 = function() drag = {'board', t} end }) end function draw_board(left, top) ui_state.board = {left=left, top=top} for y,row in ipairs(board) do for x,_ in ipairs(row) do draw_tile_bg(left+(x-1)*side, top+(y-1)*side) end end for y,row in ipairs(board) do for x,tile in ipairs(row) do draw_tile_fg(tile, left+(x-1)*side, top+(y-1)*side) end end if rabbot_state.status then color(0,0,0) g.print(rabbot_state.status, left, top-side/2) end color(0,0,0) g.print('board', left+2*side-App.width('board')/2, top+side*4) end function draw_tile_bg(x,y) color(unpack(board_color)) rect('fill', x,y, side-1,side-1) end function draw_tile_fg(t, x,y) if t == 0 then return end color(unpack(road_color)) g.setLineWidth(r) if t == 13 then line(x+side/2, y, x+side/2, y+side) elseif t == 24 then line(x, y+side/2, x+side, y+side/2) elseif t == 12 then arc('line', 'open', x+side,y, side/2, pi/2, pi) elseif t == 14 then arc('line', 'open', x,y, side/2, 0,pi/2) elseif t == 23 then arc('line', 'open', x+side,y+side, side/2, pi, pi*3/2) elseif t == 34 then arc('line', 'open', x,y+side, side/2, pi*3/2, pi*2) end end -- code function draw_code_toolbar(left, top) draw_code_tool(nil, left, top) draw_code_tool('forward', left, top+side) draw_code_tool('left', left, top+2*side) draw_code_tool('right', left, top+3*side) end function draw_code_tool(dir, left, top) button(ui_state, t, { x=left, y=top, w=side-1, h=side-1, icon = function(p) draw_code_step('', dir, p.x, p.y) end, onpress1 = function() drag = {'code', dir} end }) end function draw_code(left, top) ui_state.code = {left=left, top=top} color(0.5, 0.5, 0.5) local x, y = left, top for i=1,20 do draw_code_step(i, code[i], x, y) if i%5 > 0 then x = x + side else y = y + side x = left end end color(0,0,0) g.print('instructions', left+2.5*side-App.width('instructions')/2, top+side*4) end function draw_code_step(i, dir, x, y) if dir == nil then color(0.5, 0.5, 0.5) g.setLineWidth(1) rect('line', x,y, side, side) g.print(i, x+5, y+5) elseif dir == 'forward' then color(0.4, 0.8, 0.8) rect('fill', x,y, side, side) color(0,0,0) g.print(i, x+5, y+5) color(1, 0.1, 0) g.setLineWidth(4) line(x+side/2, y+side/4, x+side/2, y+side-side/8) poly('fill', x+side/2, y+side/8, x+side/2-side/8, y+side/4, x+side/2+side/8, y+side/4) elseif dir == 'left' then color(0, 0.5, 0) rect('fill', x,y, side, side) color(0,0,0) g.print(i, x+5, y+5) color(1, 1, 0) g.setLineWidth(4) arc('line', 'open', x+side/4, y+side-side/8, side/2, 0, -pi/2) local x, y = x+side/4-side/16, y+side/2-side/8 poly('fill', x, y, x+side/8, y-side/8, x+side/8, y+side/8) elseif dir == 'right' then color(0, 0, 0.4) rect('fill', x,y, side, side) color(0.6,0.6,0.6) g.print(i, x+5, y+5) color(1, 1, 0) g.setLineWidth(4) arc('line', 'open', x+side*3/4, y+side-side/8, side/2, pi, pi*3/2) local x, y = x+side*3/4, y+side/2-side/8 poly('fill', x+side/16, y, x-side/16, y-side/8, x-side/16, y+side/8) end end -- rabbot function draw_rabbot() if drag == nil then local x, y = rabbot_position(rabbot_state.x, rabbot_state.y, rabbot_state.dir) button(ui_state, 'rabbot', { x=x, y=y, w=32, h=32, icon = function(p) draw_sprite(rabbot, 2, p.x,p.y, rabbot_palette) end, onpress1 = function() drag = {'rabbot'} end, }) elseif drag[1] == 'rabbot' then draw_rabbot_drop_zones() local mx,my = love.mouse.getPosition() draw_sprite(rabbot, 2, mx,my, rabbot_palette) end end
Finally the third screen, save it as `rabbot-run`:
run_screen('rabbot') run_screen('rabbot-draw') -- trigger run by releasing the rabbot sprite function drop_rabbot() local mx,my = love.mouse.getPosition() -- up for x=1,4 do local px, py = rabbot_drop_zone(x, 0, 3) if in_rabbot_drop_zone(px,py, mx,my) then print('drop', x, 0, 3) rabbot_state = {x=x, y=0, dir=3} return end end -- left for y=1,4 do local px, py = rabbot_drop_zone(0, y, 2) if in_rabbot_drop_zone(px,py, mx,my) then print('drop', 0, y, 2) rabbot_state = {x=0, y=y, dir=2} return end end -- down for x=1,4 do local px, py = rabbot_drop_zone(x, 5, 1) if in_rabbot_drop_zone(px,py, mx,my) then print('drop', x, 5, 1) rabbot_state = {x=x, y=5, dir=1} return end end -- right for y=1,4 do local px, py = rabbot_drop_zone(5, y, 4) if in_rabbot_drop_zone(px,py, mx,my) then print('drop', 5, y, 4) rabbot_state = {x=5, y=y, dir=4} return end end -- invalid print('cannot drop rabbot there') status('cannot drop rabbot there') end -- valid drop zones function draw_rabbot_drop_zones() color(0.5,0.5,0.5, 0.5) -- up for x=1,4 do local x, y = rabbot_drop_zone(x, 0, 3) rect('fill', x,y, side-1,side-1) end -- left for y=1,4 do local x, y = rabbot_drop_zone(0, y, 2) rect('fill', x,y, side-1,side-1) end -- down for x=1,4 do local x, y = rabbot_drop_zone(x, 5, 1) rect('fill', x,y, side-1,side-1) end -- right for y=1,4 do local x, y = rabbot_drop_zone(5, y, 4) rect('fill', x,y, side-1,side-1) end end function step_rabbot() if rabbot_state.status then rabbot_state.ip = nil end if rabbot_state.ip == nil then return end if rabbot_state.ip > 20 then return end if Current_time < rabbot_state.next_update then return end traverse_edge() if rabbot_state.status then return end traverse_tile_with_instruction(rabbot_state.ip) rabbot_state.ip = rabbot_state.ip+1 rabbot_state.next_update = Current_time + animation_frame_duration end function traverse_edge() local r = rabbot_state -- compute next tile to move to print('jump', r.x, r.y, r.dir) if r.dir == 1 then r.y = r.y-1 elseif r.dir == 2 then r.x = r.x+1 elseif r.dir == 3 then r.y = r.y+1 elseif r.dir == 4 then r.x = r.x-1 else return status('bug: invalid dir') end r.dir = across(r.dir) -- out of bounds after any jumping (i.e. ignoring start) = success if r.x < 1 or r.x > #board or r.y < 1 or r.y > #board then print('=>', r.x, r.y, r.dir) status('across!') end end function traverse_tile_with_instruction(i) local r = rabbot_state local tile = board[r.y][r.x] print('tile', r.x, r.y, 'is', tile) -- check that there's track in the starting direction local o, t = tile%10, floor(tile/10) -- ones, tens if r.dir ~= o and r.dir ~= t then return status('fell off the track') end -- compare end direction of current tile and instruction local a, e = o, nil -- actual, expected end dir if r.dir == o then a = t end if code[i] == nil then return status('need more instructions') end if code[i] == 'forward' then e = across(r.dir) elseif code[i] == 'left' then e = clockwise(r.dir) elseif code[i] == 'right' then e = anticlockwise(r.dir) else return status('bug: invalid instruction') end if e ~= a then return status('instruction is wrong', e, a) end r.dir = e table.insert(rabbot_state.path, {r.x,r.y}) -- success end function rabbot_position(x,y, d) local x, y = rabbot_drop_zone(x,y, d) if d == 1 then -- left x = x+side/2-16 elseif d == 2 then -- right x = x+side-32 y = y+side/2-16 elseif d == 3 then -- down x = x+side/2-16 y = y+side-32 elseif d == 4 then -- left y = y+side/2-16 end return x, y end function rabbot_drop_zone(x,y, d) local x = ui_state.board.left+side*(x-1) local y = ui_state.board.top+side*(y-1) return x, y end function in_rabbot_drop_zone(px,py, mx,my) return mx >= px and mx < px+side and my >= py and my < py+side end function status(msg) rabbot_state.status = msg end function across(dir) return (dir+2-1)%4+1 end function clockwise(dir) return (dir+1-1)%4+1 end function anticlockwise(dir) return (dir+3-1)%4+1 end function draw_rabbot_path() if rabbot_state.path == nil then return end color(0,0,0) for i,p in ipairs(rabbot_state.path) do local x, y = unpack(p) g.print(i, ui_state.board.left+(x-1)*side+5, ui_state.board.top+(y-1)*side+5) end end
After saving all 3, hit run on the final screen (it'll pull in the others as needed). You'll need to first run the abbreviations on one of the example screens. Or if you've deleted that screen, here are the abbreviations I used in this post:
g = love.graphics line, rect, poly = g.line, g.rectangle, g.polygon arc = g.arc color = g.setColor min, max = math.min, math.max floor, ceil = math.floor, math.ceil pi = math.pi
Forgive the poor quality art for the bot. I'd appreciate improvements.
Also, if you've read this far, please send me any programs you've built using Lua Carousel. They can be big or tiny, working or broken. I'd appreciate hearing from you.
Files
Get Lua Carousel
Lua Carousel
Write programs on desktop and mobile
Status | In development |
Category | Tool |
Author | Kartik Agaram |
Tags | LÖVE |
More posts
- New version after 3 days26 days ago
- New version after 40 days29 days ago
- Turn your phone or tablet into a chess clock42 days ago
- Guest program: bouncing balls59 days ago
- New version after 3 months, and turtle graphics69 days ago
- Interactively zooming in to the Mandelbrot set on a touchscreen88 days ago
- A little timer app89 days ago
- New version after 4 monthsJun 28, 2024
- Visualizing the digits of πMay 04, 2024
Leave a comment
Log in with itch.io to leave a comment.