---
title: grid4
canonical: "https://presque.cool/projects/grid4/"
description: "Un jeu de gestion de ressources déguisé en puzzle casual. Grille 4×4, cartes à incrémenter, combos à enchaîner — chaque tap compte."
---

<div data-lang="fr">

## Le pitch

Grid4 est un jeu de puzzle minimaliste sur une grille 4×4. Le principe : on tape sur des cartes pour augmenter leur valeur (1→2→3... jusqu'à 9), et quand on crée un groupe de 4 cartes identiques ou plus connectées entre elles, on marque des points et on récupère des actions. La subtilité : chaque tap consomme un point d'action. Si on tombe à zéro sans créer de combo, c'est game over.

C'est un jeu de gestion de ressource déguisé en puzzle casual.

**Jouer** : [presque.cool/grid4/](https://presque.cool/grid4/)

<div class="quick-overview">
  <div class="overview-item">
    <span class="overview-label">Durée</span>
    <span class="overview-value">~1 mois</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Période</span>
    <span class="overview-value">Nov 2025</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Stack</span>
    <div class="tech-tags">
      <span class="tech-tag">React</span>
      <span class="tech-tag">TypeScript</span>
      <span class="tech-tag">Vite</span>
      <span class="tech-tag">PWA</span>
    </div>
  </div>
  <div class="overview-item">
    <span class="overview-label">Statut</span>
    <span class="overview-value accent">✓ En production</span>
  </div>
</div>

---

## Pourquoi ce projet

Grid4 est né d'une envie simple : **approfondir React après un premier projet** ([Dice Roller](/projects/dice-roller/)). Pas un énième todo-list ou un clone Twitter, mais un petit jeu complet, jouable, publiable.

L'idée n'est pas originale — et je le revendique. Grid4 est librement inspiré de [Cards +1](https://twotinydice.itch.io/) de Twotinydice, un jeu mobile que j'ai trouvé élégant dans sa simplicité. C'est crédité directement dans le jeu. Je ne voulais pas réinventer la roue, je voulais comprendre comment elle tourne.

Trois objectifs guidaient ce projet :

1. **Approfondir React** — hooks, state management, performance, les aspects avancés.
2. **Faire de la rétro-ingénierie de game design** — décortiquer un jeu existant pour comprendre ce qui le rend satisfaisant.
3. **Finir quelque chose** — un projet complet, de l'idée au déploiement, pas un prototype abandonné dans un dossier.

---

## Rétro-ingénierie : ce qui se voit et ce qui se cache

Quand on joue à Cards +1, la boucle de gameplay se comprend en 30 secondes. On tape, les chiffres montent, les groupes disparaissent, on recommence. Simple.

Mais reproduire cette sensation représente un autre défi.

**Ce qui était évident** en jouant :

- La mécanique de base (tap → incrément → match → score)
- La règle des groupes adjacents (orthogonaux, pas diagonaux)
- La tension de la ressource qui s'épuise

**Ce qui était caché** :

- Les edge cases mécaniques — les chiffres remplacés après un match doivent-ils être entièrement aléatoires ? Faut-il un algorithme pour garantir que chaque grille reste jouable ? Comment gérer les "matchs en cascade" quand les nouvelles cartes forment immédiatement un nouveau groupe ?
- La progression de difficulté — comment le jeu reste intéressant après 5000 points ?
- Et surtout : **le game feel**. La vitesse des animations. Le timing des sons. La réactivité du tap. Ces micro-détails qui font qu'un jeu se _sent_ bien ou non.

C'est là que j'ai le plus appris. Le game feel ne se lit pas dans un GDD, il se ressent et se reproduit par itération.

---

## Le défi technique : React et la performance

React n'est pas fait pour les jeux. C'est un framework pensé pour des interfaces, pas pour du rendu à 60fps avec des cascades d'animations. J'ai dû relever plusieurs défis.

**Les symptômes** :

- Lag sur les animations de tap
- Re-renders en cascade quand plusieurs cartes changeaient d'état
- Micro-freezes pendant le calcul des groupes adjacents

**Le diagnostic** :
Un mix de React DevTools, de console.log stratégiques, et de ressenti direct. Pas très scientifique, mais efficace.

**Les solutions** :

L'approche générale : **faire le maximum avec le minimum**. Chaque ligne de code, chaque dépendance, chaque re-render a un coût. Dans un jeu où la fluidité est critique, ce coût se ressent immédiatement.

Concrètement :

- **Mémoisation agressive** — `GameCard` et `GameBoard` sont mémoïsés avec des comparaisons custom pour éviter les re-renders inutiles.
- **Restructuration du state** — séparer ce qui change souvent de ce qui change rarement, pour que React ne recalcule que le nécessaire.
- **`useCallback` et `useMemo`** là où c'est nécessaire — les handlers, les valeurs dérivées, tout ce qui peut être mis en cache.
- **`startTransition`** pour les updates non-critiques comme les stats de session.
- **Code splitting** — les dialogs (règles, stats, settings, game over) sont lazy-loadés.

Le résultat : un jeu fluide, même sur mobile.

---

## Architecture : traiter un "petit jeu" comme un vrai produit

Une chose que je voulais éviter : le code spaghetti du prototype qui "fonctionne mais qu'on n'ose plus modifier". Grid4 devait être maintenable.

**Séparation des responsabilités** :

- Un **noyau pur** (data + algorithmes) : types, calcul de voisinage, BFS pour les groupes, scoring. Pas de React ici, juste de la logique testable.
- Une **couche de génération** : spawn pondéré, initialisation de grille, remplissage après les matchs.
- Des **hooks React spécialisés** : un pour la détection des matchs, un pour les actions, un pour le flow global.
- Un **context provider** qui assemble le tout pour l'app.

**Machine à états explicite** :
Le jeu formalise trois états — `idle` (le joueur peut agir), `processing` (résolution en cours), `game_over`. Cela évite les bugs de race condition où le joueur clique pendant qu'une animation tourne.

---

## Stack technique

<ul class="tech-stack">
<li><strong>React</strong> + <strong>TypeScript</strong></li>
<li><strong>Vite</strong> comme bundler (avec SWC)</li>
<li><strong>Tailwind CSS</strong> en mode CSS-first</li>
<li><strong>Web Audio API</strong> pour les sons (via un singleton AudioManager)</li>
<li><strong>PWA</strong> — installable, offline</li>
<li><strong>i18n</strong> maison (français/anglais)</li>
<li><strong>Zustand</strong> pour le state management — persistance localStorage (paramètres) + IndexedDB (progression)</li>
<li><strong>Compression</strong> — Brotli + Gzip sur les assets</li>
<li><strong>CSP</strong> — Content-Security-Policy avec hashes auto-générés</li>
</ul>

Pas de frameworks UI, pas de libs d'animation lourdes. Le moins de dépendances possible.

---

## Évolutions

| Date     | Version | Description                                                                |
| -------- | ------- | -------------------------------------------------------------------------- |
| Nov 2025 | 1.1.0   | Polish & performances — Animations CSS, ajout audio, optimisations         |
| Nov 2025 | 1.0.0   | Déploiement & PWA — Support sous-répertoire, PWA, stabilisation            |
| Nov 2025 | 0.3.0   | Settings & i18n — Internationalisation FR/EN, thèmes/palettes, persistance |
| Nov 2025 | 0.2.0   | Qualité & accessibilité — Navigation clavier, annonces SR, refactors       |
| Nov 2025 | 0.1.0   | Prototype jouable — Mécanique de base (tap → incrément → match) et scoring |

---

## Bilan

**Durée** : environ 1 mois.

**Ce que j'ai appris** :

- React en profondeur — pas seulement "cela fonctionne", mais "pourquoi cela re-render et comment l'éviter"
- La rétro-ingénierie de game design — analyser un jeu pour comprendre ses mécaniques implicites
- Le game feel ne s'improvise pas — c'est du travail d'itération sur les détails

**Ce que je referais pareil** :

- Partir d'une inspiration existante plutôt que réinventer. Cela permet de se concentrer sur l'exécution.
- Traiter le projet comme un produit fini, pas un prototype.

**Ce que je changerais** :

- J'ajouterais plus de variété côté contenu avec des **modificateurs de run** (ex: "coût des actions +1", "combo minimum à 5", "tuiles gelées"), pour renouveler la stratégie sans changer le cœur des règles.
- J'ajouterais aussi des **cartes spéciales** (bonus temporaires, effets de zone, cartes à risque/récompense) pour enrichir les choix tactiques et créer des runs plus mémorables.

</div>

<div data-lang="en">

## The pitch

Grid4 is a minimalist puzzle game on a 4×4 grid. The concept: tap cards to increase their value (1→2→3... up to 9), and when you create a group of 4 or more identical connected cards, you score points and recover actions. The twist: each tap consumes an action point. If you run out without creating a combo, it's game over.

It's a resource management game disguised as a casual puzzle.

**Play**: [presque.cool/grid4/](https://presque.cool/grid4/)

<div class="quick-overview">
  <div class="overview-item">
    <span class="overview-label">Duration</span>
    <span class="overview-value">~1 month</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Period</span>
    <span class="overview-value">Nov 2025</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Stack</span>
    <div class="tech-tags">
      <span class="tech-tag">React</span>
      <span class="tech-tag">TypeScript</span>
      <span class="tech-tag">Vite</span>
      <span class="tech-tag">PWA</span>
    </div>
  </div>
  <div class="overview-item">
    <span class="overview-label">Status</span>
    <span class="overview-value accent">✓ In production</span>
  </div>
</div>

---

## Why this project

Grid4 was born from a simple desire: **deepen my React skills after a first project** ([Dice Roller](/projects/dice-roller/)). Not another todo-list or Twitter clone, but a small complete game, playable, publishable.

The idea isn't original — and I own that. Grid4 is freely inspired by [Cards +1](https://twotinydice.itch.io/) by Twotinydice, a mobile game I found elegant in its simplicity. It's credited directly in the game. I didn't want to reinvent the wheel, I wanted to understand how it turns.

Three goals guided this project:

1. **Deepen React knowledge** — hooks, state management, performance, the advanced aspects.
2. **Reverse-engineer game design** — dissect an existing game to understand what makes it satisfying.
3. **Finish something** — a complete project, from idea to deployment, not a prototype abandoned in a folder.

---

## Reverse engineering: what's visible and what's hidden

When you play Cards +1, the gameplay loop is understood in 30 seconds. You tap, numbers go up, groups disappear, you start again. Simple.

But reproducing that feeling is another challenge.

**What was obvious** while playing:

- The core mechanic (tap → increment → match → score)
- The adjacent group rule (orthogonal, not diagonal)
- The tension of the depleting resource

**What was hidden**:

- Mechanical edge cases — should numbers replaced after a match be entirely random? Do you need an algorithm to ensure each grid remains playable? How to handle "cascade matches" when new cards immediately form a new group?
- Difficulty progression — how does the game stay interesting after 5000 points?
- And above all: **game feel**. Animation speed. Sound timing. Tap responsiveness. These micro-details that make a game _feel_ good or not.

That's where I learned the most. Game feel can't be read in a GDD, it's felt and reproduced through iteration.

---

## The technical challenge: React and performance

React isn't made for games. It's a framework designed for interfaces, not for 60fps rendering with cascading animations. I had to overcome several challenges.

**The symptoms**:

- Lag on tap animations
- Cascading re-renders when multiple cards changed state
- Micro-freezes during adjacent group calculation

**The diagnosis**:
A mix of React DevTools, strategic console.logs, and direct feeling. Not very scientific, but effective.

**The solutions**:

The general approach: **do the maximum with the minimum**. Every line of code, every dependency, every re-render has a cost. In a game where fluidity is critical, this cost is immediately felt.

Concretely:

- **Aggressive memoization** — `GameCard` and `GameBoard` are memoized with custom comparisons to avoid unnecessary re-renders.
- **State restructuring** — separate what changes often from what changes rarely, so React only recalculates what's necessary.
- **`useCallback` and `useMemo`** where needed — handlers, derived values, everything that can be cached.
- **`startTransition`** for non-critical updates like session stats.
- **Code splitting** — dialogs (rules, stats, settings, game over) are lazy-loaded.

The result: a smooth game, even on mobile.

---

## Architecture: treating a "small game" like a real product

One thing I wanted to avoid: the spaghetti code of a prototype that "works but you don't dare modify anymore". Grid4 had to be maintainable.

**Separation of concerns**:

- A **pure core** (data + algorithms): types, neighbor calculation, BFS for groups, scoring. No React here, just testable logic.
- A **generation layer**: weighted spawn, grid initialization, filling after matches.
- **Specialized React hooks**: one for match detection, one for actions, one for the global flow.
- A **context provider** that assembles everything for the app.

**Explicit state machine**:
The game formalizes three states — `idle` (player can act), `processing` (resolution in progress), `game_over`. This avoids race condition bugs where the player clicks while an animation is running.

---

## Tech stack

<ul class="tech-stack">
<li><strong>React</strong> + <strong>TypeScript</strong></li>
<li><strong>Vite</strong> as bundler (with SWC)</li>
<li><strong>Tailwind CSS</strong> in CSS-first mode</li>
<li><strong>Web Audio API</strong> for sounds (via an AudioManager singleton)</li>
<li><strong>PWA</strong> — installable, offline</li>
<li><strong>i18n</strong> custom (French/English)</li>
<li><strong>Zustand</strong> for state management — localStorage (settings) + IndexedDB (progression) persistence</li>
<li><strong>Compression</strong> — Brotli + Gzip on assets</li>
<li><strong>CSP</strong> — Content-Security-Policy with auto-generated hashes</li>
</ul>

No UI frameworks, no heavy animation libraries. The fewest dependencies possible.

---

## Timeline

| Date     | Version | Description                                                                |
| -------- | ------- | -------------------------------------------------------------------------- |
| Nov 2025 | 1.1.0   | Polish & performance — CSS animations, audio addition, optimizations       |
| Nov 2025 | 1.0.0   | Deployment & PWA — Subdirectory support, PWA, gameplay stabilization       |
| Nov 2025 | 0.3.0   | Settings & i18n — FR/EN internationalization, themes/palettes, persistence |
| Nov 2025 | 0.2.0   | Quality & accessibility — Keyboard navigation, SR announcements, refactors |
| Nov 2025 | 0.1.0   | Playable prototype — Core mechanics (tap → increment → match) and scoring  |

---

## Takeaways

**Duration**: about 1 month.

**What I learned**:

- React in depth — not just "it works", but "why it re-renders and how to avoid it"
- Game design reverse engineering — analyzing a game to understand its implicit mechanics
- Game feel can't be improvised — it's iteration work on details

**What I'd do the same**:

- Start from an existing inspiration rather than reinvent. It allows focusing on execution.
- Treat the project as a finished product, not a prototype.

**What I'd change**:

- I'd add more content variety through **run modifiers** (e.g. "actions cost +1", "minimum combo is 5", "frozen tiles"), to refresh strategy without changing the core rules.
- I'd also add **special cards** (temporary buffs, area effects, risk/reward cards) to deepen tactical choices and make each run feel more distinctive.

</div>
