Le pitch
Seedoku est un puzzle de placement sur une grille 3×3. Chaque jour, une main de 6 tuiles — cultures et boosters — à répartir sur 8 cases disponibles (une est bloquée par un rocher). L’objectif : maximiser le score de récolte en tirant parti des synergies entre les tuiles. Un puzzle par jour, le même pour tout le monde.
Jouer : presque.cool/seedoku/
Pourquoi ce projet
Seedoku est une rétro-ingénierie de seedle.io, un daily puzzle farming que j’avais trouvé satisfaisant. La démarche est la même que pour Grid4 : comprendre ce qui rend un jeu simple agréable en essayant de le reproduire aussi fidèlement que possible.
Le game design étant largement emprunté, j’ai concentré l’effort ailleurs : l’exécution technique, et surtout le juice — la couche de son, d’animations et de feedback qui transforme un puzzle fonctionnel en quelque chose de plaisant à toucher.
Le puzzle du jour
Le puzzle est généré de façon déterministe à partir de la date. Un hash FNV1a sur la chaîne seedoku-daily-v1:YYYY-MM-DD, passé dans un PRNG Mulberry32, produit la position du rocher et les 6 tuiles de la main. Pas de serveur, tout le monde a le même puzzle.
La navigation par date (/date/YYYYMMDD) permet de rejouer les jours précédents. Un lien partageable encode le score et la note obtenus.
Le moteur de scoring
Chaque culture a ses propres règles de placement. La patate marque davantage en coin (+8) ou en bord (+4). La citrouille veut le centre (+18). Le riz a besoin d’un arroseur adjacent (+8). Le champignon perd son bonus si un booster est à portée. Le berry-bush prend une pénalité de -4 s’il n’est pas à côté d’un booster.
Les boosters ne scorent pas eux-mêmes : ils appliquent des effets sur leurs voisins. L’arroseur ajoute +5 par culture adjacente, le soleil multiplie sa rangée entière par 2, le chilli double chaque culture qu’il touche directement.
Le tout est modélisé via des discriminated unions TypeScript (IntrinsicRule, BoosterEffect). Pas de switch sur les identifiants de tuile — chaque règle est une donnée, le moteur la lit. Ça rend l’ajout d’une nouvelle tuile trivial.
Pour le calcul des notes (D → S), le moteur calcule par brute-force DFS le score théorique maximum pour la grille et la main du jour. La note est le ratio score obtenu / score maximum possible.
Le juice
C’est là où j’ai passé le plus de temps — et c’était délibéré.
Les tuiles se posent avec une animation de rebond. La récolte se déroule pas à pas : d’abord les scores de base s’affichent sur chaque case, puis les bonus additifs apparaissent un par un avec un delta visible, puis les multiplicateurs s’appliquent. Chaque étape a un son. Le total monte progressivement dans un ticker dont la vitesse s’accélère.
Pendant le drag, chaque case cible affiche en temps réel le score que la tuile obtiendrait si elle était posée là. Les synergies deviennent lisibles sans avoir à mémoriser les règles.
Stack technique
- Preact 11 + TypeScript (strict, zero any)
- Vite 8 comme bundler (avec plugin-react)
- Tailwind CSS v4 en mode CSS-first (OKLCH)
- Zustand pour le state : game state, historique (undo), settings, toasts
- Web Audio API pour les SFX
- PWA via vite-plugin-pwa (Workbox, installable, offline)
- i18n maison (français/anglais)
- CSP avec hashes auto-générés
Architecture en couches : logique de jeu pure dans engine/ (scoring, puzzle, types), données dans data/, hooks d’orchestration, composants UI. Le moteur de scoring est découplé du rendu — les breakdowns de score servent à la fois aux tooltips en jeu et à l’animation de récolte.
Évolutions
| Date | Version | Description |
|---|---|---|
| Avr 2026 | 1.0 | Version complète |
| Sep 2026 | 2.0 | Nouveau générateur aléatoire : les puzzles des jours passés ne sont plus les mêmes qu’avant. Mode contraste élevé, sons préchargés. |
| Sep 2026 | 2.1 | Nouvel écran titre, commun aux jeux : logo, bouton pour jouer, puis règles, progression, partage et réglages en colonne, chacun avec sa description et son raccourci clavier. |
| Oct 2026 | 2.2 | Preact remplace React maintenant que le glisser-déposer et les infobulles n’en dépendent plus : le premier chargement passe de 134 Ko à 57 Ko de JavaScript. Le glisser-déposer, réécrit sans bibliothèque, marche à la souris, au toucher ou au clavier et les lecteurs d’écran annoncent chaque étape ; un seul appui ouvre l’infobulle d’une tuile au toucher. |
Bilan
Durée : 7-10 jours.
Ce que j’ai appris :
- Le juice, c’est la moitié du travail. Un puzzle de placement correct prend quelques jours. Rendre chaque interaction satisfaisante en prend autant. L’animation de récolte séquentielle, le score en temps réel au drag, la progression sonore — aucun de ces détails n’est nécessaire, tous changent le ressenti.
- La modélisation par données plutôt que par code conditionnel (les
IntrinsicRuleetBoosterEffecttypés) a rendu l’ajout et le débogage des règles de tuile très simple. Une leçon que je retiens pour les prochains jeux.
Ce que je referais pareil :
- Le brute-force DFS pour le score max. Sur une grille 3×3 avec 6 tuiles et 8 cases, l’espace est petit — pas besoin d’optimisation. La note est juste, le calcul se fait en quelques millisecondes.
- Emprunter un game design existant et créditer. Ça permet de se concentrer sur l’exécution plutôt que sur l’équilibrage.
Ce que je changerais :
- J’ajouterais de petits objectifs optionnels, comme “faire A ou plus trois jours d’affilée”. La série quotidienne existe déjà ; ils lui donneraient un but, sans alourdir le cœur du jeu.
The pitch
Seedoku is a placement puzzle on a 3×3 grid. Each day, a hand of 6 tiles — crops and boosters — to arrange across 8 available cells (one is blocked by a rock). The goal: maximize your harvest score by making the most of the synergies between tiles. One puzzle per day, the same for everyone.
Play: presque.cool/seedoku/
Why this project
Seedoku is a reverse-engineering of seedle.io, a daily farming puzzle game I found satisfying to play. The approach is the same as Grid4: understand what makes a simple game feel good by trying to reproduce it as faithfully as possible.
With the game design largely borrowed, I focused effort elsewhere: the technical execution, and especially the juice — the layer of sound, animation, and feedback that turns a functional puzzle into something pleasant to touch.
The daily puzzle
Each puzzle is generated deterministically from the date. An FNV1a hash of the string seedoku-daily-v1:YYYY-MM-DD, passed through a Mulberry32 PRNG, produces the rock position and the 6-tile hand. No server needed — everyone gets the same puzzle.
Date navigation (/date/YYYYMMDD) lets players replay past puzzles. A shareable link encodes the day’s score and grade.
The scoring engine
Each crop has its own placement rules. Potato scores more in a corner (+8) or on an edge (+4). Pumpkin wants the center (+18). Rice needs an adjacent sprinkler (+8). Mushroom loses its bonus if any booster is nearby. Berry-bush takes a -4 penalty if no adjacent booster is present.
Boosters don’t score themselves: they apply effects to their neighbors. Sprinkler adds +5 per adjacent crop, Sun multiplies its entire row by 2, Chilli doubles each crop it directly touches.
Everything is modeled with TypeScript discriminated unions (IntrinsicRule, BoosterEffect). No switch statements on tile IDs — each rule is data, the engine reads it. Adding a new tile is trivial.
To compute grades (D → S), the engine runs a brute-force DFS to find the theoretical maximum score for the day’s grid and hand. The grade is the ratio of your score to the maximum possible.
The juice
This is where most of the time went — deliberately.
Tiles land with a bounce animation. The harvest plays out step by step: base scores appear on each cell, then additive bonuses show up one by one with a visible delta, then multipliers apply. Each step has a sound. The total climbs progressively in a ticker that accelerates as it goes.
While dragging, each target cell shows in real time the score the tile would get if placed there. Synergies become readable without having to memorize the rules.
Tech stack
- Preact 11 + TypeScript (strict, zero any)
- Vite 8 as bundler (with plugin-react)
- Tailwind CSS v4 in CSS-first mode (OKLCH)
- Zustand for state: game state, move history (undo), settings, toasts
- Web Audio API for SFX
- PWA via vite-plugin-pwa (Workbox, installable, offline)
- i18n custom (French/English)
- CSP with auto-generated hashes
Layered architecture: pure game logic in engine/ (scoring, puzzle, types), data in data/, orchestration hooks, UI components. The scoring engine is decoupled from rendering — score breakdowns power both the in-game tooltips and the harvest animation.
Timeline
| Date | Version | Description |
|---|---|---|
| Apr 2026 | 1.0 | Full release |
| Sep 2026 | 2.0 | New random generator: puzzles from past days are no longer the ones played before. High contrast mode, preloaded sounds. |
| Sep 2026 | 2.1 | New title screen, shared by the games: logo, play button, then rules, progression, share and settings in a column, each with its description and keyboard shortcut. |
| Oct 2026 | 2.2 | Preact replaces React now that drag and drop and tooltips no longer depend on it: the first load goes from 134 KB to 57 KB of JavaScript. Drag and drop, rewritten without a library, works by mouse, touch or keyboard and screen readers announce each step; a single tap opens a tile’s tooltip on touch screens. |
Looking back
Duration: 7-10 days.
What I learned:
- Juice is half the work. A functional placement puzzle takes a few days. Making every interaction satisfying takes just as long. The sequential harvest animation, the real-time score preview during drag, the layered sound — none of these are necessary, all of them change how the game feels.
- Modeling rules as data rather than conditional code (the typed
IntrinsicRuleandBoosterEffect) made adding and debugging tile rules straightforward. Worth keeping for future games.
What I’d do the same:
- The brute-force DFS for the max score. On a 3×3 grid with 6 tiles and 8 cells, the search space is small — no optimization needed. The grade is accurate, the calculation takes a few milliseconds.
- Borrowing an existing game design and crediting it. Lets you focus on execution rather than balancing.
What I’d change:
- I’d add optional mini-goals, like “hit A or higher three days in a row”. The daily streak already exists; they would give it a purpose, without making the core game heavier.