Coderstand

Let's create a Cookie Clicker with RxJS

tags: Javascript , RxJS

28/01/2018 (c'était un dimanche)

These times, I’m playing Kittens game a lot, and it motivated me to restart my Cookie Clicker with RxJS. I previously made one last year, to test RxJS (v5). The result was fonctionning, but technically it was crap. Now that I know a lot more about Rx (thanks Angular), it’s a good time to start it again!

What are we gonna do

We’re gonna create our game’s base system. Our money will be the bit, with 3 farms (56k, DSL, Fiber) generating more or less bits. At the end if this post, we’ll have these farms, generating bits every second.

We will use RxJS v5.5, to test the new pipe system, because it’s clearly RxJS’s future. We will use webpack 4 (beta version) for compilation, because why not.

Installation

I use nvm to swap Node’s versions between projects. If you don’t, this project has been developed using node 9.4.0, but it should work with older versions.

# Init
git init

# Fixing  Node to 9.4.0
nvm use 9.4.0
node --version > .nvmrc
npm init --force

# Installing dependencies
npm i -D webpack-dev-server@next webpack@next webpack-cli
npm i -D @reactive/rxjs
npm i -D husky lint-staged prettier-standard

This first 2 npm install lines are obvious, the third one installs a pre-configured version of Prettier and everything needed to have a pre-commit hook to automatically format our code.

The code!

The DOM

We are starting from a simple div, and creating all our (ugly) game interface in Javascript. Since we are creating multiple farm buttons, let’s create a function which takes one name and a value, and give us the created DOMElement in response.

const game = document.getElementById("game");

const createButton = (name, initialValue = 1) => {
  const newButton = document.createElement("button");
  
  const textSpan = document.createElement("span");
  textSpan.innerHTML = `${name} `;
  // Separating name and value in 2 spans, 
  // because we know we'll modify the value in a future post
  const valueSpan = document.createElement("span");
  valueSpan.innerHTML = initialValue;

  newButton.appendChild(textSpan);
  newButton.appendChild(valueSpan);
  return newButton;
};

Let’s put all these buttons in a list, so let’s create a small function to wrap a button in a <li></li>.

const liForButton = button => {
  const newButtonInLi = document.createElement("li");
  newButtonInLi.appendChild(button);
  return newButtonInLi;
};

const buttonList = document.createElement("ul");

const simpleButton = createButton("56k");
buttonList.appendChild(liForButton(simpleButton));
const mediumButton = createButton("DSL", 5);
buttonList.appendChild(liForButton(mediumButton));
const highButton = createButton("Fiber", 10);
buttonList.appendChild(liForButton(highButton));

We also want to show the total, we need a field for this.

const total = document.createElement("span");
total.innerHTML = "Total: ";
const totalValue = document.createElement("span");
const unit = document.createElement("span");
unit.innerHTML = " bits";
total.appendChild(totalValue);
total.appendChild(unit);

And finally we put all in the game div.

game.appendChild(total);
game.appendChild(buttonList);

Where is Rx in all of this?

Rx’s philosophy is that everything is a stream.

A stream is created by something (a click, a network request, a timer) and emit events (a MouseEvent, a json, a timestamp).

Using these streams, we are gonna transform and merge them, to obtain our wanted result. The goal is to have the least static const truc = monObservable.value() code possible, and keep a ‘stream’ spirit everywhere, from event’s creation to showing the bits number.

Now that the DOM is finished, let’s create our events. By convention, every variable ending with $ are event’s streams.

// A tick will send an event every second
const tick$ = interval(1000);

// Observing our farm's events
const simple$ = Observable.fromEvent(simpleButton, "click");
const medium$ = Observable.fromEvent(mediumButton, "click");
const high$ = Observable.fromEvent(highButton, "click");

Events emitted by the buttons are MouseEvent, but we don’t care about the clic’s informations. We need to transform the event to replace it with the farm’s value.

const simpleMapped$ = simple$.pipe(mapTo(1));
const mediumMapped$ = medium$.pipe(mapTo(5));
const highMapped$ = high$.pipe(mapTo(10));

Now we just need to sum it all and show it.

const scanSum = scan((acc, next) => acc + next, 0);
// The sum of all farm
const totalSum$ = merge(simpleMapped$, mediumMapped$, highMapped$)
  .pipe(scanSum);

// On each tick, we are keeping only the sum value (we don't care about the tick)
tick$.withLatestFrom(totalSum$, (tick, sum) => sum)
// And we add this to the previous value
.pipe(scanSum)
// And showing it in the totalValue field
.subscribe(total => {
  totalValue.innerHTML = total;
});

That’s all folks !

This project is available on Framagit (tag v2.0.4) and playable thanks to Gitlab pages auto deploy.

Big thanks to Framasoft for hosting Gitlab and Gitlab Pages!

Next time

In the next posts, we’ll:

  • add a button which gives one bit per click (to start the game, before buying the first farm),
  • block the farm’s upgrade, depending on the money we own,
  • add a farm’s upgrade system, to allow them to create more bits,
  • and maybe other things.

References


Brice Coquereau

Bonjour! Je suis Brice Coquereau, parisien, développeur, flexitarien, et amateur de chats. Je vais à des meetups assez souvent. Venez me parler sur Mastodon ou Twitter (mais Mastodon c'est mieux)

TwitterFacebookLinkedin