Pong

Game of the Month : Pong

Pong is seen as the first home video game, released on the first video game consoles like the The Magnavox Odyssey.

Pong is also our first Game of The Month and we propose to start the adventure to make video game with it.

To build the game we will use the lua programming language available by default on the liko-12 fantasy computer. (see preview to know more about liko)

Disk (and souce code) :

pong.disk.png (image png file to save and load directly in DiskOS) -not implemented yet

pong.disk : link: http:// github (disk file) pong.lua : link: http:// github (code source only)

Manual :

pong_manual.pdf

Box cover (Front & Back) :

pong_box_cover.jpg

1: Starting with liko-12 (DiskOS)

Description:

When we start liko-12 (DiskOS), we begin with the terminal, from here we can we can type help to see some functionalities.

It is possible to create a new folder games with the command mkdir games, and go in this folder with the command cd games.

We can start directly by writing and executing a program.

Code:

We press the key Escape to go in the liko editor. It is here that we start programming with lua.

Let's write in the lua console our first code statement:

print("this is code!")

Result:

And the game title the code editor :

--PONG - GAME OF THE MONTH - JANUARY 1977 - 101 MAGAZINE

Result:

Steps:

  1. print() is a function that display what is called a string of characters (separated by " ")

  2. -- symbols are used for comments, everything on the same line is ignored

We now can save our work in the current folder with the command :

save pong

Our game is now saved in the file pong.lk12 (pong.disk).

Infos:

The String literals in lua can use single quotes ('hello') or double quotes ("hello"). To use the quote character in a string, we use an escape character which is a backslash ("Can we make \"pong\" now?").

Notes : The infos section give extended informations concerning programming with lua and anything that give a complement of knowledge in game development.

2: Initializing the game datas

Description:

The pong game consist in two players moving paddles and trying to bounce back a ball.

We need to give our game the basic datas that we will use in all our code.

Code:

We can start our program with this code:

-- game datas
-- 1
function _init()
    -- 2
    board = {
        limit_top = 1,
        limit_down = 128,
        limit_left = 1,
        limit_right = 192,
        middle_h = 128/2,
        middle_w = 192/2
    }
    -- 3
    pad = {
        height = 30,
        width = 6,
        speed = 1
    }
    -- 4
    pad1 = {
        x = 8,
        y = board.middle_h - pad.height/2
    }
    -- 5
    pad2 = {
        x = 186,
        y = board.middle_h - pad.height/2
    }
    -- 6
    ball = {
        x = board.middle_w - 2,
        y = board.middle_h - 3,
        velocity_x = 0,
        velocity_y = 0,
        angle = 0,
        speed = 3,
        size = 5
    }
end

Result:

The program do nothing except saving the datas in different tables (see infos).

Steps:

  1. The function _init() is a liko12 function that is called one time by default when we run the game. It is the best place to start (initialize) our datas before to use them later.

  2. the board datas contain everything which is related to the dimension of the board game.

  3. the pad datas concern general datas for both paddles.

  4. the pad1 data concern specific informations concerning the paddle of player 1.

  5. the pad2 data concern specific informations concerning the paddle of player 2.

  6. the ball datas concern the information of the ball.

Infos:

With lua, we can create some container of datas, or tables by giving a name to our table and giving a value to each data and separating them with a colon (,), then we can get the data we are interested in by calling the table followed by the data separated by a period (.).

For example :

table = {
    data1 = value1,
    data2 = value2,
    data3 = value3,
    data4 = value4
}
print(table.data1) -- return value1

Notes : To make it easy to read and understand the code, we use long variable names like board.limit_right, it is of course possible to simplify with shorter variables like brd.lr when readability is not a big issue.

3: The game loop and drawing the board game

Description:

For our game to keep running without stopping directly when we start it, we need a loop that keep the game going on as long as we don't ask the program to stop.

We initialize the game loop by using the functions _update(). Each loop, our game start by looking if their is a behavior or inputs that change our datas (action()), check how the new datas transform our game (check()) and then update the display of the game (draw()) which give to the player outputs of the new game state.

For now, even if the datas don't change because we have no written a behavior to the game, we can start directly with the functions we need to display the board.

Code:

Press Escape and in the code editor, we can write:

