Go Fish โ TDD Class Design
Inspired by the Bowling Game Kata approach: let the tests drive you toward the simplest structure
that works. Start with the Game and let Card, Deck, Hand, and Player emerge only when
the tests demand them.
Design Philosophy (Kata-Style)
Just like the Bowling Kata resists the temptation to model Frame as a class, resist modeling
every Go Fish noun up front. The test sequence below reveals which structs earn their existence.
Structs
Card
Represents a single playing card.
| Field | Type | Notes |
|---|---|---|
Suit | string | "Hearts", "Diamonds", etc. |
Rank | string | "2"โ"10", "J", "Q", "K", "A" |
Methods
String() stringโ human-readable label, e.g."A of Spades"Equals(other Card) boolโ rank equality (suit is irrelevant for matching)
Deck
An ordered collection of cards that can be drawn from.
Methods
NewDeck() Deckโ builds a standard 52-card deckShuffle()โ randomizes card orderDraw() (Card, bool)โ removes and returns the top card; bool signals empty deckIsEmpty() boolSize() int
Hand
The cards held by one player.
Methods
AddCard(card Card)HasRank(rank string) boolTakeCardsOfRank(rank string) []Cardโ removes and returns all matching cardsCollectBook() intโ scans for sets of 4; lays them down and returns how many books were scoredIsEmpty() boolSize() intRanks() []stringโ distinct ranks currently held (used to validate asks)
Player
Wraps a Hand with identity and a book count.
Methods
NewPlayer(name string) PlayerAsk(target *Player, rank string) boolโ askstargetfor a rank; returns true if cards were receivedGoFish(deck *Deck) boolโ draws one card; returns true if it matched a rank already heldBooks() intโ total books scored so farName() string
Game
Orchestrates the full game. This is where the tests drive almost all the logic.
Methods
NewGame(playerNames []string) Gameโ creates players, builds and shuffles the deckDeal()โ distributes opening hand (7 cards for 2 players, 5 cards for 3+)CurrentPlayer() *PlayerTakeTurn(targetPlayerName string, rank string) TurnResultโ core kata method; executes one complete turn: ask โ receive or go fish โ collect books โ advance turnIsOver() boolโ true when deck is empty AND at least one player has an empty handWinner() *Playerโ player with the most books; nil if game is not overScores() map[string]intโ snapshot of all players' book counts
TurnResult (value type)
Returned by TakeTurn to keep test assertions readable without inspecting internal state.
| Field | Type | Notes |
|---|---|---|
GotCards | bool | true if target had the rank |
FishedCard | *Card | the card drawn, if Go Fish occurred |
FishMatched | bool | true if the drawn card matched a held rank |
BooksScored | int | books laid down this turn |
TurnChanged | bool | false when player earns another turn |
Suggested Test Sequence (Kata Order)
Following the kata spirit, write tests in this order so each one forces exactly one new concept:
- New game has correct number of players โ drives
NewGame - Deal gives each player the right hand size โ drives
DealandHand.Size - Ask a player who has the rank โ receive cards โ drives
AskandHand.TakeCardsOfRank - Ask a player who lacks the rank โ Go Fish โ drives
GoFishandDeck.Draw - Four of a kind forms a book โ drives
Hand.CollectBook - Fish card that matches held rank scores a book immediately โ drives
FishMatched - Player who asks successfully goes again โ drives
TurnResult.TurnChanged - Game ends when deck is empty and a hand is empty โ drives
IsOver - Player with most books wins โ drives
Winner - Tie is handled gracefully โ hardens
Winner
What Deliberately Has No Class
| Temptation | Why to resist it |
|---|---|
Book | Just an int counter on Player; no behavior of its own |
Turn | Fully captured by TurnResult; no persistent state needed |
RuleEngine | Rules belong in Game.TakeTurn; extract only if tests demand it |
CardPair/CardSet | Over-modeling rank groupings before tests require it |