Breakout, the game the course builds in HiSoft C, running on the Tatung Einstein: coloured brick rows, the bat and the ball on a black court
← Back to Courses
module
33

Finishing Touches

Introduction

A game that starts with the court already drawn and ends at a DOS prompt is a program; a game that tells you the keys, plays a note when you hit something, and asks whether you want another go is a thing you can hand to someone. This stage adds those, restructures main so that a whole game is one function call, and ends with the .COM on its own on a disc, which is what Breakout is.

The Title

void title()
{
    cls40();
    curat(12, 6);
    printf("B R E A K O U T");
    curat(8, 10);
    printf("[ and ] move the bat");
    curat(8, 12);
    printf("SPACE launches the ball");
    curat(8, 14);
    printf("%d lives; clear the bricks", LIVES);
    curat(8, 17);
    printf("Press any key to play");
    curat(0, 22);
    rawin();
}

Five curats and printfs and a rawin: nothing new. The number of lives comes from the #define, so that the screen is right if the number changes. rawin waits for the key and takes it, which is what is wanted here; the gets at the very end deals with what kbd() leaves during play.

Sound

beep() in hit_brick after the brick is cleared, and again when a ball is lost. It is rawout(7), the bell character, and it returns at once, so the game does not pause for it. Two beeps in the game, the same note; the sound chip can do more, and psg (S25) is there for anyone who wants it.

Colour

The Einstein starts white on dark blue, and everything so far has been drawn that way. Four tcols and a bcol (S24) change it:

#define WALLCOL 0x51
#define BATCOL 0xF1
#define BALLCOL 0xF1
#define TEXTCOL 0xE1
#define TITLECOL 0xB1
#define BACKDROP 1
#define DEFAULTBACKDROP 4

int brickcol[BRICKROWS] = { 0x91, 0xB1, 0x31, 0x71 };

bcol(BACKDROP) at the start turns the whole display black; the walls are drawn light blue, the bat and ball white, the status row and messages grey, the title yellow, and each row of bricks its own colour from brickcol - light red, yellow, green and cyan - set with tcol(brickcol[r]) as draw_bricks reaches the row. Every draw_ function sets its colour before it prints, because tcol stays set (S24) and the last thing drawn is otherwise the colour of whatever went before it. A tcol is one poke and costs nothing.

The ball is the awkward one, because it goes among the bricks. S25 said why: a character shares a colour cell with its neighbour, so a white ball drawn beside a red brick turns part of the brick white - and so does the space that rubs the ball out, and the two spaces that remove a brick, since a space has a colour too. So the ball is drawn in the colour of the row it is on:

row_colour(y) int y;
{
    if (y >= FIRSTBRICKROW && y < FIRSTBRICKROW + BRICKROWS)
        return brickcol[y - FIRSTBRICKROW];
    return BALLCOL;
}

draw_ball sets tcol(row_colour(p->oldy)) before the rub-out and tcol(row_colour(p->y)) before the ball, and hit_brick sets tcol(brickcol[r]) before its two spaces. Among the red bricks the ball is red, among the cyan ones cyan, and everywhere else white; and nothing next to it changes.

The walls become a solid block, shapedef(WALL, 0xfc, ...) - six bits set, for the six pixels of a 40-column character (S25) - now that WALL is a character code rather than '#'.

And here is why the ball has kept one column clear of the walls since S28. The video chip colours the screen in cells eight pixels wide, and a 40-column character is six wide, so most characters share a colour cell with a neighbour: colour one and the shared cell takes the new colour, and the neighbour's pixels in it change too. A white ball drawn against the blue wall would turn the wall's edge white; a red brick against it would turn it red. One blank column between them, PLAYLEFT and PLAYRIGHT, and nothing coloured ever shares a cell with the walls.

At the end the colours go back. oldcol = peek(0xfb38) at the start saves the text colour MOS was using - S23 found where it keeps it - and tcol(oldcol) restores it; bcol(DEFAULTBACKDROP) puts the blue back. A game that left the prompt white on black would be a nuisance.

A Game Is A Function

S32's main set up the game, played it and tidied up. Now the set-up and the play become play(), and main is a loop:

main()
{
    char line[40];
    int c;
    define_shapes();
    oldcol = peek(0xfb38);
    bcol(BACKDROP);
    for (;;) {
        title();
        play();
        message("Game over.  Again? (Y/N)");
        c = rawin();
        if (c != 'Y' && c != 'y')
            break;
    }
    tcol(oldcol);
    bcol(DEFAULTBACKDROP);
    cls40();
    printf("Press ENTER to finish\n");
    gets(line);
}

define_shapes runs once, because the characters stay defined, and so do the two colour lines. Everything else - the score, the lives, the level, the bricks, the bat's position - is set at the top of play, so that a second game starts clean. That is the reason the state is set in code and not in initialisers (S10): a program that started its globals at their values once, at load, could not start again. Any key but Y ends it; both cases of Y are tested because CAPS LOCK may be either way.

