presque.cool
Back to projects
Slide & Sorcery icon

Slide & Sorcery

Mar 2026 Game
Visit project

A 4×4 grid dungeon crawler inspired by 2048. Every tile slide triggers combat, combos, and survival decisions.


Le pitch

Slide & Sorcery est un dungeon crawler sur grille 4x4. Un swipe déplace toutes les tuiles d’un coup : explosions, contre-attaques, combos s’enchaînent plus vite qu’on ne les anticipe. On n’orchestre pas chaque effet, on oriente le chaos. Quand un run se termine, l’or accumulé débloque une bénédiction pour le suivant.

Jouer : presque.cool/slide-sorcery/

Durée ~3 semaines
Période Mars 2026
Stack
React TypeScript Zustand PWA
Statut ✓ En production

Pourquoi ce projet

2048 et Threes reposent sur une mécanique simple : glisser des tuiles dans une direction, tout ce qui peut fusionner fusionne. L’idée était de garder ce geste mais de le pousser ailleurs.

Des ennemis à la place des chiffres. Des armes, des sorts, des bombes. Dans 2048, tu sais exactement ce qu’il va se passer avant de swiper. Ici, pas vraiment. Tu slides, des choses explosent, une chaîne se déclenche, un ennemi contre-attaque. Le jeu reste lisible, mais il déborde — et c’est là que c’est intéressant.


Le moteur de jeu

La première version gérait les collisions directement dans le store Zustand. Ça fonctionnait, mais la logique de jeu et les états de présentation étaient entremêlés. Une refonte a séparé les deux couches proprement.

src/engine/ ne contient que du TypeScript pur, sans import React. gameLogic.ts et resolveCollision.ts prennent un état en entrée et retournent le suivant — des fonctions pures. Zustand ne fait que stocker le résultat et nourrir les composants.

Cette séparation a rendu les ajouts de mécaniques beaucoup moins risqués. Chaque nouveau type de tuile (checkpoint, armes, boucliers) s’est branché sur le moteur sans toucher au rendu.


La boucle roguelite

Entre deux runs, l’or collecté s’accumule dans un coffre persistant. Avec 50 pièces, le joueur peut débloquer une bénédiction avant de lancer le run suivant.

4 bénédictions disponibles :

  • Bombe de départ : une bombe supplémentaire dès le premier tour.
  • Épée de départ : une arme disponible immédiatement.
  • +1 PV max : plafond de santé relevé à 4.
  • +10% combo : multiplicateur de score pour les combos.

Aucune progression permanente : la bénédiction s’applique au run en cours uniquement. Le jeu reste court et équilibré sans persistance complexe.


Combos et pity system

C’est là que le jeu tient ou se casse.

Combos : éliminer 2 menaces ou plus en un seul coup déclenche un combo. Des combos consécutifs forment un streak. À x3 : bonus de score. À x5 : or bonus. La récompense est exponentielle, la perte immédiate : un coup sans élimination casse tout.

Pity system : les spawns de nourriture et de pièces sont aléatoires, mais le moteur surveille le nombre de tours depuis le dernier spawn de chaque type. Après 3 tours sans nourriture, la probabilité d’en voir apparaître augmente automatiquement. Idem pour les pièces. Un anti-streak similaire existe pour les crânes : trop d’ennemis consécutifs amortit la prochaine vague.

Le résultat : le jeu reste difficile sans devenir arbitrairement injuste. Un joueur à court de ressources en voit arriver un peu plus vite, sans que ce soit affiché ou annoncé.


Audio procédural

Aucun fichier audio dans le projet. Chaque son est synthétisé en temps réel via la Web Audio API : oscillateurs, enveloppes courtes, micro-délais pour empiler les events d’un même coup. Un pickup de pièce joue une note différente d’une explosion de bombe, qui sonne différemment d’un hit ennemi.

Avantages concrets : aucun asset lourd à charger, audio désactivable sans rechargement, contenu safe en offline.


Stack technique

  • React 19 + TypeScript
  • Vite 8 comme bundler — découpage vendor (react, Font Awesome) pour réduire le bundle initial
  • Tailwind CSS v4 en mode CSS-first (tokens OKLCH)
  • Zustand 5 — state management, persistance localStorage et IndexedDB
  • Font Awesome Duotone — icônes des tuiles (ennemis, objets, nourriture)
  • Web Audio API — audio procédural, aucun fichier sonore
  • PWA via vite-plugin-pwa (Workbox, installable, offline)
  • i18n maison (français et anglais)
  • CSP — Content-Security-Policy avec hashes auto-générés
  • Compression — Brotli + Gzip sur les assets

Architecture en couches : moteur de jeu pur dans src/engine/ (TypeScript strict, zéro import React), store Zustand en coquille UI, composants sans logique métier.


Évolutions

DateVersionDescription
Mars 20261.0Version initiale
Avril 20262.0Moteur refactorisé, blessings, audio procédural, PWA

Bilan

Durée : ~3 semaines (mars-avril 2026).

