// Clear 24 x 8 bricks. Arrows or mouse move; Space/click serves; P pauses; // R restarts; Q/Escape exits. Any argument runs ten frames as a smoke test. u32 WIDTH = 1008, HEIGHT = 760, COLS = 24, ROWS = 8; int LEFT = 24, RIGHT = 984, TOP = 84, BOTTOM = 718; int BRICK_TOP = 152, BRICK_DX = 40, BRICK_DY = 22, BRICK_W = 36, BRICK_H = 18; int RADIUS = 5, PADDLE_HALF = 56, PADDLE_Y = 674; int READY = 0, PLAY = 1, WON = 2, LOST = 3; int KEY_LEFT = 263, KEY_RIGHT = 262; // MiniFB key codes; gfx.LEFT is a mouse button. i64 STEP_ms = 8, FRAME_ms = 16; u32 BACKGROUND = 0x0b1220, WHITE = 0xeef7ff, MUTED = 0x8aabc4, CYAN = 0x4ce0ef; val COLORS = new u32[~]{0xff5268,0xff8050,0xffb548,0xffdb58, 0x9bdd58,0x45d8b0,0x49b9f5,0x8d94ff}; val DIGITS = new u8[~][~]{"0","1","2","3","4","5","6","7","8","9"}; struct Game { u8[] bricks = new u8[COLS*ROWS]{int i -> 1;}; int score = 0, remaining = COLS*ROWS, lives = 3, depth = 1, phase = READY; bool boosted = false, backWall = false, paused = false; f64 x = 504.0, y = 663.0, dx = 0.4, dy = -0.916515138991168; // Fixed 8 ms simulation, split further to travel at most two pixels per step. // Even at maximum boosted speed a ball cannot tunnel through an 18 px brick. val _tick = { int direction -> if( paused || phase == WON || phase == LOST ) return 0; _paddle(paddle+direction*6.4); if( phase == READY ) { x = paddle; y = PADDLE_Y-RADIUS-6; return 0; } f64 seconds = 0.008; // 8ms while( seconds > 0.000000001 && phase == PLAY ) { f64 dt = 2.0/speed; if( dt > seconds ) dt = seconds; seconds -= dt; f64 oldX = x, oldY = y; x += dx*speed*dt; y += dy*speed*dt; if( x < LEFT+RADIUS && dx < 0.0 ) { x = 2.0*(LEFT+RADIUS)-x; dx = -dx; _cue(1); } if( x > RIGHT-RADIUS && dx > 0.0 ) { x = 2.0*(RIGHT-RADIUS)-x; dx = -dx; _cue(1); } if( y < TOP+RADIUS && dy < 0.0 ) { y = 2.0*(TOP+RADIUS)-y; dy = -dy; backWall = true; _cue(1); } if( dy > 0.0 && oldY <= PADDLE_Y-RADIUS && y >= PADDLE_Y-RADIUS && paddle-PADDLE_HALF-RADIUS <= x <= paddle+PADDLE_HALF+RADIUS ) _rebound(); if( y > BOTTOM+RADIUS ) { _lostBall(); break; } // Map the center into the grid of radius-expanded brick boxes. // Their overlap is smaller than one cell: at most 2 x 2 candidates. if( x <= LEFT+2 -RADIUS || x >= LEFT+2 +(COLS-1)*BRICK_DX+BRICK_W+RADIUS || y <= BRICK_TOP-RADIUS || y >= BRICK_TOP+(ROWS-1)*BRICK_DY+BRICK_H+RADIUS ) continue; int col = (x-(LEFT+2 -RADIUS))/BRICK_DX; int row = (y-(BRICK_TOP-RADIUS))/BRICK_DY; bool hit = false; for( int r = row > 0 ? row-1 : 0; r <= row && r < ROWS && !hit; r++ ) for( int c = col > 0 ? col-1 : 0; c <= col && c < COLS; c++ ) { int i = r*COLS+c; if( bricks[i] == 0 ) continue; f64 left = LEFT+2 +c*BRICK_DX-RADIUS; f64 top = BRICK_TOP+r*BRICK_DY-RADIUS; f64 right = left+BRICK_W+2*RADIUS; f64 bottom= top +BRICK_H+2*RADIUS; if( left < x < right && top < y < bottom ) { bool back = false; // Resolve the face crossed this step, using the previous center. // Vertical faces win a simultaneous corner contact. if( oldY <= top && dy > 0.0 ) { y = top; dy = -dy; back = true; } else if( oldY >= bottom && dy < 0.0 ) { y = bottom; dy = -dy; } else if( oldX <= left && dx > 0.0 ) { x = left; dx = -dx; } else if( oldX >= right && dx < 0.0 ) { x = right; dx = -dx; } else continue; // Missed all bricks _brick(i,back); hit = true; break; } } } return 0; }; // Several sound things might happen in this event, pick one int sound = 0; // Increasing priority: wall, paddle, brick, boost, miss, win. val _cue = { int snd -> if( snd > sound ) sound = snd; }; // Ball speed is a fraction of deepest brick hit, plus backwall boost f64 speed; val _speed = { -> speed = (320.0+35.0*(depth-1))*(boosted ? 1.65 : 1.0); }; // Clamp paddle to wall sides f64 paddle; val _paddle = { f64 x -> paddle = x < LEFT +PADDLE_HALF ? LEFT +PADDLE_HALF : x > RIGHT-PADDLE_HALF ? RIGHT-PADDLE_HALF : x; }; // Hit the paddle val _rebound = { -> // Where along the paddle did you hit? f64 offset = (x-paddle)/PADDLE_HALF; if( offset < -1.0 ) offset = -1.0; if( offset > 1.0 ) offset = 1.0; dx = 0.88*offset; // A near-center hit still drifts sideways instead of repeating forever. if( -0.12 < dx < 0.12 ) dx = dx < 0.0 ? -0.12 : 0.12; dy = -_sqrt(1.0-dx*dx); // Bounce upwards speed y = PADDLE_Y-RADIUS; // Reset ball hit to top of paddle backWall = false; // Did not hit back wall _cue(2); // Paddle bounce sound }; // Hit a brick? int soundDepth; // Which brick depth for the boop val _brick = { int index, bool back -> if( bricks[index] == 0 ) return 0; // No brick to hit! bricks[index] = 0; // Clear brick int row = ROWS-index/COLS; // Get brick row score += row; // Score by row remaining--; // Fewer bricks to iwn if( row > depth ) depth = row; // Max depth seen // Brick-hit sound if( sound <= 3 ) { _cue(3); soundDepth = row; } if( back && backWall && !boosted ) { _cue(3); boosted = true; } if( remaining == 0 ) { _cue(6); phase = WON; } _speed(); return 0; }; // Missed ball with paddle val _lostBall = { -> lives--; phase = lives == 0 ? LOST : READY; backWall = false; depth = 1; boosted = false; _speed(); x = paddle; y = PADDLE_Y-RADIUS-6; _cue(5); }; // Reset for another round val _reset = { -> for( int i = 0; i < bricks#; i++ ) bricks[i] = 1; score = 0; remaining = COLS*ROWS; lives = 3; depth = 1; phase = READY; boosted = false; backWall = false; paused = false; sound = 0; soundDepth = 1; paddle = 504.0; x = paddle; y = PADDLE_Y-RADIUS-6; _speed(); }; // -------------------------------- // Drawing code val _hud = { !sys.draw.Canvas c -> c.text(24,22,"BREAKOUT",5,WHITE); c.text(264,20,"SCORE",2,MUTED); _number(c,264,40,score,3,4,WHITE); c.text(424,20,"BRICKS",2,MUTED); _number(c,424,40,remaining,3,4,WHITE); c.text(584,20,"BALLS",2,MUTED); for( int i = 0; i < lives; i++ ) c.fillCircle(590+23*i,50,5,CYAN); c.text(714,20,"SPEED",2,MUTED); int ispeed = speed; _number(c,714,40,ispeed,3,4,boosted ? COLORS[0] : WHITE); c.text(838,40,boosted ? "BOOST!" : "NORMAL",3,boosted ? COLORS[0] : MUTED); }; val _bricks = { !sys.draw.Canvas c -> c.fillRect(LEFT-4,TOP-4,RIGHT-LEFT+8,4,MUTED); c.fillRect(LEFT-4,TOP,4,BOTTOM-TOP,0x29435a); c.fillRect(RIGHT,TOP,4,BOTTOM-TOP,0x29435a); c.text(376,108,boosted ? "BACK WALL BOOST ACTIVE" : "BREAK THROUGH FOR A BOOST",2,boosted ? COLORS[0] : MUTED); for( int i = 0; i < bricks#; i++ ) { if( bricks[i] == 0 ) continue; int row = i/COLS; i64 x = LEFT+2+(i-row*COLS)*BRICK_DX, y = BRICK_TOP+row*BRICK_DY; c.fillRect(x,y,BRICK_W,BRICK_H,COLORS[row]); c.fillRect(x,y,BRICK_W,2,WHITE); } }; val _overlay = { !sys.draw.Canvas c -> if( phase != PLAY || paused ) { c.fillRect(246,422,516,108,0x17263a).rect(246,422,516,108,2,0x29435a); u8[~] title = paused ? "PAUSED" : phase == WON ? "WALL CLEARED!" : phase == LOST ? "GAME OVER" : "READY?"; c.text(504-(title#*4-1)*4/2,442,title,4,WHITE); u8[~] help = paused ? "P TO CONTINUE" : phase == WON || phase == LOST ? "R TO PLAY AGAIN" : "SPACE OR CLICK TO SERVE"; c.text(504-(help#*4-1)*2/2,490,help,2,CYAN); } return 0; }; // With this Canvas, we draw into the blank canvas, then "present". The // underlying graphics lib copies the bits, and we are then free to clear // and draw again while the old bits continue to be presented. val _draw = { !sys.draw.Canvas c -> c.clear(BACKGROUND); _hud(c); _bricks(c); i64 paddle = paddle, ballX = x, ballY = y; int glowRadius = RADIUS+3; c.fillRect(paddle-PADDLE_HALF,PADDLE_Y,2*PADDLE_HALF,12,CYAN); c.fillRect(paddle-PADDLE_HALF+4,PADDLE_Y+2,2*PADDLE_HALF-8,2,WHITE); c.fillCircle(ballX,ballY,glowRadius,boosted ? 0x672a42 : 0x224b61); c.fillCircle(ballX,ballY,RADIUS,boosted ? COLORS[0] : WHITE); _overlay(c); c.text(24,738,"ARROWS / MOUSE: MOVE SPACE / CLICK: SERVE P: PAUSE R: RESTART Q / ESC: QUIT",2,MUTED); }; // Served and still holding serve key bool serveHeld; // Paused and still holding pause key bool pauseHeld; // Restart and still holding restart key bool restartHeld; // Last mouse position; track moving mouse int lastMouse; // Read and act on the controls val _input = { -> val gfx = sys.gfx; bool serve = gfx.key(' ') || gfx.button(gfx.LEFT); bool pause = gfx.key('P'); bool restart= gfx.key('R'); // Keyboard direction and mouse position int direction = (gfx.key(KEY_RIGHT) ? 1 : 0)-(gfx.key(KEY_LEFT) ? 1 : 0); int mouse = gfx.mouseX(); // Restart requested restartHeld = debounce( restart, restartHeld, { -> _reset(); sys.audio.stop(); } ); // Playing or ready to play game? if( phase == READY || phase == PLAY ) { // Flip pause state pauseHeld = debounce( pause, pauseHeld, { -> paused = !paused; } ); // If no key and mouse moved, set paddle to mouse if( mouse != lastMouse && direction == 0 && !paused ) { _paddle(mouse*1.0); // Set paddle to mouse // If READY and not PLAY, ball tracks paddle directly if( phase == READY ) x = paddle; } lastMouse = mouse; } // Serve requested serveHeld = debounce( serve, serveHeld, _serve ); // Return key direction, so the tick tracking can slide paddle smoothly return direction; }; val debounce = { bool b, bool bHeld, { int } action -> if( b && !bHeld ) // Action requested and repeating action(); // Do action b; // Dont action again, until 'b' is released }; // Serve the ball; give it a nominal direction and speed val _serve = { -> if( phase == READY && !paused ) { phase = PLAY; dx = 0.4; dy = -_sqrt(1.0-dx*dx); } return 0; }; }; // Fixed-width decimal labels avoid allocating new strings on every frame. val _number = { !sys.draw.Canvas c, i64 x, i64 y, int value, int digits, int scale, u32 color -> for( int i = 0; i < digits; i++ ) { c.text(x+(digits-1-i)*4*scale,y,DIGITS[value-value/10*10],scale,color); value /= 10; } }; // Short, fading triangle waves; the array initializer builds PCM once at startup. val _tone = { int period -> val samples = new i16[~2400]{int i -> int phase = i-i/period*period; int triangle = phase < period/2 ? phase : period-phase; return (triangle*4-period)*6000/period*(2400-i)/2400; }; int handle = sys.audio.load(samples); return handle < 0 ? sys.audio.BOOP : handle; }; // Fast low accuracy sqrt val _sqrt = { f64 n -> f64 r = 1.0; for( int i = 0; i < 8; i++ ) r = (r+n/r)*0.5; return r; }; val main = { u8[~][] args -> // Shortcuts val gfx = sys.gfx, audio = sys.audio; // Open a window if( gfx.open("Simple Breakout",WIDTH,HEIGHT) != gfx.OK ) return 1; // Audio setup; pre-specify a series of sounds audio.open(); // Optional: a missing audio backend leaves play silent. val sounds = new int[~]{0,_tone(120),_tone(80),audio.BOOP,audio.WIN,audio.LOSE,audio.WIN}; // Bottom to top: roughly 333..800 Hz, below the old 1000 Hz brick tone. val brickSounds = new int[~ROWS]{int i -> int period = 144-i*12; _tone(period);}; // Setup drawing board val c = new sys.draw.Canvas(WIDTH,HEIGHT); // Setup game contents val g = new Game; g._reset(); // int frames = 0, status = gfx.OK; i64 last_ms = sys.time.monotonic(), pending_ms = 0, frame_ms = last_ms; while( status == gfx.OK ) { if( gfx.key('Q') || gfx.key(gfx.ESCAPE) ) break; int direction = g._input(); i64 now_ms = sys.time.monotonic(), elapsed_ms = now_ms-last_ms; last_ms = now_ms; // Drop long stalls instead of teleporting the ball after a window drag. pending_ms += elapsed_ms < 0 ? 0 : elapsed_ms > 64 ? 64 : elapsed_ms; while( pending_ms >= STEP_ms ) { g._tick(direction); pending_ms -= STEP_ms; } if( g.sound != 0 ) { audio.play(g.sound == 3 ? brickSounds[g.soundDepth-1] : sounds[g.sound]); g.sound = 0; } if( now_ms >= frame_ms ) { g._draw(c); status = gfx.present(c.pixels); frame_ms = now_ms+FRAME_ms; if( args# > 1 && ++frames == 10 ) break; } if( status == gfx.OK ) status = gfx.wait(STEP_ms-pending_ms); } audio.close(); gfx.close(); return status == gfx.ERROR ? 1 : 0; };