loading...
Cover image for Build a Snake Game in functional JavaScript - Part 0

Build a Snake Game in functional JavaScript - Part 0

patferraggi profile image Patricio Ferraggi ・7 min read

If you are interested in reading this article in Spanish, check out my blog The Developer's Dungeon

If you have been following my latest articles you probably know I have been trying to learn functional programming.
At first, I was trying to learn Haskell by reading this book, learning Category Theory from this book and even trying to build a Snake Game in Haskell

Snake Game in Haskell

It is fair to say that I was miserable, I was making very small progress and dealing with incredible amounts of frustration, I realize then that the problem was I was trying to tackle too many things at once.

I was:

  1. Trying to learn Haskell's Syntax
  2. Trying to learn Functional Programming
  3. Trying to build a Snake Game.
  4. Trying to learn a new IDE and Dev Tools.

Mainly what was happening was that I was getting stuck constantly due to not understanding how to do basic things (like writing to the console) or not knowing the correct names for the common patterns in functional programming. So, I decided to change my approach. I went to something I knew, JavaScript. I am not an expert but I use it every day, I also had 2 books and a course prepared.

After going over them and doing some small practices I decided to take on again the challenge of building the game, now in a language I knew, so if at one point I got stuck with the functional way I could always default to doing classic JavaScript. In this tutorial, I am gonna guide you while building the game, take this not as a perfect example but as a journal of my steps into the functional way.


I took the idea of building this game from Christopher Okhravi 's explanation and decided I am gonna create this small game in multiple functional programming languages so I could check which one I liked the most and then dive deep into it. First, let me make clear that I find Christopher's content and way of explaining things amazing, but I encountered two problems with this video:

  1. Since I lacked the functional background I couldn't follow the code he wrote or understand why he had built stuff in a certain way so I just decided to take the matter into my own hands, build it on my own so I could do a comparison later.
  2. Since I don't know the common patterns for functional programming, I couldn't deal with having to build the base functions and at the same time building the game.

To solve both problems I decided to go with Ramda.js, this library implements a lot of the functions you would find in a fully functional language, they are all pure and they all come curried by default.


Sorry for the long introduction, I wanted to explain what guided my decisions and how did I got to this point. Let's start.

