Composing Event Streams: SnakeGame$

Fermin Blanco
The Startup
Published in
4 min readSep 17, 2020

Diving deeper into building user interfaces you may encounter a few challenges in how to manage asynchronicity between features and events, the y may occur in a given point in time.

So we will be recreating the snake game using event composition with Observables.

warning: this is not a introduction to Observables neither to event composition and it may have a lot of mistakes which you could/ may point them out and I will be more than happy to take into account as a feedback.

There are plenty async primitives that empower us to model our application from callbacks, promises, to all way around to Observables. I chose Observables because it makes the data flow natural and fluid. In my so adorable opinion, I think, they are the best and funniest async primitive to composing events on Javascript.

TLTR: stops this madness and show me the code!

For starters we will modelling user interaction as a stream. So every time the user press a key down the snake will reacts accordingly, in other words we could map keys over updates in the snake position. We will be listen for ‘ArrowUp’, ‘ArrowDown’, ‘ArrowLeft’, and ‘ArrowRight’ keys but you may use another set of keys combination.

Technically we’ll be listen for any ‘keydown’ but since we made explicit our interest for the Arrow keys through the filter function, this just works.

snakeMoves$

How about decomposing our stream a little bit,

fromEvent is our adapter to make the Dom API which is callback based into a stream of events. Right bellow, we start to see the first operator in the chain: map, what behaves exactly as we may expect but instead of working over arrays it does over a collection of values that changes over time (Observables). But what is an operator anyway?

An operator takes an Observable as Input and then produces an Observable as Output.

Map operator function

Please take in consideration that this is naive implementation in pure Javascript and if you are interested in a more robust implementation go to the rxjs project.

The second operator is filter and as its name suggest it filters event values out of the chain. Bur for the moment, let us drag our attention to the isKeyAllowed function.

Just Arrow keys allowed

As you may catch it returns true when any arrow key gets as a input. So every time the user press an arrow key it flows all the way back to us/our render function (our render function could be just a console.log).

Let’s rethink our stream for a moment. It does not make sense to name it snakeMoves$ since it does not connect already with any component/concept that made it semantically correct. Then let’s rename it.

ArrowKeys stream

Since arrowKeys$ lacks the meaning of the snakeMoves$ we could add some actions/operators to add meaning to the whole pipeline.

snakeMoves$

It may catch your attention the last action shouldGrowBy2 , since what would be the point of having a second function for growing the snake? but there are some special conditions to be met here and the way it elongates and modifies the snake state are just unique (a better name will be more appropriate, I totally agree).

You also may wondering why get into the trouble of having two streams if they could be perfectly just one, but as long as we keep adding features to our game, having the arrow keys in a different stream just made it modular. For now let’s just say we are going to make a second subscription to arrowKeys$ later on.

Let me introduce you to every action in the chain so we can agree this whole pipeline looks as a snake moves:

Is direction allowed?

Well a simple constraint is that two pieces of the snake can not be in the same position at the same time but if your previous movement was ArrowRight and your next move is ArrowLeft that would cause you some trouble. So we have to avoid going in opposite directions.

Move snake

If the direction is allowed we should move our snake by one unit. The concepts above what a unit is and the space from where it will move is entirely up to you. This action lets you take care of this particular feature.

Should reverse?

What happen if the snake crash with a wall or the boundaries of the game? well in this case I will provide an action to reverse snake direction but you maybe will come with a very different approached.

Should grow?

In the case that the head of the snake occupies the same space than the food are, it will elongate the snake size by one unit or the ones make sense to your application.

Should growBy2?

Really?, another action just for growing the snake size. Well in fact it comes pretty handy when you understand that this action fits special purposes such as handling the collision with a different type of food and the effect it produces over the snake size and the score of the game.

Let’s recap then, snakeMoves$ is an stream what for every Arrow key stroke produces a change of the snake state. With this in mind we could define our render function like,

Smooth right? connectStore is just a function that subscribes to the changes on the store.

Let us create the stream that will be in charge of creating the food.

placingFood$

As far as we can tell, there is not much mysterious about this Observable. It just updates the position of the food and what we should be interested about is when it does it.

Irregular Intervals Creator

It is just a wrapper around an Observable. It creates a timeout, clean it and then creates another one as long as it does not get unsubscribe.

Remember when I told you we would re-using arrowKeys$ streams later on? well, now it is the time.

fireWhenSnakeHitsItself$.js

How about a second subscription to pay attention when the snake contacts itself? pretty neat in my opinion but maybe not the best performance but it looks pretty fluid. This stream when fired (or to be more precise, when a value gets down) will unsubscribe from the whole game pipeline.

The filter action will take care of passing a value down when the a portion of the snake has the same space location. Reset will set all the state values to default.

--

--