Use your Cerebral-writing a game

Damian Płaza
17 min readFeb 21, 2019

--

Photo by rawpixel on Unsplash

The main goal of this post is to write a simple game to become familiar with Cerebral’s concepts in the practical way. Additionally, we’re going to use TypeScript so it would be really great if you are familiar with this technology. If you’re not, please keep being open-minded and I think it won’t be an obstacle for you. Just as a reference, I wrote post that was more about telling what Cerebral is and what are the biggest power-ups coming from using it. It was kind of no-code story, but this time it’s hands on code.

Web application — quick & dirty explanation

What is web application? I won’t quote any smart person or wiki. We can’t model reality as a whole, we need to make simplifications. Let’s simplify it by splitting it: web and application. I would say that at this point we can make a shortcut, an assumption — web is just a environment where our application is running on. It includes web technologies as HTML, CSS, JS and native browser features. What about application?

Well, application can be simplified to a tool that helps getting user’s thing done. Let’s assume it does it by tracking all information relevant to the user and updating those information. Good example is a shopping cart. User can put items into, replace items, etc. We can say that cart tracks all information regarding products currently kept in it. Another good word for keeping information is state, now things start to look pretty interesting. So application has state which can be transformed. Users can interact with state by user interface (UI). We can model each user interaction as attempt to change application state. Another shortcut would to be think that application without state is useless. Imagine user who infinitely adds items to shopping cart and nothing happens. Cart remains empty. Sounds like a horror, isn’t it?

Fine, how does it relate to Cerebral?

I am glad you’re asking (because you are, right?). Below I put a list of definitions that we’re going to go through with practical examples. Next sections are named almost exactly as bullet points , so it would be easier to refer to them. Just go through them and even if they sound vague/complex/misleading — give yourself time, because we’re going to use those concepts across our development so they will start becoming more and more familiar.

Cerebral in bullet points:

  • similar, logical parts of application are grouped into modules
  • application consists of a state tree — tree means plain JS object, you know, curly braces — {}
  • state can be changed only using functions called actions
  • application can use set of actions that serve user in order to achieve known result — put item in a shopping cart or increase count if item was already added, count number of items, get valid prices from web service, finish order if number of items is greater than zero
  • actions can be grouped into sequences
  • view (UI) can act upon a state (application) using sequences
  • changes applied to state might reflect changes applied to view(UI)
  • application should communicate with everything not being application itself (i.e. state) using objects called providers
  • operators are configurable actions

Enough talking, show me the code

It’s great you’re reading and processing new information, but without making your hands and brain (cerebral?) dirty, it simply doesn’t pay off. Knowledge that isn’t used will be erased (soon). I am asking you to clone this repo, containing minimal boilerplate, and just follow what we will be doing in next paragraphs. It’s crucial, believe me. The faster implementation of new skills is, the sooner you will gather fruitful results. So again, clone this repo. Additionally, please follow the instructions to setup Cerebral Debugger.

NOTE: at the end of this post you can find list of branches that refer to some of the steps met during the post. Please check them if you started struggling with anything during the process.

We will create simple memo game. Rules (professionals like to call them business rules) look as follows:

  • there’s a set of N cards where N needs to be an even number
  • player can check if two cards contain similar figure
  • if two cards contain similar figure, they are removed from set after specified time and points are increased
  • if two cards do not contain similar figure, they are deselected after specified time
  • game is finished when last pair of cards is removed

Simple as that.

And here’s how our came would look like at the end:

You can play the game here. As a hint I can say you can connect to the game using Cerebral Debugger to see how it works in “real-time”.

Similar parts of application are grouped into modules

We’re going to have only two modules — main and game. Main’s purpose is to keep all application specific state, while game would represent everything connected to cards, operations on them, etc.

Application consists of a state

Let’s try to think from types perspective what do we really need to model this case. From the rules we can see that for sure we would have card. Each of them contains figure. Also card can be selected. When player reveals cards with the same figure, those cards shouldn’t be taken into account in the next round. We can say those cards are “solved”.

