diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-14 16:23:39 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-14 16:23:39 -0800 |
| commit | 7e66b7ae6b00a2e1652db32f01f0bf480508ef05 (patch) | |
| tree | f09689493fd820544b4cd94d4b1e941d84a24b4b /chapter31/docs | |
| parent | 3d0f3f38d9d8345b6508b3289dc94b0500c53d9e (diff) | |
| download | Simple-7e66b7ae6b00a2e1652db32f01f0bf480508ef05.tar.gz Simple-7e66b7ae6b00a2e1652db32f01f0bf480508ef05.zip | |
CLeanup Breakoutmain
Fix a half-dozen compiler bugs; various kinds of forward refs and readable types.
Allow read-only self-types; and read-only methods.
Anonymous inner functions are treated as methods.
Better error messages in many places.
Diffstat (limited to 'chapter31/docs')
| -rw-r--r-- | chapter31/docs/examples/Breakout.smp | 375 |
1 files changed, 223 insertions, 152 deletions
diff --git a/chapter31/docs/examples/Breakout.smp b/chapter31/docs/examples/Breakout.smp index e5757b45..b1d0b443 100644 --- a/chapter31/docs/examples/Breakout.smp +++ b/chapter31/docs/examples/Breakout.smp @@ -1,6 +1,6 @@ // 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. -int WIDTH = 1008, HEIGHT = 760, COLS = 24, ROWS = 8; +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; @@ -17,18 +17,76 @@ struct Game { 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; - int soundDepth = 1; - // 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); + // 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; }; @@ -41,11 +99,10 @@ struct Game { f64 paddle; val _paddle = { f64 x -> paddle = x < LEFT +PADDLE_HALF ? LEFT +PADDLE_HALF : - x > RIGHT-PADDLE_HALF ? RIGHT+PADDLE_HALF : + x > RIGHT-PADDLE_HALF ? RIGHT-PADDLE_HALF : x; }; - // Hit the paddle val _rebound = { -> // Where along the paddle did you hit? @@ -62,6 +119,7 @@ struct Game { }; // 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 @@ -70,13 +128,25 @@ struct Game { remaining--; // Fewer bricks to iwn if( row > depth ) depth = row; // Max depth seen // Brick-hit sound - if( sound <= 3 ) { soundDepth = row; _cue(3); } - if( back && backWall && !boosted ) { boosted = true; _cue(4); } - if( remaining == 0 ) { phase = WON; _cue(6); } + 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; @@ -89,74 +159,133 @@ struct Game { backWall = false; paused = false; sound = 0; + soundDepth = 1; paddle = 504.0; x = paddle; y = PADDLE_Y-RADIUS-6; _speed(); }; -}; -// 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 = { !Game g, int direction -> - if( g.paused || g.phase == WON || g.phase == LOST ) return 0; - g._paddle(g.paddle+direction*6.4); - if( g.phase == READY ) { g.x = g.paddle; g.y = PADDLE_Y-RADIUS-6; return 0; } - f64 seconds = 0.008; - while( seconds > 0.000000001 && g.phase == PLAY ) { - f64 dt = 2.0/g.speed; - if( dt > seconds ) dt = seconds; - seconds -= dt; - f64 oldX = g.x, oldY = g.y; - g.x += g.dx*g.speed*dt; - g.y += g.dy*g.speed*dt; - if( g.x < LEFT+RADIUS && g.dx < 0.0 ) { - g.x = 2.0*(LEFT+RADIUS)-g.x; g.dx = -g.dx; g._cue(1); - } - if( g.x > RIGHT-RADIUS && g.dx > 0.0 ) { - g.x = 2.0*(RIGHT-RADIUS)-g.x; g.dx = -g.dx; g._cue(1); - } - if( g.y < TOP+RADIUS && g.dy < 0.0 ) { - g.y = 2.0*(TOP+RADIUS)-g.y; g.dy = -g.dy; - g.backWall = true; g._cue(1); + // -------------------------------- + // 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); } - if( g.dy > 0.0 && oldY <= PADDLE_Y-RADIUS && g.y >= PADDLE_Y-RADIUS && - g.paddle-PADDLE_HALF-RADIUS <= g.x <= g.paddle+PADDLE_HALF+RADIUS ) g._rebound(); - if( g.y > BOTTOM+RADIUS ) { - g.lives--; - g.phase = g.lives == 0 ? LOST : READY; - g.backWall = false; - g.depth = 1; - g.boosted = false; - g._speed(); - g.x = g.paddle; g.y = PADDLE_Y-RADIUS-6; - g._cue(5); - break; + }; + + 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); } - for( int i = 0; i < g.bricks#; i++ ) { - if( g.bricks[i] == 0 ) continue; - f64 left = LEFT+2+(i-i/COLS*COLS)*BRICK_DX-RADIUS; - f64 top = BRICK_TOP+(i/COLS)*BRICK_DY-RADIUS; - f64 right = left+BRICK_W+2*RADIUS, bottom = top+BRICK_H+2*RADIUS; - if( left < g.x < right && top < g.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 && g.dy > 0.0 ) { - g.y = top; g.dy = -g.dy; back = true; - } else if( oldY >= bottom && g.dy < 0.0 ) { - g.y = bottom; g.dy = -g.dy; - } else if( oldX <= left && g.dx > 0.0 ) { - g.x = left; g.dx = -g.dx; - } else if( oldX >= right && g.dx < 0.0 ) { - g.x = right; g.dx = -g.dx; - } else continue; - g._brick(i,back); - break; + 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; + } - } - return 0; + + // 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. @@ -166,55 +295,6 @@ val _number = { !sys.draw.Canvas c, i64 x, i64 y, int value, int digits, int sca value /= 10; } }; -val _hud = { !sys.draw.Canvas c, Game g -> - c.text(24,22,"BREAKOUT",5,WHITE); - c.text(264,20,"SCORE",2,MUTED); - _number(c,264,40,g.score,3,4,WHITE); - c.text(424,20,"BRICKS",2,MUTED); - _number(c,424,40,g.remaining,3,4,WHITE); - c.text(584,20,"BALLS",2,MUTED); - for( int i = 0; i < g.lives; i++ ) c.fillCircle(590+23*i,50,5,CYAN); - c.text(714,20,"SPEED",2,MUTED); - int speed = g.speed; - _number(c,714,40,speed,3,4,g.boosted ? COLORS[0] : WHITE); - c.text(838,40,g.boosted ? "BOOST!" : "NORMAL",3,g.boosted ? COLORS[0] : MUTED); -}; -val _bricks = { !sys.draw.Canvas c, Game g -> - 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,g.boosted ? "BACK WALL BOOST ACTIVE" : "BREAK THROUGH FOR A BOOST",2,g.boosted ? COLORS[0] : MUTED); - for( int i = 0; i < g.bricks#; i++ ) { - if( g.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, Game g -> - if( g.phase != PLAY || g.paused ) { - c.fillRect(246,422,516,108,0x17263a).rect(246,422,516,108,2,0x29435a); - u8[~] title = g.paused ? "PAUSED" : g.phase == WON ? "WALL CLEARED!" : g.phase == LOST ? "GAME OVER" : "READY?"; - c.text(504-(title#*4-1)*4/2,442,title,4,WHITE); - u8[~] help = g.paused ? "P TO CONTINUE" : g.phase == WON || g.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; -}; -val _draw = { !sys.draw.Canvas c, Game g -> - c.clear(BACKGROUND); - _hud(c,g); - _bricks(c,g); - i64 paddle = g.paddle, ballX = g.x, ballY = g.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,g.boosted ? 0x672a42 : 0x224b61); - c.fillCircle(ballX,ballY,RADIUS,g.boosted ? COLORS[0] : WHITE); - _overlay(c,g); - c.text(24,738,"ARROWS / MOUSE: MOVE SPACE / CLICK: SERVE P: PAUSE R: RESTART Q / ESC: QUIT",2,MUTED); -}; // Short, fading triangle waves; the array initializer builds PCM once at startup. val _tone = { int period -> @@ -226,27 +306,7 @@ val _tone = { int period -> int handle = sys.audio.load(samples); return handle < 0 ? sys.audio.BOOP : handle; }; -struct Controls { - bool serveHeld = false, pauseHeld = false, restartHeld = false; - int lastMouse = 504; -}; -val _input = { !Game g, !Controls keys -> - val gfx = sys.gfx; - bool serve = gfx.key(' ') || gfx.button(gfx.LEFT); - bool pause = gfx.key('P'), restart = gfx.key('R'); - if( restart && !keys.restartHeld ) { g._reset(); sys.audio.stop(); } - if( pause && !keys.pauseHeld && (g.phase == READY || g.phase == PLAY) ) g.paused = !g.paused; - int direction = (gfx.key(KEY_RIGHT) ? 1 : 0)-(gfx.key(KEY_LEFT) ? 1 : 0); - int mouse = gfx.mouseX(); - if( mouse != keys.lastMouse && direction == 0 && !g.paused && (g.phase == READY || g.phase == PLAY) ) { - g._paddle(mouse*1.0); - if( g.phase == READY ) g.x = g.paddle; - } - keys.lastMouse = mouse; - if( serve && !keys.serveHeld ) g._serve(); - keys.serveHeld = serve; keys.pauseHeld = pause; keys.restartHeld = restart; - return direction; -}; + // Fast low accuracy sqrt val _sqrt = { f64 n -> @@ -256,32 +316,43 @@ val _sqrt = { f64 n -> }; val main = { u8[~][] args -> - val gfx = sys.gfx, audio = sys.audio, time = sys.time; - val c = new sys.draw.Canvas(WIDTH,HEIGHT); - val g = new Game; - if( gfx.open("Simple Breakout",c.width,c.height) != gfx.OK ) return 1; + // 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);}; - val keys = new Controls; - keys.lastMouse = gfx.mouseX(); + + // 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 = time.monotonic(), pending_ms = 0, frame_ms = last_ms; + 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 = _input(g,keys); - i64 now_ms = time.monotonic(), elapsed_ms = now_ms-last_ms; + 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 ) { _tick(g,direction); pending_ms -= STEP_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 ) { - _draw(c,g); + g._draw(c); status = gfx.present(c.pixels); frame_ms = now_ms+FRAME_ms; if( args# > 1 && ++frames == 10 ) break; |
