summaryrefslogtreecommitdiff
path: root/docs/chapter31/examples/Breakout.smp
blob: b1d0b4437a2df7f83bc28e8817fc64a67ffab243 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
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;
};