Overview

5 Gordle: play a word game in your terminal

This chapter builds a Wordle-inspired terminal game called Gordle and uses it as a vehicle to practice practical Go skills. You start from a minimal playable loop and evolve it into a configurable game (word length and attempt count), while learning how to read user input from standard input, handle Unicode via runes, pick random items, read files, and propagate errors clearly. The focus is on writing small, testable units, making intentional API choices (constructors, receivers, zero values), and keeping the command-line experience accessible and informative.

Step by step, the game takes shape around a Game type with New and Play methods. Input is read with bufio.Reader, normalized (e.g., uppercasing), and validated via a dedicated validateGuess method that returns informative errors; tests are written in table-driven style and use errors.Is for robust assertions. To guide the player, the chapter introduces a compact hint model (absent, wrong position, correct position), implements fmt.Stringer for printable feedback, and uses strings.Builder to avoid costly string concatenation. The core computeFeedback algorithm carefully handles exact matches, misplaced letters, and duplicates, and the Play loop limits attempts, prints emoji (or text) hints, and reports win/lose outcomes with clear messages.

To make each game fresh, the solution is drawn from a corpus loaded from disk (os.ReadFile + strings.Fields), guarded by sentinel errors for predictable failure modes, and selected with a simple math/rand index. The constructor is updated to accept the corpus and max attempts, returning an error when initialization cannot produce a playable game; main wires everything together and surfaces errors to stderr. Finally, the chapter highlights Unicode nuances: while Go’s rune model works well for many scripts, combining marks (e.g., Devanagari) reveal the limits of counting runes as “characters,” motivating normalization/grapheme handling from x/text when broader language support is needed. By the end, you have a complete, tested CLI game and a toolkit of Go patterns you can reuse in future projects.

Summary

  • A switch / case statement is a lot more readable than a long sequence of if / else if / else statements. A switch can even be used instead of an if statement. We think that, if you need an else statement, you’re better off with a switch block.
  • A command-line tool often needs to read from the console input. Go offers different ways of doing it, in this chapter, we used the bufio.ReadLine method, which reads an input line by line.
  • Sentinel errors are a simple way of creating domain errors that can be exposed for other packages to check. It is a cleaner implementation than creating exposed errors with errors.New() or fmt.Errorf(). To declare a new sentinel error type, declare a new type that is defined as a string (this makes creating new errors simple).
  • Propagating an error to the caller is the way Go handles anything that steps out of the happy path. Functions that propagate errors have their last return value of their signature be an error. In the implementation of these functions, fmt.Errorf("... %w", … err) is the default way of wrapping errors. The w in %w stands for “wrap”.
  • Any structure with a method with the following signature: String() string implements the fmt.Stringer interface. Any structure that implements the Stringer interface will be nicely printed by fmt.Print* functions.
  • The os package provides a ReadFile function that loads a file’s contents as a slice of bytes. This function can be used for plain-text files, media files, files in XML or HTML format, etc.
  • The slices package contains useful tools such as the Equal function or the Contains function. As we’ve seen, implementing the function for a specific use case isn’t too complex.
  • A Go string can be parsed as either a slice of bytes, or as a slice of runes. The latter is recommended when iterating through the characters that compose it. Use []rune(str) to convert the str string to a slice of runes. However, even this solution isn’t perfect and won’t always work. Best to first check the language you’re dealing with to select the best libraries to parse any text.
  • All receivers of a specific type should be either pointer or value receivers. Using value receivers is only interesting if the structure is small in memory, as it will copy it. When in doubt, use pointer-receiver declarations.
  • When writing table-driven tests, it is a very common practice to use a map[string]struct{...}. The key of the map, the string, describes the test case, and the struct is an anonymous structure that contains the fields necessary for your test case.
  • Getting a random number can be achieved by both the math/rand and the crypto/rand packages. Anything related to security, cyphering, or cryptographic data should use the crypto/rand package, while the math/rand is cheaper to use.
  • When working with random numbers, make sure you’re using Go 1.20 or more. Otherwise, be explicit about setting the seed with a call to rand.Seed(). An usual value for the seed used to be the current nanosecond, retrieved with time.Now().Nanosecond().
  • Taking a step back, away from a screen, and writing pseudo-code with potatoes and arrows is valuable, and helps to see the bigger picture and imagine tricky scenarios that might prove or disprove an algorithm.

FAQ

What is Gordle and how is it different from Wordle?Gordle is a terminal-based word-guessing game written in Go. Unlike Wordle’s fixed 5-letter/6-attempt rules, Gordle is configurable: you can change word length and max attempts, support multiple languages via a corpus, and run it fully in the terminal.
How should I structure the project and initialize the game?- Initialize the module with go mod init.
- Create a gordle package exposing a Game type and a New(...) constructor.
- In main, create the game and call g.Play().
- Final form of New: New(reader io.Reader, corpus []string, maxAttempts int) (*Game, error). Pass bufio.NewReader(os.Stdin), load a corpus, and handle the returned error.
How does Gordle read player input and handle characters?- Uses bufio.NewReader(io.Reader) and ReadLine() to read a line from stdin.
- Convert bytes to string, then to runes for Unicode safety.
- Normalize input with splitToUppercaseCharacters to make comparisons case-insensitive.
Why use runes instead of bytes, and what are the Unicode caveats?- Go strings are UTF-8; non-ASCII characters use multiple bytes. Runes represent Unicode code points, so []rune aligns with characters better than []byte.
- Caveat: Some scripts (e.g., Devanagari) combine glyphs using multiple code points. A “visible character” may be several runes. To support grapheme clusters, consider golang.org/x/text/unicode/norm (or a grapheme-aware iterator) beyond the chapter’s scope.
How are guesses validated and how are errors handled?- Validation is separated in validateGuess([]rune) error (e.g., length check).
- Use wrapped errors via fmt.Errorf("... %w", err) and detect them with errors.Is.
- The ask() loop prints validation errors to os.Stderr and reprompts without crashing.
How is feedback computed and displayed for each guess?- A hint enum (absentCharacter, wrongPosition, correctPosition) models per-letter status; feedback is []hint.
- computeFeedback uses two passes: first mark correct positions, then mark misplaced letters using a used mask to handle duplicates properly.
- Display via feedback.String() (implements fmt.Stringer) using strings.Builder. Use emojis or ASCII fallbacks if your terminal doesn’t render emojis.
Why use strings.Builder instead of string concatenation?Strings are immutable in Go; concatenating in a loop creates many allocations. strings.Builder amortizes allocations and is faster and clearer for building the feedback string.
How do I create and load a corpus of words?- Store words (uppercase, one per line or whitespace-separated) in files like corpus/english.txt.
- Read with os.ReadFile, split with strings.Fields.
- Use a sentinel error type (e.g., corpusError) and return ErrCorpusIsEmpty for empty files.
How is the random solution chosen and how do I test randomness?- Use math/rand.Intn(len(corpus)) to pick an index; Go 1.20+ seeds automatically at startup.
- Testing: assert that the result is a member of the corpus (e.g., via an inCorpus helper), rather than expecting a specific value.
What testing patterns are used throughout the chapter?- Table-driven tests for ask, validateGuess, computeFeedback, and corpus reading.
- Use errors.Is for wrapped error checks.
- Compare slices with slices.Equal or type-specific Equal methods (e.g., for feedback).
- Stub input with strings.NewReader to simulate player guesses.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Learn Go with Pocket-Sized Projects ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Learn Go with Pocket-Sized Projects ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Learn Go with Pocket-Sized Projects ebook for free