We also need to know what is current status of the game: whether it wasn’t started yet, it’s running or player finished the game.

And entire game module’s state:

This is only definition,we need to provide initial value of state so here it is:

Now we need to add this state to module:

Application contains a set of actions

Our game won’t be a big one so we won’t have that many of them (at least in the first iteration). As aforementioned, “actions serve user to accomplish known result”, so for sure in order to start a game, user would need to have a collection of cards, randomly shuffled. Let’s create action for this!

We defined expected action’s arguments (in Cerebral we called them props) and created action that basically initializes cards set by transforming number of cards to randomize. In order to pass a result to Cerebral’s pipeline (also called sequences, but we’ll get back to it), we’re returning cards wrapped in plain object, because Cerebral merges each action’s result into pipeline’s props — this enables consumption of one action’s result in another action.

As you probably noticed — we’re not interacting with application’s state. That’s because we don’t want to bake all responsibility into one action. Now we have reusable piece that can fit into many places provided that some initial conditions are fulfilled, namely sufficient props were passed into a Cerebral’s pipeline.

Contradictory to “pure” action for randomizing Cards, now we will see action that DOES A LOT and probably you’ll intuitively notice what’s wrong with it. I’ll leave you with it for a while to feel its presence with all of hidden logic which needs to be studied carefully. At the end we will have separate session on refactoring this beast.

If you already failed with understanding implicitly stated logic — what this action does:

  • selects cards until two of them are selected
  • if figures are the same it waits 1 second and then marks them as “solved”. Additionally, of all cards were “solved” then status of the game is marked as “FINISHED”
  • if they aren’t the same — it waits 1 second and deselects them.

Ok, let’s move on.

Actions can be grouped into sequences

Game isn’t initialized yet. Even though we have a set of cards, they are not available for the user! We need to interact with the state. We decided to create action to only prepare some data and now we would include it into something bigger, called sequence. We’re going to call it gameInitialized. This is how it looks:

We imported sequence function that enables defining what input props (I like to call it payload to differentiate it from actual sequence’s props) are accepted when using this sequence. We are using just written action named randomizeCards and after it we put a new one — startGame. We didn’t defined it yet, so here it is:

This action simply updates underyling state with given arguments. Now our sequence can be wired with view part, e.g. React in our case.

Similarly to state from previous section, we need to add those sequences to module:

Cerebral and connection to UI

In this section we’re going to cover two things: transforming application state and changing UI based on state changes. Firstly we will see code behind this section and later we will give some explanations how things are working.

As you can see it isn’t scary piece of UI. One can even claim it’s under-engineered, but this is not story about building beautiful UI s— we’re managing application development complexity here!

Three imported things require explanation:

  • state, sequence — those two are Tags, I haven’t mentioned them through this post yet, but just briefly you can think about them as references to module’s state and defined sequences, respectively. You can read more here and here.
  • connect — this is so-called HOC — higher order component — in Cerebral’s approach it accepts dependencies and component to be wrapped — as a result it returns component with injected dependencies.

View (UI) can act upon a state (application) using sequences

In our example we passed two sequences that strictly related to sequences defined in previous sections. While we’re connecting them to component, we’re talking about Tags, but during runtime, for instance when we’re debugging component’s code, we’re talking about plain functions accepting plain object that reflects required sequence’s input. So we can use them as usual functions. Whenever user interacts with , e.g. <CardsCount /> component, function initialize is called which triggers underlying sequence which starts state transformation — our game begins!

Changes applied to state might reflect changes applied to view(UI)

Another two Tags connected as dependencies are references to state. Whenever any of them changes — component starts re-rendering process. While one might complain we’re injecting everything into root (App) component, I just want to say it’s only for demo purposes, but I hope you already got it.

