Fable Pong

Two player Pong clone using HTML5 canvas

This demo shows a Fable implementation of Pong, which is also part of the F# Advent Calendar in English 2016. You can find the full source code on GitHub and learn more in this post.

Left Paddle: w => up; s => down | Right Paddle: o => up; l => down

Defining the model

When thinking about the Pong game, there are basically three types of models:

  1. Paddles: The paddles have got a position and a specific size.
  2. Ball: The ball has got a position and a size as well. But it has also got speed and an angle.
  3. Game status: Some kind of storage containing information about the current score.
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
type PongElement = {
									x : float;
									y : float;
									width : float;
									height : float;
}

type BallElement = {
									element : PongElement;
									speed : float;
									angle : float;
}

type GameStatus = {
									scoreLeft : int;
									scoreRight : int;
									active : bool;
}

Controlling the paddles

To control a paddle there are only two functions necessary. A canMove-function, to indicate whether a paddle can move in a certain direction, and an actual move-function to move the paddle. The parameter direction is a tuple (int * int). When the first parameter of the tuple is set to 1, we want the paddle to move up. When the second parameter of the tuple is set to 1, we want the paddle to move down. By using pattern matching we can check which value of the tuple is set to 1. Since every object is immutable, we either return a copy of the current paddle with its new Y-position (line 10 and 11) or simply return the input-paddle if no movement is allowed.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
let canMove direction paddle =
									match direction with
    | (1, _) -> paddle.y > 0.
    | (_, 1) -> paddle.y + paddle.height < h
    | _ -> false

let move direction paddle =
									if paddle |> canMove direction then
									match direction with
        | (1, _) -> { paddle with y = paddle.y - 5. }
        | (_, 1) -> { paddle with y = paddle.y + 5. }
        | _ -> paddle
									else
									paddle

Collision detection

This discriminated union describes the whole collision system of the game: There can be no collision (None), the Top or Bottom of the canvas may be hit, the Left or Right part of the canvas may be hit (so a player scored) or finally a paddle was hit (LeftPaddle & RightPaddle). The following function takes the paddles and the ball as input parameters and returns the found type of collision.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
type Collision =
    | None
    | Top
    | Bottom
    | Left
    | Right
    | LeftPaddle
    | RightPaddle

With this function, implementing a final collision-function to determine the new angle of the ball is straight forward again. (Thanks to pattern matching)

 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: 
let checkCollision leftPaddle rightPaddle ball =
									let hitTop = ball.element.y <= 0.
									let hitBottom = ball.element.y + ball.element.height >= h
									let hitLeft =
									ball.element.x <= leftPaddle.x 
									&& ((ball.element.y >= leftPaddle.y 
									&& ball.element.y <= leftPaddle.y + leftPaddle.height) 
									|> not)
									let hitRight =
									ball.element.x + ball.element.width >= rightPaddle.x + rightPaddle.width 
									&& ((ball.element.y >= rightPaddle.y 
									&& ball.element.y <= rightPaddle.y + rightPaddle.height) 
									|> not)
									let hitLeftPaddle =
									ball.element.x <= leftPaddle.x + leftPaddle.width 
									&& ball.element.y >= leftPaddle.y 
									&& ball.element.y <= leftPaddle.y + leftPaddle.height
									let hitRightPaddle =
									ball.element.x + ball.element.width >= rightPaddle.x 
									&& ball.element.y >= rightPaddle.y 
									&& ball.element.y <= rightPaddle.y + rightPaddle.height
									match (hitTop, hitBottom, hitLeft, hitRight, hitLeftPaddle, hitRightPaddle) with
    | (true, _, _, _, _, _) -> Top
    | (_, true, _, _, _, _) -> Bottom
    | (_, _, true, _, _, _) -> Left
    | (_, _, _, true, _, _) -> Right
    | (_, _, _, _, true, _) -> LeftPaddle
    | (_, _, _, _, _, true) -> RightPaddle
    | _ -> None

When hitting either the top or the bottom of the canvas, we negate the value of the angle (angle of incidence is equal to the angle of reflection). When hitting the left or right part of the canvas, we simply keep the input angle, since evaluating the score isn’t done here. To actually calculate the angle when a paddle is hit, we use yet another function.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let calculateAngle paddle hitRightPaddle determineAngle ball =
									let relativeIntersectY = (paddle.y + (paddle.height / 2.)) - ball.element.y
									let normalizedRelativeIntersectionY = (relativeIntersectY / (paddle.height / 2.))
									if normalizedRelativeIntersectionY = 0. && hitRightPaddle then
									Math.PI
									else
									normalizedRelativeIntersectionY |> determineAngle

