Créons un Cookie Clicker avec RxJS
This post is also available in English
En ce moment, je suis à fond sur Kittens game, et ça m’a donné envie de recommencer mon Cookie Clicker en RxJS. J’en avais fait un l’an dernier, pour tester RxJS (v5). Le résultat était fonctionnel, mais techniquement dégueulasse. Maintenant que j’ai beaucoup plus d’expérience avec Rx (merci Angular), j’ai eu envie de m’y remettre!
Ce qu’on va faire ici
On va mettre en place la base du jeu. Notre monnaie sera le bit
, avec 3 fermes (56k, ADSL, Fibre) qui génèreront plus ou moins de bits. A la fin de cet article, on aura ces fermes qui génèreront des bits à chaque seconde.
On va utiliser RxJS v5.5, pour utiliser les nouveaux pipes, qui vont sûrement devenir la norme. Pour la compilation, on va tester webpack 4 en version beta, parce que pourquoi pas.
Installation
J’utilise nvm pour changer de version de node entre mes projets. Si vous ne l’utilisez pas, le projet a été développé avec node 9.4.0, mais il devrait fonctionner avec des versions inférieures.
# Init
git init
# On fixe la version de Node à 9.4.0
nvm use 9.4.0
node --version > .nvmrc
npm init --force
# On installe tous nos outils
npm i -D webpack-dev-server@next webpack@next webpack-cli
npm i -D @reactive/rxjs
npm i -D husky lint-staged prettier-standard
Les 2 premières lignes de npm install
sont assez évidentes, la 3ème installe une version pré-configurée de Prettier et tout ce qu’il faut pour mettre un pre-commit hook qui formattera tout seul notre code.
Le DOM
On va partir d’une div simple, basique, et on va créer tout notre jeu (moche) en Javascript pur. Comme on va créer plusieurs boutons de fermes, on va se faire une fonction qui prendra un nom et une valeur, et qui nous renverra le DOMElement du bouton créé.
const game = document.getElementById("game");
const createButton = (name, initialValue = 1) => {
const newButton = document.createElement("button");
const textSpan = document.createElement("span");
textSpan.innerHTML = `${name} `;
// On sépare le nom et la valeur,
// parce qu'on voudra faire évoluer la valeur dans le futur
const valueSpan = document.createElement("span");
valueSpan.innerHTML = initialValue;
newButton.appendChild(textSpan);
newButton.appendChild(valueSpan);
return newButton;
};
On va mettre tous ces boutons dans une liste, donc on va se faire une petite fonction pour mettre notre bouton dans un <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));
On veut aussi afficher le total, il nous faut un champ pour ça.
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);
Et on met tout ça dans la page
game.appendChild(total);
game.appendChild(buttonList);
Et Rx dans tout ça?
La philosophie de Rx, c’est de considérer que tout est un flux.
Un flux est initié par quelque chose (un clic, un appel réseau, un timer) et émet des évènements (un MouseEvent, un json, un timestamp).
A partir de ces flux, on va les transformer et les fusionner, pour obtenir le résultat souhaité. Le but, c’est d’avoir le moins possible de code statique const truc = monObservable.value()
pour garder la notion de flux le plus possible, de la création des évènements jusqu’à l’affichage de nombre de bits.
Maintenant qu’on a notre DOM, créons toutes nos sources d’évènements. Par convention, les variables finissant par $ sont des flux d’évènements, également appelés streams.
// Un tick enverra un évènement toutes les secondes
const tick$ = interval(1000);
// On observe les clics sur nos fermes
const simple$ = Observable.fromEvent(simpleButton, "click");
const medium$ = Observable.fromEvent(mediumButton, "click");
const high$ = Observable.fromEvent(highButton, "click");
Les évènements émis par les boutons sont des MouseEvent, mais les infos du clic nous importent peu. On va donc transformer cet évènement, en le replaçant par la valeur de la ferme.
const simpleMapped$ = simple$.pipe(mapTo(1));
const mediumMapped$ = medium$.pipe(mapTo(5));
const highMapped$ = high$.pipe(mapTo(10));
Il ne reste qu’à additionner tout ça, pour obtenir la somme totale, et l’afficher.
const scanSum = scan((acc, next) => acc + next, 0);
// La somme de chacune des fermes
const totalSum$ = merge(simpleMapped$, mediumMapped$, highMapped$)
.pipe(scanSum);
// A chaque tick, on ne garde que la somme (le tick ne sert pas)
tick$.withLatestFrom(totalSum$, (tick, sum) => sum)
// Qu'on additionne aux valeurs précédentes
.pipe(scanSum)
// Et on affiche ça dans le champ totalValue
.subscribe(total => {
totalValue.innerHTML = total;
});
C’est tout pour le moment !
Le projet est disponible sur Framagit (tag v2.0.4) et testable grâce au déploiement automatique des Gitlab pages.
Un grand merci à Framasoft pour l’hébergement du Gitlab et des Gitlab pages!
La suite
Dans les prochains articles, on va:
- ajouter un bouton qui donnera un bit à chaque clic (pour amasser nos premiers bits au début du jeu),
- bloquer les mises à jour de fermes, selon l’argent qu’on possède,
- rajouter un système d’amélioration de fermes, pour qu’elles génèrent de plus en plus de bits,
- et peut-être autre chose.
Références
- Webpack 4: https://medium.com/webpack/webpack-4-beta-try-it-today-6b1d27d7d7e2
- Les pipes dans RxJS 5.5: https://blog.hackages.io/rxjs-5-5-piping-all-the-things-9d469d1b3f44
Article originellement publié sur mon site personnel