Super Fable Mario

Mario clone using HTML5 canvas

Mario clone, based on functional reactive sample written in Elm. The Fable version is using HTML 5 canvas to render the background and an img tag showing the Mario (using animated GIFs). You can find the full source code on GitHub.

To see how it works, use the left, right and up buttons to play. Our Mario respects no boundaries!

Mario and composable physics

We keep information about Mario in a single record type with fields that represent the current x and y coordinates (x and y), current velocity (vx and vy) and the current direction dir. The direction is used to pick the correct Mario image when rendering:

1: 
2: 
3: 
4: 
type Mario =
  { x:float; y:float;
    vx:float; vy:float;
    dir:string }

The step function of the game takes previvous Mario value and returns a new one. It is composed from 4 functions that represent different aspects of the game.

The functions that depend on keyboard take the current keyboard state as the first argument. This is represented as a tuple int*int consisting of x and y directions. For example, when the left key is pressed, the value is (-1, 0).

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
// If the Up key is pressed (y > 0) and Mario is on the ground,
// then create Mario with the y velocity 'vy' set to 5
let jump (_,y) m =
  if y > 0 && m.y = 0. then { m with vy = 5. } else m

// If Mario is in the air, then his "up" velocity is decreasing
let gravity m =
  if m.y > 0. then { m with vy = m.vy - 0.1 } else m

// Apply physics - move Mario according to the current velocities
let physics m =
  { m with x = m.x + m.vx; y = max 0. (m.y + m.vy) }

// When Left/Right keys are pressed, change 'vx' and direction
let walk (x,_) m =
  let dir = if x < 0 then "left" elif x > 0 then "right" else m.dir
  { m with vx = float x; dir = dir }

The step function takes a dir parameter representing the keyboard status and a current Mario state. It simply runs 4 components in a pipeline:

1: 
2: 
let step dir mario =
  mario |> physics |> walk dir |> gravity |> jump dir

Rendering Mario with HTML5

Now we're ready to render Mario using HTML 5 canvas! To do that, we need the width and height of the canvas and the current state of Mario. The following function fills the bottom half of the canvas with green, upper half with blue and then chooses the right Mario image. It uses helpers from the Win module, which are discussed below:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
/// Render mario on canvas
let render (w,h) (mario:Mario) =
    // Render background
    (0.,0.,w,h) |> Win.filled (Win.rgb 174 238 238)
    (0.,h-50.,w,50.) |> Win.filled (Win.rgb 74 163 41)
    // Select and position Mario
    // (walking is represented as an animated gif)
    let verb =
        if mario.y > 0. then "jump"
        elif mario.vx <> 0. then "walk"
        else "stand"
    "images/mario" + verb + mario.dir + ".gif"
    |> Win.image
    |> Win.position (w/2.-16.+mario.x,  h-50.-31.-mario.y)

Driving the game

The last thing that needs to be done is to write the main function that drives the game. The function does some initialization and then starts a recursive update function that calculates a new game state using step and renders it in a loop. The Keyboard helper module is discussed below.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
// Some initialization
Keyboard.init()
let w,h = Win.dimensions()
// Recursive function that updates the state & renders it
let rec update mario () =
    let mario = mario |> step (Keyboard.arrows())
    render (w,h) mario
    window.setTimeout(update mario, 1000. / 60.) |> ignore
// Start the game with Mario in the center
let mario = { x=0.; y=0.; vx=0.; vy=0.; dir="right" }
update mario ()

Keyboard helpers

The Keyboard module handles keydown and keyup events of the window and exposes them using the arrows property (which is a tuple int*int with -1 if the left/up key is pressed, 1 if right/down key is pressed and 0 otherwise).

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
module Keyboard =
  /// Set of currently pressed keys
  let mutable keysPressed = Set.empty

  /// Returns 1 if key with given code is pressed
  let code x =
    if keysPressed.Contains(x) then 1 else 0

  /// Update the state of the set for given key event
  let update (e : KeyboardEvent, pressed) =
      let keyCode = int e.keyCode
      let op =  if pressed then Set.add else Set.remove
      keysPressed <- op keyCode keysPressed
      null

  /// Returns pair with -1 for left or down and +1
  /// for right or up (0 if no or both keys are pressed)
  let arrows () =
    (code 39 - code 37, code 38 - code 40)

  let init () =
    document.addEventListener_keydown(fun e -> update(e, true))
    document.addEventListener_keyup(fun e -> update(e, false))