For the calculation, we determine the relative intersection where the ball hit the paddle. Afterwards we normalize that value. So for example, if the paddle is 20 pixels high, that value will be between -10 and 10. Therefore we can dynamically calculate the angle depending on the impact. As seen in the collision function, the determineAngle parameter of this calculation-function is a function itself. Depending on which paddle is hit, we have to use a slightly modified calculation of the final angle. As you can see in line 5, we’ve also got a special case we have to deal with. If the right paddle got hit in the exact center, so normalizedRelativeIntersectionY = 0. && hitRightPaddle, we will have to return Pi as the new angle, since the radiant value of Pi is equal to 180°.

 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: 
let collision leftPaddle rightPaddle ball =
									match ball |> checkCollision leftPaddle rightPaddle with
    | None -> ball.angle
    | Top | Bottom -> -ball.angle
    | Left | Right -> ball.angle
    | LeftPaddle -> ball 
									|> calculateAngle leftPaddle false 
                        (fun intersection -> 
									intersection * (5. * Math.PI / 12.))
    | RightPaddle -> ball 
									|> calculateAngle rightPaddle true 
                        (fun intersection -> 
									Math.PI - intersection * (5. * Math.PI / 12.))

let moveBall angle ball = {
									element = { x = ball.element.x + ball.speed * cos angle;
									y = ball.element.y + ball.speed * -sin angle;
									width = ball.element.width;
									height = ball.element.height };
									speed = ball.speed + 0.005;
									angle = angle;
}

let checkGameStatus leftPaddle rightPaddle ball gameStatus =
									match ball |> checkCollision leftPaddle rightPaddle with
    | Left -> { gameStatus with scoreRight
									= gameStatus.scoreRight + 1; active = false }
    | Right -> { gameStatus with scoreLeft 
									= gameStatus.scoreLeft + 1; active = false }
    | _ -> gameStatus

Game loop and rendering

 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: 
let render (w, h) leftPaddle rightPaddle ball gameStatus  =
    (0., 0., w, h) |> Win.drawRect("black")
    (leftPaddle.x, leftPaddle.y, leftPaddle.width, leftPaddle.height)
									|> Win.drawRect("white")
    (rightPaddle.x, rightPaddle.y, rightPaddle.width, rightPaddle.height) 
									|> Win.drawRect("white")
    (w / 4., 40.) |> Win.drawText (string(gameStatus.scoreLeft)) 
									"white" "30px Arial"
    (w / 1.25 - 30., 40.) |> Win.drawText (string(gameStatus.scoreRight)) 
									"white" "30px Arial"
    (ball.element.x, ball.element.y, ball.element.width, 0., 2. * Math.PI) 
									|> Win.drawCircle("yellow")
									if gameStatus.active |> not then
        (w / 2. - 230., h / 2. + 40.) 
									|> Win.drawText "Press 'r' to start" "green" "40px Lucida Console"

let initialLeftPaddle =
    { x = 10.; y = h / 2. - 70. / 2.; width = 15.; height = 70. }
let initialRightPaddle =
    { x = w - 15. - 10.; y = h / 2. - 70. / 2.; width = 15.; height = 70. }
let initialBall =
    { element = { x = w / 2.; y = h / 2.; width = 5.; height = 5. };
									speed = 3.;
									angle = 0. }

let initialGameStatus = { scoreLeft = 0; scoreRight = 0; active = false; }

Keyboard.init()

let rec update leftPaddle rightPaddle ball gameStatus () =
									let leftPaddle = if gameStatus.active then 
									leftPaddle 
									|> move (Keyboard.leftControlsPressed()) 
									else initialLeftPaddle
									let rightPaddle = if gameStatus.active then
									rightPaddle
									|> move (Keyboard.rightControlsPressed()) 
									else initialRightPaddle
									let angle = if gameStatus.active then
									collision leftPaddle rightPaddle ball 
									else ball.angle
									let ball = if gameStatus.active then
									ball |> moveBall angle 
									else 
                   { initialBall with angle = if angle = 0. then Math.PI else 0. }
									let gameStatus = if Keyboard.rKeyPressed() = 1 then 
                        { gameStatus with active = true } 
									else
									gameStatus |> checkGameStatus leftPaddle rightPaddle ball
									render (w, h) leftPaddle rightPaddle ball gameStatus
									window.setTimeout(update leftPaddle rightPaddle ball gameStatus, 1000. / 60.)
									|> ignore

