Haskell roguelike - UI code

Posted on April 2, 2018

start prev next

Boilerplate warning

There is a bit of support code that needs to be written to connect haskell and the browser UI. None of this is roguelike specific so it may be worth quickly skimming over the rest of this chapter and coming back when required. The remaining chapters will cover roguelike topics so don’t be discouraged by the infrastructure code below. Its also not nearly as bad as it looks.

The infrastructure

Scotty

I’m using Scotty for the HTTP and web-socket handling. It is simple and not particularly opinionated.

Scotty host code

First the require imports

01_web_ui/src/GameHost.hs (12 to 20)

The connection type lets us wrap up the scotty connection details so that other code can pass it around without having to know anything about scotty.

01_web_ui/src/GameHost.hs (24 to 28)

runHost starts scotty listening on port 61492 and sets up the web sockets.

01_web_ui/src/GameHost.hs (32 to 36)

scottyApp handles the serving of the static resources. Here several get routes have been configured.

01_web_ui/src/GameHost.hs (40 to 58)

Finally wsapp handles the web socket calls. A new connection object is created and passed to the startHost function

Communication between the browser and haskell

The browser will send haskell simple text based commands such as redraw. Haskell will send JSON encoded objects.

Types for the UI

These types are used to send data to the UI. They all have Aeson toJSON instances.

01_web_ui/src/GameCore.hs (33 to 72)

Types for the game

01_web_ui/src/GameCore.hs (19 to 28)

Running the host

runGame is called to start the host and pass control to manageConnection

01_web_ui/src/GameEngine.hs (22 to 23)

manageConnection waits for a command from the browser, parses it and executes the instruction.

If manageConnection gets a init command, it will created a new World and store it in a TVar. This is the mutable state of the world managed by STM.

01_web_ui/src/GameEngine.hs (28 to 58)

The init command is expected to pass the size of the screen as a parameter. initialiseConnection and parseScreenSize handle this logic

01_web_ui/src/GameEngine.hs (63 to 70)
01_web_ui/src/GameEngine.hs (145 to 153)

bootWorld creates a new World and Config. For this chapter it sets up a single shortcut key t, that should send the test command back

01_web_ui/src/GameEngine.hs (75 to 85)

Once the world is initialised, a thread is started in manageConnection’s runConnection function that waits for commands from the web socket. Each command is passed to runCmd for handling

The current version of runCmd will

01_web_ui/src/GameEngine.hs (90 to 107)

Since the JSON data payload could get large it is being compressed with bzip before being sent over the websocket. bzlib handles this for us.

01_web_ui/src/GameEngine.hs (137 to 140)

And the final few helper functions.

01_web_ui/src/GameEngine.hs (112 to 133)

Json encoding

As I’m using Text it would be nice to have an Aeson function that encodes to Text rather than ByteString. The Data.Aeson.Text.Extended module defines encodeText to do this and re-exports Data.Aeson

01_web_ui/src/Data/Aeson/Text/Extended.hs (2 to 17)

HTML and Javascript

Here is the full HTML for the game. The core of the html is the canvas on which the tiles will be drawn. Notice that

01_web_ui/html/rogue.html (2 to 47)

Javascript libraries

The following libraries are being used

Keyboard handling

Keyboard handling is made pretty simple by mousetrap. As an example here is how the ? key is configured to do an alert

01_web_ui/html/rogue.js (152 to 152)

Mousetrap offers a few fantastic features that we’ll be using for the game

As you saw above the haskell code sends the shortcut keys that should be setup. This makes it possible to control what shortcuts are available dynamically and sticks with the idea of keeping as much logic in haskell as possible.

The sendKey function is called when a key is pressed. It stops all further key presses and sends the key press command to the haskell server. Key presses are enabled again when a response is received by runWebSocket

01_web_ui/html/rogue.js (16 to 25)

The websocket loop

runWebSocket handles the communication over websockets and responds to commands.

01_web_ui/html/rogue.js (29 to 81)
01_web_ui/html/rogue.js (32 to 33)
01_web_ui/html/rogue.js (37 to 43)
01_web_ui/html/rogue.js (47 to 50)
01_web_ui/html/rogue.js (54 to 75)

Dealing with resizing

The haskell code is going to need to know when the screen is resized so that it can act accordingly. E.g. it may need to recenter the player or clip tiles. The resizeCanvas function does this for us

01_web_ui/html/rogue.js (5 to 11)
01_web_ui/html/rogue.js (93 to 116)

Browsers will trigger the redraw handler while the browser window is being resized, i.e. as the user is dragging it to the desired size. We definitely do not want to request a redraw hundreds of times while the window is being dragged. So a debounce routine is used to minimise the number of requests sent.

The window resize event is setup to call resizeCanvas after it has been debounced. Here I’m setting it up to only send the command after no subsequent resize events are triggered for 250ms.

01_web_ui/html/rogue.js (132 to 147)

Done

That is quite a lot of functionality for ~100 lines of JavaScript so far. There is about another 50 lines that needs to be added in later chapters to deal with drawing of the tiles. But that is it for the front-end logic, everything else happens in Haskell.

start prev next