summaryrefslogtreecommitdiff
path: root/docs/chapter31/examples/Breakout.smp
diff options
context:
space:
mode:
Diffstat (limited to 'docs/chapter31/examples/Breakout.smp')
-rw-r--r--docs/chapter31/examples/Breakout.smp364
1 files changed, 364 insertions, 0 deletions
diff --git a/docs/chapter31/examples/Breakout.smp b/docs/chapter31/examples/Breakout.smp
new file mode 100644
index 00000000..b1d0b443
--- /dev/null
+++ b/docs/chapter31/examples/Breakout.smp
@@ -0,0 +1,364 @@
+// 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;
+};