update initialLeftPaddle initialRightPaddle initialBall initialGameStatus ()
module Pong
namespace Microsoft.FSharp.Core
namespace System
val w : float

Full name: Pong.w
val h : float

Full name: Pong.h
type PongElement =
  {x: float;
   y: float;
   width: float;
   height: float;}

Full name: Pong.PongElement
PongElement.x: float
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
PongElement.y: float
PongElement.width: float
PongElement.height: float
type BallElement =
  {element: PongElement;
   speed: float;
   angle: float;}

Full name: Pong.BallElement
BallElement.element: PongElement
BallElement.speed: float
BallElement.angle: float
type GameStatus =
  {scoreLeft: int;
   scoreRight: int;
   active: bool;}

Full name: Pong.GameStatus
GameStatus.scoreLeft: int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
GameStatus.scoreRight: int
GameStatus.active: bool
type bool = Boolean

Full name: Microsoft.FSharp.Core.bool
val canMove : int * int -> paddle:PongElement -> bool

Full name: Pong.canMove
val direction : int * int
val paddle : PongElement
val move : int * int -> paddle:PongElement -> PongElement

Full name: Pong.move
type Collision =
  | None
  | Top
  | Bottom
  | Left
  | Right
  | LeftPaddle
  | RightPaddle

Full name: Pong.Collision
union case Collision.None: Collision
union case Collision.Top: Collision
union case Collision.Bottom: Collision
union case Collision.Left: Collision
union case Collision.Right: Collision
union case Collision.LeftPaddle: Collision
union case Collision.RightPaddle: Collision
val checkCollision : leftPaddle:PongElement -> rightPaddle:PongElement -> ball:BallElement -> Collision

Full name: Pong.checkCollision
val leftPaddle : PongElement
val rightPaddle : PongElement
val ball : BallElement
val hitTop : bool
val hitBottom : bool
val hitLeft : bool
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
val hitRight : bool
val hitLeftPaddle : bool
val hitRightPaddle : bool
val calculateAngle : paddle:PongElement -> hitRightPaddle:bool -> determineAngle:(float -> float) -> ball:BallElement -> float

Full name: Pong.calculateAngle
val determineAngle : (float -> float)
val relativeIntersectY : float
val normalizedRelativeIntersectionY : float
type Math =
  static val PI : float
  static val E : float
  static member Abs : value:sbyte -> sbyte + 6 overloads
  static member Acos : d:float -> float
  static member Asin : d:float -> float
  static member Atan : d:float -> float
  static member Atan2 : y:float * x:float -> float
  static member BigMul : a:int * b:int -> int64
  static member Ceiling : d:decimal -> decimal + 1 overload
  static member Cos : d:float -> float
  ...

Full name: System.Math
field Math.PI = 3.14159265359
val collision : leftPaddle:PongElement -> rightPaddle:PongElement -> ball:BallElement -> float

Full name: Pong.collision
val intersection : float
val moveBall : angle:float -> ball:BallElement -> BallElement

Full name: Pong.moveBall
val angle : float
val cos : value:'T -> 'T (requires member Cos)

Full name: Microsoft.FSharp.Core.Operators.cos
val sin : value:'T -> 'T (requires member Sin)

Full name: Microsoft.FSharp.Core.Operators.sin
val checkGameStatus : leftPaddle:PongElement -> rightPaddle:PongElement -> ball:BallElement -> gameStatus:GameStatus -> GameStatus

Full name: Pong.checkGameStatus
val gameStatus : GameStatus
val render : w:float * h:float -> leftPaddle:PongElement -> rightPaddle:PongElement -> ball:BallElement -> gameStatus:GameStatus -> unit

Full name: Pong.render
val w : float
val h : float
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
val initialLeftPaddle : PongElement

Full name: Pong.initialLeftPaddle
val initialRightPaddle : PongElement

Full name: Pong.initialRightPaddle
val initialBall : BallElement

Full name: Pong.initialBall
val initialGameStatus : GameStatus

Full name: Pong.initialGameStatus
val update : leftPaddle:PongElement -> rightPaddle:PongElement -> ball:BallElement -> gameStatus:GameStatus -> unit -> unit

Full name: Pong.update
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
Fork me on GitHub