In this first part of the series, we are gonna try to build the 3 basic elements of the game: Map, Snake(represented by X 's), and Apple(represented by O) and display them in the console. So we get something like this:

Final result

The foundations, a point.

The map is a two-dimensional array with an X and Y coordinate, we are gonna call this type a point and we are gonna define it as follows:

const point = (x, y) => {
  return {
    x: x,
    y: y
  };
};

From this, we can create the snake which is nothing more than a collection of points, the apple which is just a single point in the map. These 2 things will be part of the state of our game.

/// I chose an arbitrary position for our apple and snake
const initialState = {
  snake: [point(2, 2)],
  apple: point(5, 5)
};

Displaying the world

In our case, the UI is gonna be the terminal, we want that to be decoupled from the logic of our game, so we leave the previous code in a module called snake.js and we create a new module called ui.js where we can start creating the code that will display a map, the initial snake and the initial apple.

The Map

As we said before the map is just a two-dimensional array filled with ., how can we do that?

We import ramda

const r = require("ramda");

We create a function that receives, the number of rows, the number of columns and the initial state of our game(we are gonna use that state later to draw the apple and the snake on top of the map).

const createWorld = (rows, columns, state) => {
  // We create a function that will create an array of a certain length 
  // with the '.' on every element by partially applying ramda.repeat
  const repeatDot = r.repeat(".");

  // we create an array with the length of `columns` with all dots in it, 
  // then we map over it and for every element we insert a new array 
  // with the length of rows with all dots in it.
  return r.map(r.thunkify(repeatDot)(rows), repeatDot(columns));
};

The apple

Let's continue with the apple since it is just a single point. We could start by doing this:

const addApple = (state, map) => {
  map[state.apple.x][state.apple.y] = "0";

  return map;
};

The function would receive the map and the state and it would add a O in the position the apple should be. This works, but I know is not "very functional" since I am mutating an array in place. Instead, we could use a function called adjust that will receive an index, a string, and an array and it will copy that array but replace the element in the index by the string it received as a parameter. So let's create a helper function for updating our map.

// This function will take a string and a point, it will first replace `X` 
// coordinate of the array and then replace the `Y`.
const update = r.curry((str, point) =>
  r.adjust(
    point.y,
    r.adjust(point.x, () => str)
  )
);

You probably noticed something strange in this function, we are not passing the map anywhere, this is because we are delaying evaluation, instead of passing the map we are returning a function that will receive the map and produce a result, I know this looks weird, but it will become evident in a moment, trust me.
Now that we have the update helper function we can refactor our addApple function like this:

const addApple = state => update("O")(state.apple);

Our addApple function will take the state, call the update function and return the function that will do the work when passed the map.
So, let's try to draw the apple, for that, I imagined that it would be like an assembly line. First, we create the map, then we draw the apple on top, so we are gonna make use of a function very common in Functional Programming called pipe.

const createWorld = (rows, columns, state) => {
  const repeatDot = r.repeat(".");
  const map = r.map(r.thunkify(repeatDot)(rows), repeatDot(columns));

  return r.pipe(addApple(state))(map);
};

With pipe what we do is set up a number of functions that will be run one after the other passing the return value of each to the next function. This seems pretty much what we want to do right? first, draw the map, then draw the apple on top and lastly draw the snake.

The snake

So now that we have a way to draw on top of the map let's extend that to draw the snake

const addSnake = state => r.pipe(...r.map(update("X"), state.snake));

So what are we doing here? well, we are creating a function that will put an X on every single position of the snake and then returning all those changes in the form of a single function by partially applying pipe. When that function gets executed and receives the map is gonna do all the changes in a chain. Now our createWorld will look like this:

const createWorld = (rows, columns, state) => {
  const repeatDot = r.repeat(".");
  const map = r.map(r.thunkify(repeatDot)(rows), repeatDot(columns));

  return r.pipe(addSnake(state), addApple(state))(map);
};

Now how can we display that? let's create a displayWorld function

const intercalate = r.curry((str, xs) => xs.join(str));

const displayWorld = matrix => {
  console.clear();
  console.log(intercalate("\r\n", r.map(intercalate(" "), matrix)));
};

This function is nothing magical, it just takes the map, logs every line by putting a space in between each element, and when it gets to the end it breaks the line, I did extract the logic of joining to a helper function to make it more readable.

Finally, we can put our initial state together and show it in the console

const display = (rows, columns, state) => {
  return r.pipe(createWorld, displayWorld)(rows, columns, state);
};

display(15, 15, Snake.initialState);

Final result

I know, we really need to get someone to work on the graphics if we are gonna try to sell this game right?


I hope this example wasn't too hard for you guys, I tried my best to explain my thought process when creating the UI.

In the following post, we will try to cover all the logic for moving the snake, eating the apple, restarting the game and losing.

If you liked this article please don't forget to share or comment, if you have any doubts about the code don't doubt to ask me in the comments. You can also check the source code here 😄.

Discussion

pic
Editor guide
Collapse
jlrxt profile image
Jose Luis Ramos T.

Hey amigo primero lo primero: gracias.
Justo en lo que estoy documentando me. Quiero crear un juego clásico ping Pong. Pero como estoy empezando con JS, créame , se a que se refiere con las frustraciones. Me despido con la siguiente pregunta ¿ Este juego estará basado solo en Javascript o también HTML5?. Gracias/ te sigo ahora. Saludos desde cdmx 🇲🇽

Collapse
patferraggi profile image
Patricio Ferraggi Author

Hola como estas? me alegro mucho que te haya gustado. Por el momento solo estoy haciendolo en consola, de cualquiera manera la idea seria que el codigo del juego este separado del codigo de la consola por lo que escribir una version para mostrar en una pagina web seria sumamente sencillo. Podria agregarlo luego de la serie :)

Collapse
jlrxt profile image
Jose Luis Ramos T.

Gracias. Estaré pendiente.

Collapse
youpiwaza profile image
max

Good effort,and pretty well explained. Can't wait to see the next post :p

I just don't get the ramda import. What is it, and why use it ? ^^'

Collapse
patferraggi profile image
Patricio Ferraggi Author

Hey Max, I am glad you liked it, hopefully I can get the next one done in the upcoming week.

Well, most of the functions I used like pipe, thunkify, map and repeat they don't come by default with JavaScript, in order not implement them myself (as it is not the goal of this project) I used a library called Ramda.js, you can check the documentation here ramdajs.com/docs/. So in order to use this library inside node.js, I first need to import it, otherwise, methods are no visible. If this would be done directly in a browser I could have used the 'import' syntax or just put a reference for the ramda.js file inside the html.