One of passed state dependencies is gameStatus. You can see that we’re conditionally rendering respective components depending on status value. As you probably recall, whenever game is initialized (i.e. our initialize sequence was finished), status changes from “NOT_STARTED” to “PLAYING”. This state transformation is reflected in UI change. Simple, isn’t it?

Application should communicate with everything not being application itself using providers

Provider is a really simple term for separating logic and side-effects. Basically it’s plain object hiding some impure/effectful code— our application without any side-effects would be…Useless?

Usually people explain side-effects as interacting with web server using HTTP requests, writing/reading from database, working with images using canvas, etc. This boils down to reaching external resources — let’s assume this is good explanation of side-effects. In this particular case, we would see side-effects in the wild that for first glance doesn’t seem as side-effects.

Let me introduce…TIME!

Suppose someone gave us feedback that playing without any points is completely useless. We thought it’ll be fun to play against decreasing time. As a consequence, we can introduce points that would have some mathematical relationship with time. Let’s say each second left times 10 gives total points after finishing the game. When time is up — game is unsuccessfully finished and player can start again.

Let’s take it as a good habit and start with types. What shape would our Time provider have?

I think type itself is self explanatory. The only thing I would like you to pay attention to is return type of both methods. It’s void. It’s not an axiom, but usually when a function or method returns void you can assume it produces some effects. In this particular example we would interact with something causing flow of the time. Let’s see implementation of aforementioned interface:

According to Cerebral’s provider documentation, we can define our provider as factory function. If you don’t know what is a factory function — it’s a function that accepts some arguments and returns a function.

So our Time provider accepts name of a sequence to be run every interval and returns function that accepts Cerebral’s context and returns object with methods compliant with ITime interface. Internally, it sets timer to run obtained Cerebral’s sequence every second. As you probably noticed, this provider is responsible only for running side-effects in shape of executing some code for given interval. Nothing more. It’s caller’s responsibility to provide respective sequence and decide what to do on each execution. Pretty neat, right?

What’s more, it’s quite common to provide handy helpers when using providers. We can prepare useful actions that would utilize Time provider under the hood.

Next step is to create update state with points and currentTime and of course add timeUpdated sequence:

Last setup step is to add Time provider to module:

In order to enable time flow in our small game, we need to update our initializeGame sequence with startTimeFlow action:

Now whenever game was started, time is going to be inevitable part of each session!

STOP!

Have you noticed one subtle thing, namely that we’re tracking current time, but for the user we would like to display seconds left? How we should deal with that challenge?

Assumingly, total time available for each game session equals to cards count times constant — let’s say 4. We’re going to substract current time from total time. How we can implement such? For sure we have a piece of logic to be coded! Where to put it? Well, we have numerous options. Let’s think a bit about the simplest one — adding small component for displaying time left — we will call it <TimeLeft /> and put logic there.

Should this bother us? Usually it’s good to separate logic from UI and in this case we’re coupling them together. This means multiple things:

  • worse testability
  • no indication in Cerebral Debugger (result of substraction)
  • no logic reusability
  • and more

Don’t be scared, Cerebral comes with handy construct called computed. This small hero can react upon specific state change and return new value for underlying path in the state tree. Creating computed is easy thing so let’s jump into and have fun!

Routine, routine, routine — let’s add it to the state:

Now we can use this computed state as regular Tag when injecting it to React’s component. Here’s how it looks in exemplary<TimeLeft /> component:

We need to use <TimeLeft /> component in our app, but I believe you can do it on your own — as a nice break from Cerebral’s world.

Operators are configurable actions

I started becoming impatient, because I waited so long to reach this section! I hope we can reveal truly Cerebral’s powers and enhance ugly piece of code we saw in section “Application contains a set of actions” (here is the CODE, I used uppercase, because it’s overwhelming…). I’ll show step by step how we’re refactoring cardSelected action, during this process I’ll gradually move things outside of this action and build a sequence of actions out of it. I’ll start each subsection with code that will be followed by explanation.

