DEV Community

Cover image for I shipped a card game in a weekend — but I didn't really "vibe code" it
Stanislav Kremeň
Stanislav Kremeň

Posted on

I shipped a card game in a weekend — but I didn't really "vibe code" it

This is my entry for the Vibe Coding Weekend Challenge. The honest version, anyway — because what I did wasn't quite vibe coding.

The app, in two sentences

It's a browser version of Prší, a Czech card game from the Mau-Mau family. You play one full match, 1v1 against a deliberately dumb AI, from the deal to someone emptying their hand — mouse or touch.

Prší game board: opponent's face-down cards on top, draw pile and discard pile in the middle, your hand at the bottom

Stack: TypeScript + Vite, no framework, plain DOM rendering. The game engine (rules, move validation, win detection) is pure functions with zero DOM dependencies, so it's fully testable on its own — it ended up with ~300 unit/integration tests.

The tool: Claude Code, but on a leash

The challenge suggests Lovable, Cursor, or Bolt. I used Claude Code instead — and not in the "describe the app, accept whatever comes out" way that vibe coding usually means.

Instead I drove it through a small phase orchestrator. The whole build was sliced into ~10 explicit phases, each with a written goal and a verifiable output:

  1. Vite + TypeScript scaffold
  2. Engine: data model (32-card deck, deal)
  3. Engine: basic move (suit/rank match, draw)
  4. Engine: special cards + win
  5. AI opponent heuristic
  6. UI: render the table
  7. UI: interaction loop + suit picker
  8. End of game, new match, stalemate
  9. End-to-end simulation (200 AI-vs-AI games)
  10. README + deploy

Each phase got planned, implemented, and reviewed before the next one started. So less "vibe", more spec-driven development with an AI doing the typing. The upside: when the AI got something subtly wrong, I had tests and a tight scope to catch it instead of a 2000-line blob I had to trust.

What actually went wrong

The challenge asks for the real iteration count and the things that broke. Honestly, the rules were the hard part, not the code:

  • The seven. A 7 forces the opponent to draw 2, and sevens stack — up to four of them, so the last player can be forced to draw 8. Getting "you can only respond to a seven with another seven" right took a couple of passes, because it interacts with which cards are even considered playable.
  • The queen is a wildcard. It can be played on anything and changes the demanded suit. The first engine version wrongly restricted it to a normal suit/rank match. Caught by a test, fixed.
  • Deployment bit me. Everything worked on localhost, then broke on GitHub Pages. The cards live under /cards/... as absolute paths, but the site is served from /prsi/ (a project subpath), so every card 404'd. Fix: build the paths through Vite's import.meta.env.BASE_URL instead of hardcoding the leading slash.
  • A dumb versioning slip. I tagged v1.0.2 but forgot to actually bump package.json, so the tag's tree still said 1.0.1. Small, but exactly the kind of thing you only notice when you look.

The AI side stayed simple on purpose: the opponent only ever plays legal moves and picks them with a basic heuristic. A smart AI wasn't the point — a correct one was, which is why those 200 simulated games matter (every one has to end in a win or a stalemate without breaking an invariant).

Was it worth it?

Yes. The weekend constraint forces you to cut scope — no accounts, no multiplayer, no score across rounds, just one good game. And going phase-by-phase instead of pure vibe meant the thing that shipped is one I can actually reason about and extend.

If you want to see a "dumb" AI lose at a Czech card game, it's right here: https://czsoftcode.github.io/prsi/

Top comments (0)