Monday, January 3, 2022

Making a turn-based game: Don't prematurely optimize, and different types of turns, and code available on GitHub :)

One of the things I wanted to add to Swătch was the ability to play different types of games. In addition to the initial game of "choose the shade of a color based on its name," I wanted to have the following:

  • Given a shade and several options, pick the correct name
  • "Trick" mode: each player makes up a name for a displayed color shade. Then, they're shown the names provided by other players in addition to the correct name. Choose which name was the correct name (players also get points for tricking the other players into choosing their name).

Supporting this atop proved extremely possible, with just a bit of work.

But first, a digression...

Don't prematurely optimize!

To support the "choose name based on shade" mode, I needed to extend the JSON file listing the set of all colors to include references to several nearby colors in the dataset. There are several clever approaches to this, but I chose the simplest one: for every color, find the distance to every other color and assign the N nearest as nearby colors. This is an O(n2) algorithm; pretty inefficient.

But since it was easy to code, I ran it anyway. On a 1,500 line input, it took about 0.1 seconds.

Friends, computers are fast nowadays. For heavy-lifting computation, don't burn time on being clever when you can use the sledgehammer of modern clock speeds.

Different types of rounds provides a fairly robust framework for distinct game turns, as well as describing what moves are valid in different stages of play. The framework also supports hidden state (both global and player-specific) for each player. Originally, I constructed the game with just a single type of turn, the guess-the-shade-from-the-name turn. To support multiple different types of turns, I made the following changes:

  1. Added a Round interface, which described a round of play by providing the following methods:
    • initState: Prep the public and private state for the round (set to initial, empty values)
    • onBegin: Begin the round, choose hidden values (such as what color to guess), set up player state.
    • scoreRound: Calculate and update scores and assign "best" (and possibly "second best") answers for later display.
    • buildPreviousRound: Every round, upon conclusion, bundles its results into a previousRound data structure used to describe how the round went to the players. This is the method that turns the round state into that structure.
    • moves: A map from the valid moves in this round to the code to execute them
    • getPlayerState: Accessor for the per-player state for this round.
    • getPublicState: Accessor for the public state for this round
    • getLastRoundState: Accessor for the previous-round state for this round
  2. With the interface in place, I moved around the existing Context state to carry different types of context depending on the current round (and reworked the client-side code to pull from those new contexts and show a specific view based on what the current round was).
  3. Now, I could refactor the existing turn logic into a GuessShadeRound implementation and generalize the onBegin and onEnd methods to select a round to play (with only one option currently available) and score the round when everyone had taken a turn. At this point, the refactor could be tested.
  4. I built out a Rounds.ts file to consolidate the separate round implementations.
  5. With all of that out of the way, I could write three more Rounds:
    • GuessNameRound, which shows a shade and asks players to select the right name for the shade.
    • A pair of sibling rounds: MakeUpNameRound and GuessMadeUpNameRound. These are special-cased: instead of the turn ending when MakeUpNameRound completes, it transitions directly into the GuessMadeUpNameRound and calls that round's initState to prep up the round for it.

This is all tough to follow from description alone, which is why I've made all the code available on GitHub to see. Feel free to run it locally and give it a try!

No comments:

Post a Comment