--[...]

-- action functions
-- 1
function action()
end

-- checking functions
function check()  
end

-- drawing functions
-- 2
function draw_board()
    -- 3
    rect_line(board.limit_left, board.limit_top, board.limit_right, board.limit_down, 7)

    -- 4
    for i=board.limit_top, board.limit_down, 10 do
        line(board.middle_width, i, board.middle_width, i + 6, 7)
    end
end

-- 5
function draw()
    -- 6
    clear(1)
    -- 7
    draw_board()
end

-- main loop function
-- 8
function _update()
    action()
    check()
    draw()
end

Result:

  • Press Escape and write run to test the program and use Escape key two time to stop the loop and return to the code editor.

    ( It is also possible to start directly the game from the code editor with the keys Ctrl and R pressed together)

We can now press again Escape to return to the terminal and write :

Run

When we run, we should now have the pong board drawn on our screen.

Steps:

  1. We create the main functions action() and check(). We will use them as we go later.

  2. We create a new function draw_board() which will contain the code statements to draw the game board

  3. We create a rectangle with the function rect_line(x, y, w, h, color), x and y are the coordinate of top/left of the rectangle, h and w are the width and the height, we use the board limit datas to build a surrounding rectangle with a white color.

  4. To draw the middle dotted line, we use a for loop, it create dashs using the function line(x1, y1, x2, y2, color) between the point x1,y1 and the point x2, y2. (see below for more info about for loop)

  5. We then create a main draw() function, this will be executed each loop, we generally use it to call together our drawing functions here after action and check in the _update() loop.

  6. With clear(color) we fill the screen each loop with a defined color, here 1 is black.

  7. We then call the draw_board() function from the draw() function which is called each loop.

  8. Finally we set the _update() loop where we call action, check and draw main functions.

Infos:

for init,max/min value, increment
do
   statement(s)
end

To make a loop with lua we can use the key for, we init the loop with a first value, we set the value we wish to reach and then increase (or decrease) the init value until we reach the final value. At each step, the statements inside the loop are evaluated.

For example :

for i=10,1,-1 
do 
   print(i) 
end
--10, 9, 8, 7, 6, 5, 4, 3, 2, 1

Notes : We use the comment --[...] to simply signal that there is code written previously here to keep in our code. All the comments --#number are used to link to the related step. All these comments can be ignored when writing the code.

4: Drawing paddles and ball

Description:

We have the board, it is the background of our game. We need now to draw the dynamic elements of our game, the paddles and the ball.

Code:

--[...]

-- drawing functions
-- 1
function draw_paddles()
    -- 2
    rect(pad1.x, pad1.y, pad.width, pad.height, 7)
    rect(pad2.x - pad.width, pad2.y, pad.width, pad.height, 7) 
end

-- 1
function draw_ball()
    -- 3
    rect(ball.x, ball.y, ball.size, ball.size, 7)
end

function draw()
    --[...]

    -- 4
    draw_paddles()
    draw_ball()
end

--[...]

Result:

We now have paddles and ball drawn in the game.

Steps:

  1. We create two new functions draw_paddles() and draw_ball().

  2. We use for drawing the paddle the function rect(x, y, w, h, color), which is a similar function that rect_line used before, but fill entirely the rectangle with the color.

  3. For the ball, we do the same than for the paddles, we use the rect() function to make a square with the height and width equal the the ball size.

  4. Finally we add the two functions inside the draw() main function to call them at each frame of the game loop.

Infos:

In lua, the main arithmetic operators are (+) addition, (-) subtraction or negation, (*) multiplication and (/) division.

5: Paddles movements by giving control to the players

Description:

We have the differents elements of the game drawn, what we need now is to update the data dynamically either by taking input from the player that will change the datas or by modifying the datas directly each loop. Each time a data is modified, our previous code (through the draw function) will update the game display.

Code:

We can add to in our code the necessary behavior to give control to the player.

--[...]