Ce que j’ai appris :

  • Construire un moteur de jeu en fonctions pures, découplé de React — la refonte a validé que séparer les deux couches dès le début aurait évité du travail en double
  • Équilibrer un système de spawn probabiliste — les constantes ne sont jamais arbitraires, elles résultent de tests et d’ajustements successifs
  • Synthétiser de l’audio procédural avec la Web Audio API sans librairie tierce

Ce que je referais pareil :

  • Moteur pur dans engine/, zéro import React — c’est contraignant au début, payant ensuite

Ce que je changerais :

  • Les bénédictions sont trop peu nombreuses : 4 options, c’est bien pour un MVP, mais une vingtaine rendrait les runs beaucoup plus variés.

The pitch

Slide & Sorcery is a dungeon crawler on a 4×4 grid. One swipe moves everything at once: explosions, counter-attacks, combos stack up faster than you can track. You don’t control every effect — you steer the chaos. When a run ends, the gold you collected unlocks a blessing for the next one.

Play it: presque.cool/slide-sorcery/

Duration ~3 weeks
Period Mar 2026
Stack
React TypeScript Zustand PWA
Status ✓ In production

Why this project

2048 and Threes share a simple mechanic: slide tiles in one direction, merge what can merge. The idea here was to keep that gesture but push it somewhere else.

Enemies instead of numbers. Weapons, spells, bombs. In 2048, you always know what will happen before you swipe. Here, not really. You slide, things explode, a chain triggers, an enemy counter-attacks. The game stays readable, but it overflows a little — and that’s what makes it interesting.


The game engine

The first version handled collisions directly inside the Zustand store. It worked, but game logic and presentation state were tangled together. A refactor split the two layers cleanly.

src/engine/ contains only pure TypeScript, no React imports. gameLogic.ts and resolveCollision.ts take a state and return the next one — pure functions. Zustand just stores the result and feeds the components.

This separation made adding mechanics far less risky. Each new tile type (checkpoint, weapons, shields) plugged into the engine without touching the rendering layer.


The roguelite loop

Between runs, collected gold accumulates in a persistent vault. With 50 coins, the player unlocks a blessing before the next run.

4 blessings available:

  • Starter bomb: an extra bomb from turn one.
  • Starter sword: a weapon available immediately.
  • +1 max HP: health cap raised to 4.
  • +10% combo: score multiplier for combos.

No permanent progression: the blessing applies to the current run only. The game stays short and balanced without complex persistence.


Combos and pity system

This is where the game holds together or breaks apart.

Combos: eliminating 2 or more threats in one move triggers a combo. Consecutive combos build a streak. At ×3: score bonus. At ×5: gold bonus. The reward is exponential, the loss immediate: one move without an elimination resets everything.

Pity system: food and coin spawns are random, but the engine tracks how many turns have passed since the last spawn of each type. After 3 turns without food, the probability of one appearing increases automatically. Same for coins. A similar anti-streak exists for skulls: too many consecutive enemies dampens the next wave.

The result: the game stays hard without becoming arbitrarily unfair. A player running low on resources sees them arrive a bit faster — no display, no announcement.


Procedural audio

No audio files in the project. Every sound is synthesized in real time via the Web Audio API: oscillators, short envelopes, micro-delays to stack events from the same move. A coin pickup plays a different note than a bomb explosion, which sounds different from an enemy hit.

Practical benefits: no heavy assets to load, audio togglable without reloading, safe for offline.


Tech stack

  • React 19 + TypeScript
  • Vite 8 as bundler — vendor splitting (react, Font Awesome) to reduce initial bundle
  • Tailwind CSS v4 in CSS-first mode (OKLCH tokens)
  • Zustand 5 — state management, localStorage and IndexedDB persistence
  • Font Awesome Duotone — tile icons (enemies, items, food)
  • Web Audio API — procedural audio, no sound files
  • PWA via vite-plugin-pwa (Workbox, installable, offline)
  • Custom i18n — French and English
  • CSP — Content-Security-Policy with auto-generated hashes
  • Compression — Brotli + Gzip on assets

Layered architecture: pure game engine in src/engine/ (strict TypeScript, zero React imports), Zustand as the UI shell, components with no business logic.


Timeline

DateVersionDescription
Mar 20261.0Initial version
Apr 20262.0Refactored engine, blessings, procedural audio, PWA

Takeaways

Duration: ~3 weeks (March-April 2026).

What I learned:

  • Building a game engine as pure functions, decoupled from React — the refactor confirmed that separating the two layers from day one would have saved duplicate work
  • Balancing a probabilistic spawn system — the constants are never arbitrary, they come from testing and successive adjustments
  • Synthesizing procedural audio with the Web Audio API without a third-party library

What I’d do the same:

  • Pure engine in engine/, zero React imports — constrained at first, pays off later

What I’d change:

  • The blessings pool is too small: 4 options is fine for an MVP, but a set of twenty would make runs feel much more varied.

Settings

Language
Theme
Privacy Policy