Bulls & Cows active
A simple guessing game that turned into a months-long arc of learning. Started as an overnight C++ terminal build in March — just to see if I could. A couple of months later I rebuilt it as a single-player web app to learn the basics of a real backend. Right now I'm rebuilding it again as a real-time two-player multiplayer game over WebSockets.
Same rules every time. Same scoring algorithm. Each rewrite forced me to learn something the previous one let me ignore.
v1 · The overnight CLI Mar 2026
One evening I read about the Bulls and Cows guessing game and decided
to build it before going to sleep. I didn't. I built it before going
to sleep the next day. C++17, just <iostream> and
<string>, no external libraries.
A terminal version of Bulls & Cows. The computer picks a secret 4-digit number, you guess, the game reports Bulls (right digit, right spot) and Cows (right digit, wrong spot) until you crack it.
The game loop. State that lives across iterations of a
while loop without anything like classes or objects to
hold it. Input validation that doesn't crash on garbage. The Bulls
and Cows scoring rule itself, which is trickier than it sounds when
the secret has repeated digits.
A terminal game can't be shared. I wanted to send a link to a friend, and "clone my repo and compile this" is not a link.
v2 · The solo web app May 2026
Same game, same scoring, now in a browser. The computer still picks the secret; you still guess. But the entire stack underneath had to change to support the rendering of a single web page.
FastAPI backend with one endpoint that creates a new game and one that accepts a guess. Game state stored in SQLite by session id. Frontend was plain HTML and one JS file — the same minimal philosophy I now use for this site.
What "stateless API + persistent store" actually means. Why you shouldn't trust the client to tell you the secret number (yes, I made that mistake in the first draft). How a request/response cycle is fundamentally different from a function call — the server has no memory of you unless you give it a token.
Playing alone against a computer is fine. Playing against another human is a different game entirely. And to do that, the request/response model wasn't going to work — I needed both players to see updates in near-real-time. Polling would have worked, but it would have been ugly. Time to learn WebSockets.
v3 · The multiplayer rewrite Jun 2026
Two players. One room. Each player picks their own secret number, then they take turns guessing each other's. The first to crack the other's number wins. Real-time, no refresh.
Same FastAPI backend, but with WebSocket endpoints instead of REST. Players join a room via a 6-character code; game state lives in an in-memory Python dict keyed by room code. SQLite is only touched at game end to write a leaderboard row.
The frontend uses one WebSocket connection per player and a small
JS dispatcher that handles incoming messages by type:
guess, opponent_guessed, turn,
win, opponent_left.
Real-time state is the hard part. When two clients are simultaneously connected, the server is the source of truth — clients can only ask, never assert. Turn synchronization without polling means the server has to explicitly push "it's your turn" messages rather than letting clients infer it.
And disconnect handling is the deep end. When a player's socket drops mid-game, the room can't die immediately (they might just have flaky wifi) but it also can't hang forever. The current rule: the room stays alive while at least one player is connected, and is garbage-collected when both leave.
Adding a spectator mode — same WebSocket, but read-only. A rejoin flow so a player who drops can come back to the same room within a grace window. Eventually a Redis-backed room store so I can run more than one server instance.
Looking back
I didn't plan this as an arc. v1 was an overnight challenge. v2 was an excuse to learn FastAPI a couple of months later. v3 is the one I'm still building. But looking at them in sequence, the thing I notice is that each rewrite forced me to learn what the previous version had let me skip — state, persistence, concurrency — and each one was small enough that I actually finished it.
The lesson, I think, is that the same toy project, rebuilt at a higher level of abstraction, can teach more than three different toy projects built at the same level.