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.