-- action functions
-- 1
function move_paddles()

    -- 2
    if btn(2,1)
    -- 3
    and pad1.y > board.limit_top + 1
    then
        -- 4
        pad1.y = pad1.y - pad.speed

    -- 2
    elseif btn(3,1)
    -- 3
    and pad1.y + pad.height < board.limit_down
    then
        -- 4
        pad1.y = pad1.y + pad.speed
    end

    -- 5
    if btn(2,0)
    and pad2.y > board.limit_top + 1
    then
        pad2.y = pad2.y - pad.speed
    elseif btn(3,0)
    and pad2.y + pad.height < board.limit_down
    then
        pad2.y = pad2.y + pad.speed
    end
end

-- 6
function move_ball()
    ball.x = ball.x + ball.velocity_x
    ball.y = ball.y + ball.velocity_y
end

-- 7
function action()
    move_paddles()
    move_ball()
end

--[...]

Result:

We can now control the paddles by pressing up/down buttons of player 1 and player 2. (arrow up/down and e/d by default)

Steps:

  1. We create a new function move_paddles().

  2. We use a conditional statement if to check if a button is pressed. We test a button with the function btn(button, player). (button up is 2, button down is 3, player 1 is 0, player 2 is 1)

  3. With logical and relational operators (see infos) we check if the paddle coordinates are not going out of the board limits, limit up when the paddle go up, limit down when the paddle go down.

  4. If a check is true (button pressed and limit not reached by paddle) then the y coordinate of the paddle is modified accordingly with the paddle speed.

  5. We do the same as paddle 1 for paddle 2 (step 2, 3, 4).

  6. We create a new function move_ball() that will update each loop the coordinates of the ball with the velocities, but nothing will happen yet, we will see in the next part how to set the ball velocities (angle + speed).

  7. We then call the function move_paddles() and move_ball() from the main function action() which is called before the draw() function at each step (frame) of the main game loop.

Infos:

When using relational and logical operator, the return value has boolean type, which is either false or true.

It is particular useful when doing conditional test : if something is true then do something.

With lua there is different relational operators:

  • < check if the value on the left is less than the value on the right

  • > check if the value on the left is greater or equal to the value on the right

  • <= check if the value on the left is lesser or equal to the value on the right

  • >= check if the value on the left is greater or equal to the value on the right

  • == check if two values are equal

  • ~= check if two values are not equal

There is also logical operators:

  • and check if all value are true

  • or check if at least one value is true

  • not change the value to it's opposite

Finally, the type nil is a non-value.

Examples:

    print(true and true)     --> true
    print(nil and true)      --> false
    print(false and true)    --> false
    print(true or true)      --> true
    print(false or true)     --> true
    A = 3
    B = 5
    A == B  --> false
    A ~= B  --> true
    A < B  --> true
    A > B  --> false

6: Preparing and moving the ball

Description:

