Using functional programming to create a game in JS

December 19, 2019 0 Comments

Using functional programming to create a game in JS

 

 

There has been a lot of hype around the functional programming paradigm for some time and there are a lot of great books and articles about it on the internet, but it’s not so easy to find real examples with its application. So have I decided to create a game trying to follow its concepts using Javascript, which is the most popular programming language today. In this post, I will share some of this experience and tell you if it was worth it.

What is functional programming?

To keep it short, functional programming (FP) is a paradigm that attempts to reproduce the concept of mathematical functions, which is a relationship between the sets of domains (valid inputs) and the codomain (valid outputs). Mathematical function outputs are always related to just one input, so whenever a mathematical function is calculated with the same input, it returns the same output. This is one of FP’s most important concepts, also known as determinism.

Non-deterministic function example:

let x = 1 const nonDeterministicAdd = y => x + y nonDeterministicAdd(2) // 3 x = 2 nonDeterministicAdd(2) // 4 

Deterministic function example:

const deterministicAdd = (x, y) => x + y deterministicAdd(1, 2) // 3 

In addition to determinism, functions in FP seek not to cause changes beyond their scope. These types of functions are called pure. Last but not least, the data in FP must be immutable, meaning it cannot have its value changed after creation. These concepts together make testing, caching, and parallelism easier.

Beside these basic concepts, I also tried to use Point-free style during the development of the game, which aims to make the code cleaner as it omits the use of unnecessary parameters and arguments. Here¹² are a couple of good references about it.  

Since the beginning of the project, the intention was to create a game that runs in the browser. Because Javascript (JS) is a language that I feel comfortable with and is a multi-paradigm language, it was language I chose for this project.

There are two excellent books about FP that I recommend:

The Project

The implemented project is a turn-based spaceship game. In the game, each player has 3 ships and on each turn the player must choose the place they want to move the spaceship within its reach range and in which direction they want to shoot. When the spaceship gets shot it loses part of its shield. If a spaceship has no shield left, it gets destroyed and the player who runs out of spaceships loses the game.

Initial turn of a match

So far, the game only allows for one player to participate, and this person controls the 3 spaceships at the top of the screen, facing a script that controls the 3 spaceships at the bottom, which randomizes the position and the target of its spaceships. Regarding the graphic part, I used the PixiJS package to control the rendering, which is the only production dependency of the project, and I also used spaceship sprites that were freely available from UnLucky Studio on the OpenGameart site.

Base and Helper Functions

In the beginning of the implementation, a file was created with base functions that were used in almost all project files. Some of these base functions exist natively in JS, such as map e reduce. JS also has several other functions that fit the FP paradigm by not changing their input values, and which were used in the project, such as filter, find, some, every. A good source for discovering these functions is Does it mutateTo follow the point-free style, it was also necessary to implement the following base functions:

  • Curry: Allows the function to receive its arguments in separate moments
const add = curry((x, y) => x + y) add(1, 2) // 3 add(1)(2) // 3
  • Compose: The functions are passed as arguments and are executed in reverse order. Each function consumes the return of the previous function.
const addAndIncrement = compose(  add(1), // previous add result + 1  add // arg1 + arg2 ) addAndIncrement(2, 2) // 5

There are several libraries in which these functions are already implemented, such as Ramda, but in this project, I decided to implement them to try to better understand how they work. This article was a great source to investigate how they might work and how you could implement these and other functions recursively.

To facilitate the composition of the native JS functions that were used, I created helpers using curry, where the entries are passed as arguments.

Example:

const filter = curry((fn, array) => array.filter(fn)) const getAliveSpaceships =   compose(     filter(isAlive),     getSpaceships

And how do we declare the models?

Regarding the models’ implementations, we used the functional-shared style, in which a model instance is an object with its properties and functions. To manage the state of models, we created the following helper, where `getState` returns the state of the instance; `assignState` returns a new instance with the old state concatenated with the new one and` getProp` returns the value of the passed property encapsulated in a monad.  Monad is a popular construction in FP, and is a bit hard to summarize an one-line definition, so here is an article that explains it in a friendly manner.

const modelFunctions = (model, state) => ({   getState: () => state,   assignState: newProps => model({ ...state, ...newProps }),   getProp: name => getProp(state, name), })


With this helper we can declare models, create instances and use their functions as follows:

const Engine = state => ({ ...modelFunctions(Engine, state) }) Engine({ a: 'a' }).assignState({ b: 'b' }).getState() // { a: 'a', b: 'b' }

Implementing the rest

Once you have the base functions and templates defined there is still a lot left to implement. Below are some of the project functions that give a good taste of readability and how easy it is to compose the functions.

  • Remove the player’s destroyed spaceships
const removeDestroyedSpaceships = player => compose(   setSpaceships(player),   getAliveSpaceships )(player)

 
  • Reduce the spaceship shield

export const reduceShield = curry((spaceship, damage) =>   compose(     checkDestroyed,     shield => assignState({ shield }, spaceship),     shield => sub(shield, damage),     getShield   )(spaceship) )

Implementations made through composition are often easier to understand compared to imperative programming. This function, for example, was analyzed by SonarQube for cognitive complexity and received the best possible score.

  • Get the spaceship’s bullets

export const getBullets = compose(   either([]),   getProp('bullets') )

Here it was possible to omit the function argument as it was only being used by one of the compound functions. There is also a guarantee that the returned value will be valid because ‘getProp’  returns a monad and ‘either’ returns the monad’s encapsulated value if it is a valid value or an empty array.

  • Set a new position for the bullet

const setPosition = curry((coordinate, bullet) =>   compose(     callListenerIfExist('onMove'),     assignState({ coordinate })   )(bullet) ) 

FP composition requires functions to always have a return value. If  ‘callListenerIfExist’ does not return any value, it would not be possible to chain other functions with it or with ‘setPosition’ after their execution.

Was it worth it?

This is the project repository and hosted on this link. Because I have not had any previous experience with FP, I had to refactor the project several times and also found FP hard to debug, because of things like stack tracing limit. But on the other hand, the functions are very readable and easy to be reused. I do not advise projects that have require ambition and short deadlines to be done using paradigms/technologies that you are not used to, but this project has been developed with the intention of learning. Avoiding using libraries and implementing the base functions was very helpful to understand how each one of them works, and the final package size was pretty much only the size of the PixiJS modules that were used.

 


Tag cloud