Grandma's Games
In developmentA card game of nine or eighteen holes where the rules change every hole. Hidden cards, locked pairs, wilds, attacks, and the lowest score wins.
At a glance
- Outcome: A turn-based card game where a human and a computer opponent play a run of changing grid rules, managing hidden information, positional pairs, locked cards, wilds, obstructions, attacks, and three once-per-match powers, trying to finish with the lowest cumulative score.
- Status: In development, offline mobile MVP. The pure-Dart core now drives a Flutter app: the complete Standard and Dealer’s Choice eighteen-hole courses, local save and resume, deterministic replay, a fair-information AI opponent, and semantic labels and large-text layouts covered by widget tests and golden images. Implementation and the automated gates are done.
- Role: Solo.
- Stack & libraries: Flutter and Dart targeting iOS and Android, with a pure-Dart rules core that runs headless, a Nix flake for the toolchain, and the Flutter version pinned in CI.
- Source: Private. Working title in the repo is Dealer’s Choice Golf.
- Limitations: Playtesting with people hasn’t started: 0 of the 6–8 planned playtesters. There’s no physical iPhone or Android evidence yet, so the accessibility work is verified in widget tests and goldens rather than on a device. The app currently starts an eighteen-hole run only. The nine-hole course exists in content, tests and the simulator, but isn’t selectable. Offline single-player against AI for the MVP; no online multiplayer in scope.
The idea
Card golf is a good small game, and every table plays it with slightly different house rules. The move here is making those changing rules the product: each hole changes the grid, the available information, the replacement rules, the turn pressure, or the scoring hazards, while the objective stays fixed at the lowest cumulative score.
Equal ranks only cancel when they sit orthogonally adjacent, so a hole is a grid you build instead of a pile of low cards you collect.
Swings are meant to be large. A bigger hole can matter far more than a small one, and that imbalance is intentional and shown before the hole starts. But an attack usually lands on a card, row or column you can still pair away.
Dealer’s Choice
The working title isn’t decoration. The losing player picks the next hole, from a seeded shortlist of three.
That’s the best idea in the game, and it’s also the reason the architecture had to look the way it does: if the loser chooses what comes next, holes can’t be a fixed authored sequence with special cases hanging off them. They have to be interchangeable data.
A hole is a package, not a diff
The unit is a hole package: a complete ruleset, not a modifier bundle layered over a base game.
The claim is mechanically checkable, which is why I’d point at it: no hole ID appears anywhere in the engine or the app. A finite registry admits exactly eight effect types, five event-card identities, and three power effects, and semantic validation rejects a bad package before a course can run: deck capacity, peek count versus hidden cells, position overlap, effect-to-card matching, and duplicate gameplay signatures.
So a new hole is authored and validated, not coded, and a bad one fails at load instead of halfway through a match.
One immutable match, one command seam
The core is an immutable match advanced through a single typed command seam. There are four commands: begin a hole, route a command into the active hole, advance the match, and commit a Dealer’s Choice pick.
A save keeps the seed and the command log rather than trusting derived state, and restoring replays that log. If the replayed result disagrees with the state stored beside it, the restore is rejected instead of accepted.
Crash recovery, reproducible bug reports, and AI simulation all come out of the same seam.
Two decisions I’d defend
- The engine ran headless before any widget existed. The design was written first, and the specification is what let the core be built and tested in pure Dart. The concrete thing that needed it: scoring has to solve pairings exhaustively, because adjacency, wilds and multipliers all interact, so the best arrangement of a grid isn’t something you can compute greedily. That solver needed to be testable without a widget tree.
- Fair-information AI. The opponent plays with the information a player would have. An AI that peeks is easier to write and ruins the game the moment anyone suspects it.