Window helpers

The Window module contains basic functionality for creating and rendering window using HTML5 canvas and moving images around. The functionsbelow fill the window, set position of image and create image.

 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: 
module Win =
  // Get the canvas context for drawing
  let canvas = document.getElementsByTagName_canvas().[0.]
  let context = canvas.getContext_2d()

  // Format RGB color as "rgb(r,g,b)"
  let ($) s n = s + n.ToString()
  let rgb r g b = "rgb(" $ r $ "," $ g $ "," $ b $ ")"

  /// Fill rectangle with given color
  let filled color rect =
      let ctx = context
      ctx.fillStyle <- U3.Case1 color
      ctx.fillRect rect

  /// Move element to a specified X Y position
  let position (x,y) (img : HTMLImageElement) =
      img.style.left <- x.ToString() + "px"
      img.style.top <- (canvas.offsetTop + y).ToString() + "px"

  let dimensions () =
    canvas.width, canvas.height

  /// Get the first <img /> element and set `src` (do
  /// nothing if it is the right one to keep animation)
  let image (src:string) =
      let image = document.getElementsByTagName_img().[0.]
      if image.src.IndexOf(src) = -1 then image.src <- src
      image
namespace Fable
namespace Fable.Core
namespace Fable.Import
module Browser

from Fable.Import
val max : a:'a -> b:'a -> 'a (requires comparison)

Full name: Mario.max
val a : 'a (requires comparison)
val b : 'a (requires comparison)
val mutable keysPressed : Set<int>

Full name: Mario.Keyboard.keysPressed


 Set of currently pressed keys
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> =
  interface IComparable
  interface IEnumerable
  interface IEnumerable<'T>
  interface ICollection<'T>
  new : elements:seq<'T> -> Set<'T>
  member Add : value:'T -> Set<'T>
  member Contains : value:'T -> bool
  override Equals : obj -> bool
  member IsProperSubsetOf : otherSet:Set<'T> -> bool
  member IsProperSupersetOf : otherSet:Set<'T> -> bool
  ...

Full name: Microsoft.FSharp.Collections.Set<_>

--------------------
new : elements:seq<'T> -> Set<'T>
val empty<'T (requires comparison)> : Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.empty
val code : x:int -> int

Full name: Mario.Keyboard.code


 Returns 1 if key with given code is pressed
val x : int
member Set.Contains : value:'T -> bool
val update : e:KeyboardEvent * pressed:bool -> 'a (requires 'a : null)

Full name: Mario.Keyboard.update


 Update the state of the set for given key event
val e : KeyboardEvent
Multiple items
val KeyboardEvent : KeyboardEventType

Full name: Fable.Import.Browser.KeyboardEvent

--------------------
type KeyboardEvent =
  interface
    inherit UIEvent
    abstract member getModifierState : keyArg:string -> bool
    abstract member DOM_KEY_LOCATION_JOYSTICK : float
    abstract member DOM_KEY_LOCATION_LEFT : float
    abstract member DOM_KEY_LOCATION_MOBILE : float
    abstract member DOM_KEY_LOCATION_NUMPAD : float
    abstract member DOM_KEY_LOCATION_RIGHT : float
    abstract member DOM_KEY_LOCATION_STANDARD : float
    abstract member altKey : bool
    abstract member char : string
    ...
  end

Full name: Fable.Import.Browser.KeyboardEvent
val pressed : bool
val keyCode : 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<_>
property KeyboardEvent.keyCode: float
val op : (int -> Set<int> -> Set<int>)
val add : value:'T -> set:Set<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.add
val remove : value:'T -> set:Set<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.remove
val arrows : unit -> int * int

Full name: Mario.Keyboard.arrows


 Returns pair with -1 for left or down and +1
 for right or up (0 if no or both keys are pressed)
val init : unit -> unit

Full name: Mario.Keyboard.init
val document : Document

Full name: Fable.Import.Browser.document
abstract member Document.addEventListener_keydown : listener:System.Func<KeyboardEvent,obj> * ?useCapture:bool -> unit
abstract member Document.addEventListener_keyup : listener:System.Func<KeyboardEvent,obj> * ?useCapture:bool -> unit
val canvas : HTMLCanvasElement

Full name: Mario.Win.canvas
abstract member Document.getElementsByTagName_canvas : unit -> NodeListOf<HTMLCanvasElement>
val context : CanvasRenderingContext2D