Guard against clicking on already “solved” card

This is the plan what we’re going to do initially:

  • name action as selectCard
  • remove aforementioned line from this action
  • use when operator
  • change default export to use Cerebral’s sequence

Here’s new, shiny and altered code:

Previously we had “if” inside our action, but in above example we’re using when operator. What’s that? This is extremely powerful and expressive way of implementing logic. Using operators one might easily embed domain specific language into codebase and communicate clearly with people paying for building stuff (aka stakeholders for simplicity). Apart from that, operators give possibilities to reuse, share and abstract code. Under the hood operators are just functions accepting some arguments and returning Cerebral’s actions. We’re going to write our own operators, but you need to know that Cerebral has many useful operators baked into. So we cut one responsibility of initial action — checking whether card is already solved. Can you anticipate to what results does it lead to?

Just to finish about when operator — whenever used, it always needs to be followed by plain object with true/false properties containing either action or another sequence. Did I mention composability? You can read more in this introductory post.

Guard against clicking more than two cards

The time has come — you’re going to create your own operator! We’re going to isolate logic responsible for checking whether two cards are already selected and prevent further selections.

Our operator accepts Tag pointing to cards (in the state or props) uses resolve object from context to evalute Tag’s underyling value. We put some error handling to help future ourselves in debugging legacy code we’re creating right now. We won’t cover exception handling in Cerebral’s world, but you can check it in the docs.

Additionally, we’re using path object from context and invoke two functions: true and false. As you probably already guessed they are responsible for branching your code (surprise surprise!). Thanks to them we would be able to add a plain object with true/false properties after using our operator, the same as we did when we used when operator.

Here’s usage of newly created operator:

Looking for selected card index in cards collection

We’re going to combine checking index of found card from cards collection into findIndex operator. Basically we’ll lift well-known JS code into Cerebral’s syntax.

Ouch. This pain. Looks scary? Be brave. I bet first function resembles operator we created recently. Basically here’s the pattern that’s emerging while building new operator:

  • resolve (evaluate) passed tags using value method
  • handle missing data that can cause runtime exceptions
  • apply well separated logic to resolved values
  • use path object and return different cases after processing data
  • profit

But why second function? Isn’t the first one enough? Well, at least my practice shows that people (including me, you, etc.) tend to easily forgot what operator returns, expects after using it, etc. Second function accepts third argument which is object containing true/false properties. This construct is purely used for having better intellisense support. You can manage without it, but is it really worth to omit this couple of lines to live in eternal fear? I guess not ;-)

Here’s updated sequence:

Additionally we needed to rename selectCard action, because it no longer selects anything. I temporarily called it processSelectedCards. Check line number 13. As you probably noticed, it’s another handy built-in operator — it’s role is to set some value under given Tag. Our findIndex operator adds index of found card into props in the context. We’re composing it with state Tag to update underlying card’s selection flag.

Checking if exactly two cards were selected and getting them

Didn’t we implement exactly first part of this subsection? Yes, you’re right. We have areExactlyTwoSelected operator. We would need to extend it by returning selected cards in “true” path. It shouldn’t be that hard.

You’ve already seen that we can add result of an action to the sequence execution context by passing an object in path’s invocated method. Now we can reach for those selected cards using props.selectedCards Tag.

Unfortunately, this refactoring step was bigger than we expected. Because we factored out code responsible for checking number of selected cards and getting their indexes, we need to update our processSelectedCards action. Thankfully, we have TypeScript so entire process went really smoothly. So firstly we’re going to see how entire sequences looks like:

Can you believe that? We are so lucky! We’ve already created findIndex operator so we can easily get selected cards indexes. We know that areExactlyTwoSelected operator adds tuple of objects with type of Card into the execution context so we can combine them with findIndex operator. We handled “some” path by writing respective indexes into props Tag. They will be available until the end of sequence execution. As I mentioned, we needed to update processSelectedCards action.