The Finished Program

BREAK7.C is the game: about 300 lines, a dozen functions, and the #include lines. Compiled, it is BREAK7.COM, 8,704 bytes - the 4,352 of runtime and a little over 4K of your own - and that file needs nothing else - not the compiler, not the library, not the source. Copy it to a disc, type its name, and it plays. ERA the .C files that built it if you need the room; they made it once and can make it again.

It is on the course disc as BREAKOUT.COM, with BREAKOUT.C beside it, which is the same file under the game's name.

The Code

BREAK6.C with the colour #defines and brickcol above, WALL as 202 and its shapedef, a tcol at the top of draw_court, draw_bricks (per row), draw_bat, draw_score, message, place_ball and title; row_colour and the new draw_ball:

void draw_ball(p) ball *p;
{
    tcol(row_colour(p->oldy));
    curat(p->oldx, p->oldy);
    putchar(' ');
    tcol(row_colour(p->y));
    curat(p->x, p->y);
    putchar(BALLCHAR);
}

tcol(brickcol[r]) in hit_brick before its spaces; title added before wait_launch; beep() in hit_brick after bricks[r][c] = 0; and in the lost-ball code after lives--; and main replaced by play and the new main:

void play()
{
    int c;
    paddle.width = 5;
    paddle.left = 18;
    score = 0;
    lives = LIVES;
    level = 1;
    tick = STARTTICK;
    draw_court();
    draw_bricks();
    draw_bat(&paddle);
    draw_score();
    curoff();
    while (lives > 0) {
        if (remaining == 0) {
            level++;
            tick = STARTTICK - level + 1;
            if (tick < 1)
                tick = 1;
            draw_bricks();
            draw_score();
        }
        place_ball(&b, &paddle);
        wait_launch();
        while (remaining > 0) {
            c = kbd();
            if (c == LEFTKEY)
                move_bat(&paddle, -1);
            if (c == RIGHTKEY)
                move_bat(&paddle, 1);
            move_ball(&b, &paddle);
            if (b.y >= BATROW)
                break;
            hit_brick(&b);
            draw_ball(&b);
            wait_frames(tick);
        }
        if (b.y >= BATROW) {
            curat(b.oldx, b.oldy);
            putchar(' ');
            lives--;
            beep();
            draw_score();
        }
    }
    curon();
}

main()
{
    char line[40];
    int c;
    define_shapes();
    oldcol = peek(0xfb38);
    bcol(BACKDROP);
    for (;;) {
        title();
        play();
        message("Game over.  Again? (Y/N)");
        c = rawin();
        if (c != 'Y' && c != 'y')
            break;
    }
    tcol(oldcol);
    bcol(DEFAULTBACKDROP);
    cls40();
    printf("Press ENTER to finish\n");
    gets(line);
}

The whole file is BREAKOUT.C on the course disc, and BREAK7.C in Appendix III.

Starting from

BREAK6.C, edited as above; HC BREAK7.C; BREAK7.

What you should see

A title screen, black, with the game's name in yellow and three lines of instructions. A key, and the game in colour; a beep at every brick and every lost ball; after the third, Game over. Again? (Y/N). Y, and the title again and a fresh game; N, and Press ENTER to finish; ENTER, and the prompt.

The title

The game in colour

Change One Thing

  • Take define_shapes() out of main and put it at the top of play(). Does anything change? Then put it in title() after the cls40() and see if anything changes.
  • Change c != 'Y' && c != 'y' to c != 'Y'. Press CAPS LOCK once and play. What does y do now?
  • Put beep() in move_ball at the wall bounces as well. Is it better?

Exercises

33.1 Keep a high score across games in a global, show it on the title screen, and update it after each game.

33.2 Save the high score to BREAKOUT.HI with S20's files, and read it back when the program starts, so that it survives between runs.

33.3 Add a pause: P in the game loop stops the ball until any key is pressed, with a message on the status row, and the score redrawn afterwards.

Worked solutions are in Appendix II.

When It Goes Wrong

SymptomCause
The second game starts with the old score, or no bricksSomething is set outside play(). All the state goes at its top.
The characters look wrong in the second gamedefine_shapes() moved somewhere that a cls40() follows - it is harmless, the shapes persist; look elsewhere.
Y does not restartCAPS LOCK is off and only 'Y' is tested.
Bricks beside the ball turn whiteThe ball, or a space rubbing it out, was printed in white on a brick row. row_colour.
No beepThe volume, or the emulator's sound; beep() is rawout(7).
BREAKOUT says No File on another discOnly the .COM was needed, but it was not copied.

Summary

A title with curat and printf; beep() where it counts; the game as play() with every piece of state set at its top; main a loop that asks Again?. The .COM is the game, complete in itself.

Next

S34, Reading A Real Program: CPM.LIB - the other library on the disc: command lines, files by record, and the calls into CP/M and XtalDOS, read the way S23 read EIN.LIB.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.