Last active
November 27, 2025 14:46
-
-
Save wkjagt/142e10aa9ee18cf24f234c94721f0366 to your computer and use it in GitHub Desktop.
Simple breakout game for picolua on the PicoCalc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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