Can you see what changed? We needed to update all occurrences of previously declared variables that we’re obtained by intermediate operations. Now all mentioned variables are available by accessing them from execution props object.

It’s time to recap what’s left:

  • checking whether two selected cards are equal
  • if they are the same, deselecting cards and marking them as “solved” after specified time
  • if they are not the same, deselecting cards after specified time
  • checking if game is finished by verifying whether all cards are “solved”

We are almost there, keep up the good work!

Refactoring missing pieces

This time entire implementation of processSelectedCards changed. We moved from single action doing multiple things to a highly readable sequence with operators .

There are three new operators:

  • wait — responsible for waiting (surprise surprise) for given amount of time, available in “cerebral/factories” package
  • areCardsEqual — accepts two Tags pointing to cards and compare them based on their figures
  • isGameFinished — accepts collection of cards and checks if all are “solved”

As you probably noticed, we used when operator again, but this time we passed a function as last argument that works as a predicate and checks if two cards are equal based on their figures.

It’s the final countdown!

That was really long, but rewarding journey. We gained new experience in using Cerebral to build small game. But are we finished? Not yet, unfortunately. We have two unhandled pieces of logic: losing the game when time is up and counting points when player won the game.

Losing the game :-(

In order to handle this state of the game, we need to introduce new status. Let’s call it “PLAYER_LOSE”. Doing so requires changing existing case, namely “FINISHED”. It no longer gives any meaning, especially we would have it’s antagonistic friend. Let’s rename “FINISHED” to “PLAYER_WIN”. It’s merely changing three occurrences of this string so I think you can handle it by yourself.

We said that player loses the game when time left is equal to zero. So we need to adjust timeUpdated sequence. Here are changes:

I think it’s really readable and verbose what we’re doing here. It’s almost 1:1 what we described as termination condition for losing the game. We also need to show meaningful, but sad message to the player:

Counting player’s points

This is more joyful scenario we will implement right now. Let’s quickly think what’s relationship between points and game status. Basically, points change only when game status changes to “PLAYER_WIN”, in other cases they have zero value. We can put one action in cardSelected sequence to count points when game is finished, but this would require to update initialization sequence by reseting points to zero. Wouldn’t it be better to hit two birds by one stone? Of course, that’s why we will use computed to count points.

We created computed that reacts upon each gameStatus value change. Only when status is “PLAYER_WIN” we’re counting points by using timeLeft computed. Otherwise, we return zero for all other cases. We don’t need to reset any state, because this computed does it for us. What a good ally!

Counting points without showing them to the player after finishing the game sounds irrational. We need to update our main component with adding points to the message:

Summary

Well, if you’re reading those words, it means we reached the end of having fun. Sorry, things aren’t eternal. Hopefully, you enjoyed entire adventure and now you feel at least a little bit more confident while writing web apps using Cerebral.

What we’ve learned so far?

  • Cerebral is awesome
  • we can group related code using modules (as we did in game module)
  • application contains state defined as plain object (in our case we kept entire game’s state, e.g. points, status, card, etc.)
  • we can use actions to implement some logic (for instance we used randomizeCards to generate randomly shuffled)
  • we group actions using sequences to encapsulate some bigger logic (as we did in initializeGame sequence)
  • we can interact with the state using sequences which causes UI to update
  • we can push usage of external resources to the boundaries of our application by using providers (we isolated flowing time)
  • we can lift actions to operators that provide better reusability and readability (we chopped big cardSelected action into sequence with many operators that are reusable)
  • Cerebral promotes building expressive sequences (we saw that during refactoring when we moved line by line)
  • if you used Cerebral Debugger — you get comprehension how application works by using this tool

If you struggled with anything during this post, you can find list of branches that contain some of intermediate steps:

I hope everything was clear and interesting. If you have any questions, please leave your comment below or write to me on Twitter. That’s all folks, see you next time!

--

--