DEV Community

MRZHU
MRZHU

Posted on

Building fair higher-or-lower clues for a daily football guessing game

A daily player-guessing game has a tiny interface, but its feedback model can become surprisingly messy. While working on Wordle Cup, I found that the hardest part was not choosing a mystery player. It was making sure every failed guess taught the player something reliable.

Separate comparison types

I started by dividing clues into three groups:

  1. Exact categories such as nationality, club, league, and position.
  2. Ordered values such as age, height, and shirt number.
  3. Contextual facts that depend on a season or roster snapshot.

Exact categories are simple matches. Ordered values need direction as well as color: a miss should say whether the target is higher or lower. Contextual facts need an explicit date, because a club or shirt number can change even when the player record is otherwise correct.

This distinction removed several awkward special cases from the UI. Instead of every field deciding its own behavior, the clue definition now determines how it is compared and displayed.

Normalize before comparing

User input and roster data rarely use identical strings. Accents, initials, suffixes, and common aliases can make a valid guess look invalid. I treat normalization as a data concern rather than a visual patch.

A useful normalization pass can:

  • trim and collapse whitespace;
  • compare case-insensitively;
  • normalize Unicode consistently;
  • map known aliases to one player ID;
  • keep the displayed name separate from the lookup key.

The important rule is that normalization helps identify a player; it should not silently merge two different people.

Make clue direction unambiguous

An arrow only works when the question behind it is obvious. For height, “up” can mean the target is taller. For age, it can mean older. For a shirt number, the direction is numerical but not necessarily meaningful football knowledge.

I therefore keep the operator attached to the clue definition and pair the arrow with accessible text. Color alone is not enough, especially on a dense mobile grid.

The comparison result can be modeled as a small, predictable object:

type ClueResult = {
  state: "match" | "miss";
  direction?: "higher" | "lower";
  label: string;
};
Enter fullscreen mode Exit fullscreen mode

That shape is easy to test and keeps rendering logic boring, which is a compliment in a daily game.

Test the boundaries, not just the happy path

The bugs that matter tend to sit at the edges:

  • equal numeric values;
  • a player who changed clubs;
  • two players with similar names;
  • missing height or jersey data;
  • a guess that is valid in the database but outside today's pool;
  • narrow mobile layouts with long club names.

I use fixture players that deliberately trigger each case. A single end-to-end “correct answer” test does not tell us whether the clues remain trustworthy after a roster update.

Show the data scope

The interface should tell players which roster or season it uses. That short note prevents a large class of reports that are really snapshot disagreements. It also makes maintenance more disciplined because an update changes a visible product contract, not just a hidden data file.

I have been applying these ideas to the football guessing games on Wordle Cup. The biggest lesson so far is that difficulty should come from football knowledge, not from unclear comparison rules. Once each guess produces dependable information, a very small game loop becomes much more satisfying to replay.

Top comments (0)