Using functional programming to create a game in JS

functional programming
Summary
  • The author built a turn-based spaceship browser game in JavaScript to practice functional programming concepts like determinism, pure functions, immutability, and point-free style, using PixiJS as the only production dependency.
  • Base functions such as curry and compose were implemented from scratch (instead of using libraries like Ramda) to better understand their inner workings, along with helpers wrapping native JS functions like map, filter, and reduce.
  • Models were built using a functional-shared style with helpers like getState, assignState, and getProp (returning a monad), enabling composition-based implementations that scored well on SonarQube cognitive complexity.
  • The author found FP code highly readable and reusable but harder to debug and requiring multiple refactors, recommending against using unfamiliar paradigms on ambitious projects with tight deadlines.

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.

functional programmingInitial 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 functional 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.

 

FAQ

What is functional programming?

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). Key concepts include determinism (the same input always returns the same output), pure functions (which do not cause changes beyond their scope), and immutable data (which cannot have its value changed after creation). Together, these concepts make testing, caching, and parallelism easier.

What project was built and what technologies were used?

The project is a turn-based spaceship game that runs in the browser. Each player has 3 ships and on each turn chooses where to move a spaceship within its reach range and in which direction to shoot. When a spaceship gets shot, it loses part of its shield; if it has no shield left, it is destroyed, and the player who runs out of spaceships loses. The game was built in JavaScript, uses PixiJS for rendering (the only production dependency), and uses spaceship sprites freely available from UnLucky Studio on OpenGameart.

What base functions were implemented to support the functional style?

Native JS functions such as map, reduce, filter, find, some, and every were used. To follow point-free style, Curry (which allows a function to receive its arguments at separate moments) and Compose (which takes functions as arguments and executes them in reverse order, with each function consuming the return of the previous one) were implemented. Curried helpers were also created to wrap native JS functions for easier composition.

How were models declared in the project?

Models were implemented using the functional-shared style, where a model instance is an object with its properties and functions. A helper called modelFunctions provides 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).

Was using functional programming worth it for this project?

Because the author had no previous experience with FP, the project had to be refactored several times, and FP was hard to debug due to issues like the stack tracing limit. On the other hand, the functions are very readable and easy to reuse. The author does not advise using unfamiliar paradigms/technologies on projects with ambition and short deadlines, but this project was developed for learning. Avoiding libraries and implementing the base functions helped in understanding how each works, and the final package size was essentially only the size of the PixiJS modules used.

About the author.

Karran Besen
Karran Besen

A computer scientist who loves to study new technologies. Also enjoys rap, watching movies and TV shows, sports (especially soccer), and playing videogames.