We can now focus on the ball. This time instead to wait for player to give input, the ball datas is automatically modified at each loop. Because the ball datas will be modified, we also want a method to reset the ball to it's starting value. We need to consider two elements for the ball movement, an angle and a speed, giving us a velocity (We can see it as a direction movement, the speed and angle of the ball). Let's do some math (But don't worry, the computer do the math).

Code:

We can add this code pieces inside our existing code.

-- [...]

-- game datas
function _init()
    -- [...]

    -- 1
    angles1 = {0.2, 0.1, 0.0, 0.9, 0.8}
    angles2 = {0.3, 0.4, 0.5, 0.6, 0.7}

    -- 8
    reset_ball()
end

-- setup functions
-- 2
function reset_ball()
    -- 3
    pad1.y = board.middle_height - pad.height/2
    pad2.y = board.middle_height - pad.height/2
    -- 4
    ball.x = board.middle_width - 2
    ball.y = board.middle_height - 3
    -- 5
    local random_nb = rand()
    if random_nb < 0.5 then
        ball.angle = 0.5
    else
        ball.angle = 0
    end
    -- 6
    set_velocities()
end

-- 6
function set_velocities()
    -- 7
    ball.velocity_x = (math.floor(math.cos(math.rad(360 * ball.angle)) * 100) / 100) * ball.speed
    ball.velocity_y = (math.floor(math.sin(math.rad(360 * ball.angle)) * 100) / 100) * ball.speed
end

-- [...]

Result:

When running the game the ball move in direction of paddle 1, angle (0.5) or paddle 2, angle (0) and then vanish.

Steps:

  1. We start by adding in the _init() function the differents angles the ball can take in the case of collision with paddle1 (angles1) or collision with paddle2 (angles2), (see infos for how angles works here)

  2. We create a new reset_ball() function that will be useful each time we want the ball to take back the initial values..

  3. In the function we first reset the paddle position to their original state.

  4. We then set the value of x and y of the ball to the middle.

  5. We use the function rand() to get a random number between 0.0 and 1.0. If the random number (random_nb) is less than 0.5 (fifty-fifty chance), the ball angle is set to 0.5 (go on the left), else it keep 0.0 (go on the right).

  6. We then call the set_velocities() function that we will create next.

  7. The ball.velocity for x and y is updated with the current angle of the ball. (see infos to understand the math behind it).

  8. We add the reset_ball() function at the end of the _init() function to be called one time when we start the game, it is a way to take advantage of a starting random angle.

Infos:

Lua provide a library of mathematical functions that are really useful. For example rand() (or math.random) is one of them.

Here how the angles work in our game :

We uses an input range of 0.0 to 1.0 to represent the angle, as a percentage of the unit circle.

On this example, a ball with an angle of 0.90 go in a direction top-right and a ball with an angle of 0.4 go down-left. An angle of π (3.1415) radians (180 degrees) corresponds to an angle of 0.5 (360 * 0.5 = 180).

  • We use the math function math.cos() and math.sin() to find the coordinates of x and y of the angle and help us here for the velocity of our ball.

  • We need to convert first our angle in radian with math.rad() our angle in degree.

  • To get the angle in degree we multiply the ball angle to 360. (our range 0.0 to 1.0 is a percent of a full circle of 360 degree)

  • Finally we use (math.floor(result*100) / 100) to have a number that have a maximum of two digits after the decimal.

Notes: It is possible to use the command print(rand()) in the lua console to see how this function works, it return a decimal number between 0 and 1.

7: The ball bounce against the walls

Description:

Before to take care of the collision between the ball and the paddles, we can add the behavior of the ball bouncing with the walls

Code:

-- [...]

-- checking functions
function ball_bounce()
    if ball.y <= board.limit_top + 1 then 
        ball.velocity_y = -ball.velocity_y
    end
    if ball.y + ball.size > board.limit_down - 1 then
        ball.velocity_y = -ball.velocity_y
    end
end

function check()  
    ball_bounce()
end

-- [...]

Result:

The game now check each loop if the ball has not reached the board limit on the y axis. A visual result will be produced when we complete the paddle collision and update the angles.

Steps:

  1. Create a ball_bounce() function that check if the ball coordinate on the y axis is less or greater than the board limits up and down.

    If the limits are crossed, the ball velocity of y is inverted.

  2. We call the ball_bounce() function from the check() function to check at each loop the ball y coordinate.

8: The ball collide with the paddles

Description:

At this point, we want to focus on the most complex part of the program, the collision between the paddles and the ball. We need to define different conditional checks between the ball position and the paddles and, if collision detected, define a new angle for the ball. This part focus on the detection of a collision between the ball and the paddles, the next will focus in updating the ball angle.

Code:

-- [...]

-- game datas
-- 1
function _init()
    -- [...]
    collision = 0
end

-- checking functions
-- 2
function ball_collision()
    -- 10
    if collision > 0 then collision = collision - 1 end

    -- 3
    if ball.x < pad1.x + pad.width
    -- 4
    and ball.y + ball.size >= pad1.y
    -- 5
    and ball.y < pad1.y + pad.height
    -- 6
    and collision == 0
    -- 7
    then
        ball.x = pad1.x + pad.width
        set_angle(1)
    end

    -- 8
    if ball.x + ball.size > pad2.x - pad.width
    and ball.y + ball.size >= pad2.y
    and ball.y < pad2.y + pad.height
    and collision == 0
    then
        ball.x = pad2.x - pad.width - ball.size
        set_angle(2)
    end

    -- 9
    if ball.x < pad1.x + pad.width
    or ball.x + ball.size > pad2.x - pad.width
    then
        collision = 10
    end
end

-- 11
function check()
    -- [...]
    ball_collision()
end

-- [...]

Result:

If we run the program now, we have a run time error. Because the function update_angle() have not been created nor defined yet. The next part will complete the collision behavior between the ball and the paddle.

Steps:

  1. First we add a new variable flag collision in the _init() function and set it to 0.

  2. We create a new function ball_collision().

  3. For paddle 1, we check here if the x coordinate of the ball is less or equal than the x coordinate of the paddle.

  4. We check also if the y coordinate (+ ball size to have the y below point ) is greater or equal that the top coordinate of the paddle.

  5. We check also if the y coordinate (top) of the ball is less than the y coordinate of the paddle (+ paddle height to have the below y point).

  6. Finally we check is there have already been a collision with this same paddle before, 0 mean than there was not.

  7. If all this previous condition are true, it is a collision and we then call the function update_angle() with 1 as parameter to designate the paddle 1 (2 for paddle 2).

  8. We use the same method since step 3 for the paddle 2. (we do the checks with the right x point by adding the ball size)

  9. To avoid multiple collision between the paddle and the ball, we do a little trick, we check when the paddle collide with the paddle and we give a value (something like 10 give enough time for the ball to go away from the paddle) to the collision flag.

  10. If collision is more than 0, the ball and paddles won't collide, we need to decrease collision until it reach 0 to be able to collide again. Each loop, the collision flag decrease by 1.

  11. We add the ball_collision() call in the function check() to be sure to check each loop the coordinates of the ball with those of paddles.

Infos:

Errors can be of two types in lua:

  • Syntax errors : Occur when the operators and expressions are used in an improper way. (for example : a = "ee + 2)

  • Run time errors : When the program run without syntax error, but a mistake occur when the program running. (for example: a function which have not been defined is called)

Notes : Sometimes we use tricks (or hacks) to experiment ideas in our programs and try to have a particular effect that could solve a problem, there is not one perfect solution to every problems, there is better solution and bad solutions, but part of learning and to create program is not to be afraid to change the code and make your own experiments without fear to make mistakes, mistakes are part of the learning process and are not supposed to be avoid but understood. Experience come with many mistakes.

9: The ball angle is updated dynamically

Description:

We need now to add a big chunk of code to evaluate and define the new angle for our ball after it collide with the paddle. What we want is a way to define an angle related to where the ball collide on the paddle. We choose to cut our paddle in piece and check the ball on which piece the ball collide.

Code:

-- [...]

-- setup functions
-- 1
function set_angle(p)
    -- 2
    if p == 1 then
        -- 3
        if ball.y + ball.size >= pad1.y
        and ball.y + ball.size < pad1.y + (pad.height *  1/10)
        then
            -- 4
            ball.angle = angles1[5]
        -- 5
        elseif ball.y + ball.size >= pad1.y + (pad.height *  1/10)
        and ball.y < pad1.y + (pad.height *  4/10)
        then
            ball.angle = angles1[4]
        elseif ball.y >= pad1.y + (pad.height *  4/10)
        and ball.y < pad1.y + (pad.height *  6/10)
        then
            ball.angle = angles1[3]
        elseif ball.y >= pad1.y + (pad.height *  6/10)
        and ball.y < pad1.y + (pad.height *  9/10)
        then
            ball.angle = angles1[2]
        elseif ball.y >= pad1.y + (pad.height *  9/10)
        and ball.y < pad1.y + pad.height
        then
            ball.angle = angles1[1]
        end
    end

    -- 6
    if p == 2 then
        if ball.y + ball.size >= pad2.y
        and ball.y + ball.size < pad2.y + (pad.height *  1/10)
        then
            ball.angle = angles2[5]
        elseif ball.y + ball.size >= pad2.y + (pad.height *  1/10)
        and ball.y < pad2.y + (pad.height *  4/10)
        then
            ball.angle = angles2[4]
        elseif ball.y >= pad2.y + (pad.height *  4/10)
        and ball.y < pad2.y + (pad.height *  6/10)
        then
            ball.angle = angles2[3]
        elseif ball.y >= pad2.y + (pad.height *  5.5/10)
        and ball.y < pad2.y + (pad.height *  9/10)
        then
            ball.angle = angles2[2]
        elseif ball.y >= pad2.y + (pad.height *  9/10)
        and ball.y < pad2.y + pad.height
        then
            ball.angle = angles2[1]
        end
    end
    -- ?
    set_velocities()
end

-- [...]

Result:

When we run the game, the paddle collide with the ball and depending on the position the ball collide on the paddle, the ball bounce with a different angle. When reaching the wall limit top and down, the ball bounce.

Steps:

  1. We create a new function set_angle().

  2. We first check if the new angle is related to the paddle 1 (if p == 1) or the paddle 2 (if p == 2). (see infos)

  3. Then we check each time if the ball position is between two points of the paddle, in the first case we check if the ball collide between the up point of the paddle and the 1/10 of the paddle height.

  4. If the ball collide at this position, then take a particular angle from the table angles1.

  5. Repeat this process for all along the paddle.

  6. Repeat the previous steps in the case the ball collide with the paddle 2.

  7. Then finally, update the new velocities with the new angle by calling the function set_velocities().

Infos:

function name( arg1, arg2..., arg#)    
    code applied to arguments
    return result
end

name(a, b..., etc) --call the function name with parameters

A function with lua can be defined with arguments that are passed when the function is called.

For example : When we call the function print("hello, world!") we pass to the function a string parameter "hello, wold!".

10: displaying score and game over

Description:

When the paddle miss to catch a ball, the ball is lost. We need to have a way to keep the game going by checking if the ball is out of the board, to keep track of the score, to display it and give a end to the game. For example by reaching a maximum score.

Code:

-- [...]

-- game datas
-- 1
function _init()
    -- [...]
    game = true
    max_score = 10
    score1 = 0
    score2 = 0
end

-- setup functions
-- 2
function reset_game()
    game = true
    max_score = 10
    score1 = 0
    score2 = 0
    reset_ball()
end

-- [...]

-- checking functions
-- 3
function ball_score()
    -- 4
    if ball.x < board.limit_left then
        score2 = score2 + 1
        reset_ball()
    end
    if ball.x > board.limit_right then
        score1 = score1 + 1
        reset_ball()
    end

    -- 5
    if score1 == max_score
    or score2 == max_score
    then
        game = false
    end
end

-- 6
function check()
    -- [...]
    ball_score()   
end

-- [...]

-- drawing functions
-- 7
function draw_score()
    color(7)
    print(score1, board.limit_right * 2/10, 10)
    print(score2, board.limit_right * 8/10, 10)
end

--8
function draw()
    -- [...]
    draw_score()
end

-- main loop function
-- 9
function _update()
    if game == true then
        action()
        check()
        draw()
    else
        game_over()
    end
end

-- game over event
-- 10
function game_over()
    clear(1)
    draw_score()
    -- 11
    if score1 > score2 then
        print("PLAYER 1 WIN!", 70, 50)
    else
        print("PLAYER 2 WIN!", 70, 50)
    end

    -- 12
    print("Press Button 1 to play again", 40, 60)

    -- 13
    if btn(4, 0) or btn(4, 1) then
        reset_game()
    end
end

Result:

Except the sounds, the game is now complete, the paddle can score and win the game.

Steps:

  1. We add to the _init() function the datas we will need for the score system, one variable for each player, score1 and score2, a max score variable to reach. Also we add a flag to know when the game is finished.

  2. We create a function reset_game() which set to default all the new variable we added, and reset the ball to the center. This function will be used to restart the game when needed.

  3. We create a function ball_score().

  4. We check if the ball x position is out of the screen on the board limit left and right. If the ball go beyond, it is a goal. We add 1 point to the score of the related player.

  5. We check if the score of player 1 or player 2 have reach the maximum score, if yes, the game is finished.

  6. We don't forget to add the ballscore() checking function to the main function check() which is called from the game loop updated().

  7. We can create a drawing function draw_score() that will display the two score in the game.

  8. We add the draw_score() to the main function draw() to connect it to the game loop.

  9. We change the game loop _update() function with a conditional statement. The main functions action, check and draw are called only if the game flag is true. Else, the game call the function game_over().

  10. We create the game_over() function, it's purpose is to display a message and propose the player to restart the game.

  11. We display the winner by checking which of score1 or score2 is the biggest.

  12. If the button 4 of player 1 or player 2 is pressed, the game_reset() function is called and restart the game.

11: add sound to the game

For now, the sound is not added to the game engine, this part will be updated when available.

Last updated