Skip to content

Instantly share code, notes, and snippets.

@wkjagt
Last active November 27, 2025 14:46
Show Gist options
  • Select an option

  • Save wkjagt/142e10aa9ee18cf24f234c94721f0366 to your computer and use it in GitHub Desktop.

Select an option

Save wkjagt/142e10aa9ee18cf24f234c94721f0366 to your computer and use it in GitHub Desktop.
Simple breakout game for picolua on the PicoCalc
local splash={
[[ ____________________ ____ __.]],
[[ \______ \______ \ |/ _|]],
[[ | | _/| _/ < ]],
[[ | | \| | \ | \ ]],
[[ |______ /|____|_ /____|__ \]],
[[ \/ \/ \/]],
[[ ________ ____ ______________]],
[[ \_____ \ | | \__ ___/]],
[[ / | \| | / | | ]],
[[ / | \ | / | | ]],
[[ \_______ /______/ |____| ]],
[[ \/ ]],
}
local BALLRAD <const> = 3 -- ball radius
local BLCKHGT <const> = 15 -- block height
local BLCKWDT <const> = 30 -- block width
local PADLWDT <const> = 60 -- paddle width
local PADLHGT <const> = 8 -- paddle height
local white=colors.fromRGB(250,250,250)
local backgr=colors.fromRGB(0,0,0)
local flash=colors.fromRGB(255,150,0)
local sounds={
bounce=sound.instrument(sound.drums.tom)
}
sounds.play=function(pitch)
sound.playPitch(1,pitch,sounds.bounce)
end
local function gray(i)
return colors.fromRGB(i,i,i)
end
local blockColors = {
gray(60),gray(100),gray(140),gray(180),gray(220)
}
local currentLevel=1
local levels={
--"1111111111.............1000000000",
"2323232323.3000000002.2023232303.3030000202.2020230303.3030020202.2023230303.3000000202.2323232303",
"000055.0005225.00522225.052222225.052222225.052222225.00522225.0005225.000055",
"3.23.323.2323.32323.232323.3232323.23232323.323232323",
"2222222222.2222222222.2000000002.2000000002.2000000002.2000000002.2000000002.2222222202.5555555505",
}
local blocks
-- state
local ball
local paddle
local score=0
local redraw=true
local continue=true
local levelComplete=false
local canLowerBlocks=true
local lowerBlocks=false
local blockStartPos=0
local gameover=false
local paused=false
local fast=false
------ helper functions -------
local function blockPos(col, row)
local x = 5+(col-1)*(BLCKWDT+1)
local y = 25+blockStartPos+(row)*(BLCKHGT+1)
return x, y
end
local function outside(n, s, e)
return n<s or n>e
end
local function centerText(y,text,col)
local x=(320-#text*8)/2
draw.text(x,y,text,col)
end
local function rightText(y,text)
local x=320-(#text-1)*8
draw.text(x,y,text)
end
local function wait(ms)
waitUntil=os.clock()+ms/1000
while os.clock()<waitUntil do end
end
-- call a function for each block in a level
-- continue to next block if function returns true
local function allBlocks(func)
for row, blks in ipairs(blocks) do
for col, b in ipairs(blks) do
if b>=0 then
local x, y = blockPos(col,row)
if not func(x, y, b, row, col) then
return
end
end
end
end
end
-- count remaining block hits
local function remaining()
local r=0
allBlocks(function(_,_,blk)
if blk>0 then r=r+blk end
return true -- to continue
end)
return r
end
-- draw all blocks in a level. takes the color
-- from blockColors, unless col is passed.
-- bgcol is passed to clear blocks, and flash
-- to flash blocks orange
-- if a block is drawn below y=280 gameover is set
local function drawBlocks(col)
allBlocks(function(x, y, b, r, c)
color = b==0 and backgr or (col or blockColors[b])
draw.rectFill(x,y,BLCKWDT,BLCKHGT,color)
if b==0 then blocks[r][c]=-1 end
if y+BLCKHGT>=280 and b>0 then gameover=true end
return true
end)
end
-- check for all possible collisions between
-- the ball and other elements:
-- - walls,floor,ceiling
-- - paddle
-- - blocks
local function collide()
bounced=false
-- walls
if ball.x == 320-BALLRAD-2 -- right
or ball.x == BALLRAD+1 then -- left
sounds.play(5)
ball.sx=-ball.sx
bounced=true
end
if ball.y == 320-BALLRAD-2 -- bottom
or ball.y == BALLRAD+17 then -- top
sounds.play(5)
if ball.sy>0 and canLowerBlocks then
lowerBlocks=true
sounds.play(1)
redraw=true
end
ball.sy=-ball.sy
bounced=true
end
if bounced then return end
-- paddle
if ((ball.sy>0 and ball.y==300-BALLRAD-1)
or (ball.sy<0 and ball.y==300+PADLHGT+BALLRAD))
and ball.x>paddle.x
and ball.x<paddle.x+PADLWDT then
ball.sy=-ball.sy
sounds.play(6)
end
-- blocks
allBlocks(function(x,y,blk,row,col)
if blk <= 0 then return true end
if outside(
ball.x,
x-BALLRAD-1,
x+BLCKWDT+BALLRAD)
or outside(
ball.y,
y-BALLRAD-1,
y+BLCKHGT+BALLRAD) then return true end
local hit=false
-- up
if (ball.sy<0 and ball.y==y+BLCKHGT+BALLRAD)
-- down
or (ball.sy>0 and ball.y==y-BALLRAD-1) then
hit=true
ball.sy=-ball.sy
end
-- left
if (ball.sx<0 and ball.x==x+BLCKWDT+BALLRAD)
-- right
or (ball.sx>0 and ball.x==x-BALLRAD-1) then
hit=true
ball.sx=-ball.sx
end
if not hit then return true end
curBlk=blocks[row][col]
sounds.play(11-curBlk)
blocks[row][col]=curBlk-1
score=score+100
redraw=true -- dont't draw in here
levelDone=remaining()==0
return false
end)
end
---------- ball related functions ----------
local function clearBall()
draw.circleFill(ball.x,ball.y,BALLRAD,backgr)
end
local function drawBall()
if ball.y<280 then canLowerBlocks=true end
local col=fast and flash or white
draw.circleFill(ball.x,ball.y,BALLRAD,col)
end
local function moveBall(x,y)
clearBall()
ball.x=x
ball.y=y
drawBall()
end
local function animateBall()
if ball.sx==0 and ball.sy==0 then
--follow paddle before game start
if ball.x==paddle.x+25 then
return
else
moveBall(
math.floor(paddle.x+PADLWDT/2),
ball.y)
return
end
end
moveBall(ball.x+ball.sx,ball.y+ball.sy)
collide()
end
--------- paddle related functions ----------
local function clearPaddle()
draw.rectFill(paddle.x,300,PADLWDT,8,backgr)
end
local function drawPaddle()
draw.rectFill(paddle.x,300,PADLWDT,8,white)
end
local function animatePaddle()
if paddle.xs == 0 then return end
paddle.xs=paddle.xs*0.99
if math.abs(paddle.xs)<0.1 then paddle.xs=0 end
newx=paddle.x+paddle.xs
if outside(newx,1,320-PADLWDT) then return end
clearPaddle()
paddle.x=newx
drawPaddle()
end
-- parse level string and build the blocks table
local function buildLevel(l)
blocks={}
local level=levels[l]
local line={}
for i=1,#level do
c=level:byte(i)
if c==46 then -- dot separates rows
table.insert(blocks,line)
line={}
else
table.insert(line,c-48)
end
end
table.insert(blocks,line)
end
local function clearScreen()
term.setBackgroundColor(backgr)
term.clear()
draw.rectFill(0,0,320,320,backgr)
draw.rect(0,15,320,305,white)
end
local function drawStatus()
draw.text(0,0,"Score: "..score)
centerText(0, paused and "PAUSED" or " ")
rightText(0," Remaining:"..remaining())
end
local function printSplash()
local i
for i=1,#splash do
centerText(20+13*i,splash[i],flash)
end
end
local function animate(s)
sys.repeatTimer(s,function()
if paused then return end
animateBall()
animatePaddle()
end)
end
-- reset state for new level, and start timer
local function initLevel()
sys.stopTimer()
clearScreen()
printSplash()
centerText(240,"Level: "..currentLevel)
centerText(250,"Score: "..score)
wait(700)
centerText(270,"Press any key to start")
keys.wait()
clearScreen()
-- reset positions
ball = { x=160, y=295, sx=0, sy=0 }
paddle = { x=(320-PADLWDT)/2, xs=0 }
blockStartPos=0
levelDone=false
gameover=false
fast=false
buildLevel(currentLevel)
-- draw all the things
drawBlocks()
drawBall()
drawPaddle()
drawStatus()
animate(5)
end
local function keyInput()
local state, mod, code = keys.poll()
if state == keys.states.pressed then
if not paused and code == keys.left then
if paddle.xs>0 then paddle.xs=-.5 end
paddle.xs=paddle.xs-1
elseif not paused and (code == keys.right or code=="]") then
if paddle.xs<0 then paddle.xs=.5 end
paddle.xs=paddle.xs+1
elseif code == keys.esc then
continue=false
elseif code == "p" then
paused=not paused
drawStatus()
elseif code == " " then
fast=not fast
animate(fast and 4 or 5)
else
if ball.sx==0 then ball.sx=0.5 end
if ball.sy==0 then ball.sy=-1 end
end
end
end
local function start()
term.loadFont("fonts/ProggyClean.fnt")
initLevel()
while continue do
if redraw then
redraw=false
drawBlocks()
drawStatus()
end
if lowerBlocks then
canLowerBlocks=false
lowerBlocks=false
score=score-100
draw.line(1,280,318,280,flash)
if not fast then drawBlocks(flash) end
drawBlocks(backgr)
draw.line(1,280,318,280,backgr)
blockStartPos=blockStartPos+10
drawBlocks()
drawStatus()
end
if levelDone then
currentLevel=currentLevel+1
initLevel()
end
if gameover then
sys.stopTimer()
clearScreen()
printSplash()
centerText(240,"GAME OVER")
centerText(250,"Score: "..score)
wait(700)
centerText(280,"Press any key to continue")
keys.wait()
currentLevel=1
score=0
initLevel()
end
keyInput()
end
end
start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment