Two Tetromino Tetris with Fable and F#

In my last post, I talked about my early adventures Fable and building a memory tiles game.

Recently I have been exploring Fable further and, in particular, using it to interact with the Javascript canvas API. I’ve been wanting to try out building a version of Tetris for a while now. I used to love playing it on the Gameboy many years ago. Fable afforded me the perfect tool to build this game in the beautiful language of F#.

There is a video snippet above of my implemented game in action. This blog won’t go into every detail of the code as it is available here but I wanted to go through a few code snippets here that may help other people looking to build a game involving game animation frames and using the canvas API with Fable and F#.

What’s a tetromino and why two tetrominoes and not the full thing?

A tetromino is the name given to each of the shapes in the Tetris game. Each tetromino also has a number or rotations that it can go through. There are more details about Tetris tetrominoes here.

I think of this version as a version 1. I wanted to get as much of a fully featured game implemented as I could in a pretty short time frame – while I had some summer vacation time. I hope to return to this at some point and add more shapes and also improve the rotation algorithm – unlike the original game, my rotation algorithm simply prevents rotations when blocks are in the way although it does handle wall kicks. I chose the I-Block along with its rotations and the T-Block along with its rotations as the two tetrominoes that I would use in this initial Tetris implementation.

The domain modelling

I modeled the gameboard as a map of row indexes to rows with each row itself being a map of the block left X position indexes to blocks. Blocks are the primitives that tetrominoes are built from. I wanted to capture the states that a gameboard can be in using the F# type system. A gameboard that is in a resting state has no moving tetromino and a gameboard in motion has a moving tetromino. The main game engine processes these gameboard states so I made them distinct using a Discriminated Union – a DU.

60   type LeftBlockPosition = float 
61    
.......
65   and RowData = Map<LeftBlockPosition,Block> 
66    
67   type RowBottomPosition = float 
68    
69   type GameboardInMotion = { 
70       Height : float 
71       Width : float 
72       BlockSize : float 
73       MovingTetromino : Tetromino 
74       Rows : Map<RowBottomPosition, RowData> 
75   } 
76    
77   type RestingGameboard = { 
78       Height : float 
79       Width : float 
80       BlockSize : float 
81       PlacedTetromino : Tetromino 
82       Rows : Map<RowBottomPosition, RowData> 
83   } 
84    
85   type Gameboard =  
86   | GameboardInMotion of GameboardInMotion 
87   | RestingGameboard of RestingGameboard 
88    

A block is modeled as a an F# record.

3    type Block = { 
4        BottomX : float 
5        BottomY : float 
6        Color : string 
7    } 

The tetrominoes are modelled as a DU also with each case representing a tetromino rotated in a particular direction. Each tetromino also has tetromino detail about the rows of blocks that it comprises of.

19   type TetrominoRow = { Blocks : Block list } 
...........
36   type TetrominoDetail = { TetrominoRows : TetrominoRow list } 
37    
38   type Tetromino =  
39   | StraightUp of TetrominoDetail 
40   | StraightRight of TetrominoDetail 
41   | StraightDown of TetrominoDetail 
42   | StraightLeft of TetrominoDetail 
43   | TShapeUp of TetrominoDetail 
44   | TShapeRight of TetrominoDetail 
45   | TShapeDown of TetrominoDetail 
46   | TShapeLeft of TetrominoDetail 

Valid key presses are also modeled using a DU and using designing with capabilities to prevent the creation of an invalid key press. I learned about designing with capabilities from Scott Wlaschin – Designing with Capabilities and from reading Scott’s new book:
Domain Modeling Made Functional

89   type KeyCode = float 
90    
91   type GameControl =  
92       | Up 
93       | Left 
94       | Right 
95    
96   type ValidKeyPress = private ValidKeyPress of GameControl * KeyCode  
97    
98   let (|ValidKeyPress|) validKeyPress =  
99       match validKeyPress with  
100      | ValidKeyPress(control,keyCode) -> ValidKeyPress (control, keyCode) 
101   
102  module ValidKeyPress =  
103       
104      let toValidKeyPress keyCode = 
105          match keyCode with 
106          | 37. -> Some <| ValidKeyPress (Left, 37.) 
107          | 38. -> Some <| ValidKeyPress (Up, 38.) 
108          | 39. -> Some <| ValidKeyPress (Right, 39.) 
109          | _ -> None

Working with the canvas API

The Fable.Import namespace provides a thin wrapper over the Javascript DOM API.

I get a reference to my actual canvas div with the following:

9    let tetrisView = Browser.document.getElementById("tetris-view") :?> Browser.HTMLCanvasElement 
10   let ctx = tetrisView.getContext_2d() 

To render rows of blocks, I used fillRect as follows:

27       let renderRow blockSize (blocks:Map<LeftBlockPosition,Block>) =  
28           blocks |> Map.toSeq |> Seq.map snd |> Seq.iter (fun block ->  
29               ctx.fillStyle <- U3.Case1 block.Color 
30               ctx.fillRect(block.BottomX, block.BottomY - blockSize, blockSize, blockSize)) 

To clear the canvas area before each re-render of the gameboard, I used clearRect:

 ctx.clearRect(0., 0., tetrisView.width, tetrisView.height) 

Getting the game in motion!

Using .Net events and also the Javascript interval API (wrapped nicely by Fable), I was able to set up a repeating game frame clock. So, each time the time interval passes, an event is triggered causing a new gameboard to be evaluated from the current gameboard and from any user game control input and this new game board is then rendered.

46   let frameChangeEvent =  new Event<Gameboard>() 
47    
48   let mutable private frameClockId = 0. 
49    
50   let startFrameClock() =  
51       frameClockId <- Browser.window.setInterval((fun() ->  
52               match lastRenderedGameBoard with  
53               | GameboardInMotion _ 
54               | RestingGameboard _ -> frameChangeEvent.Trigger lastRenderedGameBoard 
55               )  
56           , 150.) 

I handle user input in a separate UserGameController module using the wrapper that Fable gives over DOM event handling:

1    module Tetris.UserGameController 
2     
3    open Fable.Core 
4    open Fable.Import 
5    open Tetris.Definitions 
6     
7    Node.require.Invoke("core-js") |> ignore 
8     
9    let private keyPressed= ref (None:ValidKeyPress option) 
10    
11   let private handleKeyDown keyCode =  
12       match ValidKeyPress.toValidKeyPress keyCode with 
13       | Some (ValidKeyPress _ as vkp) -> 
14           keyPressed := Some vkp 
15       | None -> () 
16    
17   let private handleKeyUp keyCode =  
18       match !keyPressed with 
19       | Some (ValidKeyPress(_, currentlyPressed)) when keyCode = currentlyPressed ->  
20           keyPressed := None 
21       | _ -> () 
22     
23     
24   Browser.window.addEventListener_keydown (fun e -> handleKeyDown e.keyCode :> obj) 
25   Browser.window.addEventListener_keyup (fun e -> handleKeyUp e.keyCode :> obj) 
26    
27   let getKeyPressed() = !keyPressed 
28           

The game engine

The game engine is essentially a state machine. From the gameboard it is given and the current user input (if any), it works out what the next gameboard should be. There is a fair bit of logic in there around things like:
– whether a rotation is allowed,
– what the next rotation is for the current tetromino,
– whether the tetromino can move left or right,
– whether the tetromino should rest on blocks below
etc.

With the domain modeled using the F# type system and immutable DUs and records, these transitions became simpler to reason about. As an example, after it is determined if a rotation is possible (if the up arrow is currently pressed), the engine works out which way the tetromino can move. I created what I called a TransitionReferee for this. This is somewhat verbose but I wanted the logic and rules to be self documenting as much as possible. The snippet from the TransitionReferee is below to give an example of what I mean.

21   module TransitionReferee =  
22    
23       type RefereeDecision =  
24           | MoveHorizontallyAndVertically of GameboardInMotion 
25           | MoveVerticallyOnly of GameboardInMotion 
26           | MoveVerticallyOnlyAndRestOnBottom of GameboardInMotion 
27           | MoveVerticallyOnlyAndRestOnBlockBelow of GameboardInMotion 
28           | MoveAndRestOnBlockBelow of GameboardInMotion 
29           | MoveAndRestOnBottom of GameboardInMotion 
30           | CheckForCompletedRowsAndReleaseAnotherBlock of RestingGameboard 
31            
32       let blocksOverlapHorizontally block1XPos block2XPos blockSize = 
33           block1XPos > block2XPos - blockSize && block1XPos < block2XPos + blockSize 
34        
35       let tetrominoRowOverlapsWithExistingBlocks (horizontalTransitionDirection:HorizontalTransitionDirection) blockSize (tetrominoRow:TetrominoRow) row =  
36            
37           tetrominoRow.Blocks 
38           |> List.exists (fun tetrominoBlock -> 
39               row  
40               |> Map.exists (fun existingX  _ ->  
41                   blocksOverlapHorizontally existingX (tetrominoBlock |> nextXPosition horizontalTransitionDirection) blockSize)) 
42    
43       let otherRowsInRangeContainingBlocksInTheWay direction (gameboard:GameboardInMotion)  =  
44           //compare gameboard rows when all tetromino blocks have been removed 
45           gameboard.MovingTetromino.TetrominoRows 
46           |> List.fold (fun gameboardRows tetrominoRow -> 
47                gameboardRows 
48                |> Map.tryFind tetrominoRow.BottomY  
49                |> Option.map (fun gameboardRow ->  
50                   tetrominoRow.Blocks 
51                   |> List.fold (fun row b -> row |> Map.remove b.BottomX) gameboardRow) 
52                |> function | Some gameboardRowWithTetrominoBlocksRemoved -> gameboardRows |> Map.add tetrominoRow.BottomY gameboardRowWithTetrominoBlocksRemoved | None -> gameboardRows 
53           ) gameboard.Rows 
54           |> fun gameboardRows -> 
55               gameboardRows 
56               |> Map.toSeq 
57               |> Seq.exists (fun (rowY, gameboardRow) -> 
58                   gameboard.MovingTetromino.TetrominoRows  
59                   |> Seq.exists (fun tetrominoRow ->  
60                       (rowY > ((tetrominoRow.TopY gameboard.BlockSize) + transitionDistance) &&  
61                        rowY < (tetrominoRow.BottomY + transitionDistance) + (tetrominoRow.Height gameboard.BlockSize) && 
62                        tetrominoRowOverlapsWithExistingBlocks direction gameboard.BlockSize tetrominoRow gameboardRow))) 
63    
64       let decideTransition (direction:HorizontalTransitionDirection) (gameboard:Gameboard) =  
65           match gameboard with 
66           | GameboardInMotion gameboard -> 
67               let otherRowsInRangeContainingBlocksInTheWay direction  = otherRowsInRangeContainingBlocksInTheWay direction gameboard                     
68                
69               let tetrominoShouldMoveToRestOnBlocksBelow direction (gameboard:GameboardInMotion) =  
70                   gameboard.MovingTetromino.TetrominoRows 
71                   |> Seq.exists (fun tetrominoRow -> 
72                       gameboard.Rows 
73                       |> Map.tryFind (tetrominoRow.BottomY + transitionDistance + gameboard.BlockSize)  
74                       |> Option.map (fun row ->  
75                           tetrominoRowOverlapsWithExistingBlocks direction gameboard.BlockSize tetrominoRow row)  
76                       |> function | Some b -> b | None -> false 
77                   ) 
78                    
79               if otherRowsInRangeContainingBlocksInTheWay direction then 
80                   if (gameboard.MovingTetromino.TetrominoRows.[0].BottomY + transitionDistance) = gameboard.Height then  
81                       MoveVerticallyOnlyAndRestOnBottom gameboard 
82                   elif tetrominoShouldMoveToRestOnBlocksBelow NoHorizontalTransition gameboard then 
83                           MoveVerticallyOnlyAndRestOnBlockBelow gameboard 
84                   else MoveVerticallyOnly gameboard 
85               else  
86                   if (gameboard.MovingTetromino.TetrominoRows.[0].BottomY + transitionDistance) = gameboard.Height then  
87                       MoveAndRestOnBottom gameboard 
88                   elif tetrominoShouldMoveToRestOnBlocksBelow direction gameboard then     
89                       MoveAndRestOnBlockBelow gameboard 
90                   else MoveHorizontallyAndVertically gameboard 
91                    
92           | RestingGameboard gameboard -> CheckForCompletedRowsAndReleaseAnotherBlock gameboard 

Conclusion

I hope the code snippets that I have shown here can be of some help to others who are exploring Fable. The full source code is available here:
Also, the current version of this Two Tetromino Tetris game can be played here.

One thought on “Two Tetromino Tetris with Fable and F#

Leave a Reply

Your email address will not be published. Required fields are marked *