Full name: Mario.Win.context
abstract member HTMLCanvasElement.getContext_2d : unit -> CanvasRenderingContext2D
val s : string
val n : 'a
System.Object.ToString() : string
val rgb : r:'a -> g:'b -> b:'c -> string

Full name: Mario.Win.rgb
val r : 'a
val g : 'b
val b : 'c
val filled : color:string -> float * float * float * float -> unit

Full name: Mario.Win.filled


 Fill rectangle with given color
val color : string
val rect : float * float * float * float
val ctx : CanvasRenderingContext2D
property CanvasRenderingContext2D.fillStyle: U3<string,CanvasGradient,CanvasPattern>
type U3<'a,'b,'c> =
  | Case1 of 'a
  | Case2 of 'b
  | Case3 of 'c

Full name: Fable.Core.U3<_,_,_>
union case U3.Case1: 'a -> U3<'a,'b,'c>
abstract member CanvasRenderingContext2D.fillRect : x:float * y:float * w:float * h:float -> unit
val position : x:'a * y:float -> img:HTMLImageElement -> unit

Full name: Mario.Win.position


 Move element to a specified X Y position
val x : 'a
val y : float
val img : HTMLImageElement
Multiple items
val HTMLImageElement : HTMLImageElementType

Full name: Fable.Import.Browser.HTMLImageElement

--------------------
type HTMLImageElement =
  interface
    inherit HTMLElement
    abstract member align : string
    abstract member alt : string
    abstract member border : string
    abstract member complete : bool
    abstract member crossOrigin : string
    abstract member currentSrc : string
    abstract member height : float
    abstract member hspace : float
    abstract member isMap : bool
    ...
  end

Full name: Fable.Import.Browser.HTMLImageElement
property HTMLElement.style: CSSStyleDeclaration
property CSSStyleDeclaration.left: string
property CSSStyleDeclaration.top: string
property HTMLElement.offsetTop: float
val dimensions : unit -> float * float

Full name: Mario.Win.dimensions
property HTMLCanvasElement.width: float
property HTMLCanvasElement.height: float
val image : src:string -> HTMLImageElement

Full name: Mario.Win.image


 Get the first <img /> element and set `src` (do
 nothing if it is the right one to keep animation)
val src : string
Multiple items
val string : value:'T -> string

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

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
val image : HTMLImageElement
abstract member Document.getElementsByTagName_img : unit -> NodeListOf<HTMLImageElement>
property HTMLImageElement.src: string
System.String.IndexOf(value: string) : int
System.String.IndexOf(value: char) : int
System.String.IndexOf(value: string, comparisonType: System.StringComparison) : int
System.String.IndexOf(value: string, startIndex: int) : int
System.String.IndexOf(value: char, startIndex: int) : int
System.String.IndexOf(value: string, startIndex: int, comparisonType: System.StringComparison) : int
System.String.IndexOf(value: string, startIndex: int, count: int) : int
System.String.IndexOf(value: char, startIndex: int, count: int) : int
System.String.IndexOf(value: string, startIndex: int, count: int, comparisonType: System.StringComparison) : int
type Mario =
  {x: float;
   y: float;
   vx: float;
   vy: float;
   dir: string;}

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

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

--------------------
type float = System.Double

Full name: Microsoft.FSharp.Core.float

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

Full name: Microsoft.FSharp.Core.float<_>
Mario.y: float
Mario.vx: float
Mario.vy: float
Mario.dir: string
val jump : 'a * y:int -> m:Mario -> Mario

Full name: Mario.jump
val y : int
val m : Mario
val gravity : m:Mario -> Mario

Full name: Mario.gravity
val physics : m:Mario -> Mario

Full name: Mario.physics
val walk : x:int * 'a -> m:Mario -> Mario

Full name: Mario.walk
val dir : string
val step : int * int -> mario:Mario -> Mario

Full name: Mario.step
val dir : int * int
val mario : Mario
val render : w:float * h:float -> mario:Mario -> unit

Full name: Mario.render


 Render mario on canvas
val w : float
val h : float
module Win

from Mario
val verb : string
module Keyboard

from Mario
val w : float

Full name: Mario.w
val h : float

Full name: Mario.h
val update : mario:Mario -> unit -> unit

Full name: Mario.update
val window : Window

Full name: Fable.Import.Browser.window
abstract member WindowTimers.setTimeout : handler:obj * ?timeout:obj * [<System.ParamArray>] args:obj [] -> float
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
val mario : Mario

Full name: Mario.mario
Fork me on GitHub