Web Games with Dart: Hangman

Well, here we are again, sports fans. We're going to whip out a stupid simple game application in order to demonstrate some key programming principles, as well as highlight the power and simplicity of Dart. Hangman is a game as old as time (or thereabouts), so I'm not going to spend a bunch of time explaining to you how it works. With the completion of this tutorial, you'll have your very own version of the game for your portfolio, though you might want to spruce up the interface a bit before you go showing everyone.

This code base will demonstrate a principle that's become more important than ever in this age of devices, when you may need to create versions that run on everything from phones and tablets to PCs and (maybe) even toasters: Separation of concerns. The game logic will operate entirely independently of the user interface (UI) code, so it'll be relatively easy to share that logic with different apps, even on totally different platforms, such as Google's new Flutter framework for mobile.

Note: This tutorial's full code is available on GitHub for those who would like to play with it or use it as a reference.

The code for this tutorial was tested with Dart SDK v1.24.3.

Setting Up

To create this game, you won't necessarily need a full Dart work environment on your computer, but if you want one, here's the stuff you need:

Good News!

All of the code in this tutorial will run just fine in the online Dart editor, DartPad. If you don't want to muck around with workspace setup right now, you don't have to. You can code the whole thing right in your browser.

Dart Primers

Check out the Dart Language Tour for a crash course in the Dart language. If you already know JavaScript, Java, PHP, ActionScript, C/C++/C#, or another "curly brace" language, you'll find Dart to be familiar, and you can be productive with Dart in no time.

If you like learning from videos, take a look at Learn Dart in One Video.

The Tech

In order to remain simple and friendly to beginners, this tutorial will not make use of the fabulous AngularDart framework. Instead, we'll rely on a few simple commands to manipulate the UI, not too different from how you might go about it with vanilla JavaScript or jQuery. This will also allow the code to run successfully in the DartPad coding environment, as it is not possible to import non-core packages there.

We will not spend any appreciable time going over the little bit of HTML and CSS code required to build the crappy UI, instead concentrating on the Dart code.

The Approach

We will create a game class called HangmanGame that will house all of the game logic and data. It will communicate with the outside world (typically the UI code) strictly via streams. Streams are kind of like asynchronous arrays, virtual pipes that can be monitored for the arrival of new data. Whenever something significant happens in the game, such as a right or wrong guess, or a win or loss, the game object will emit events over the appropriate streams, without knowing or caring whether anything is listening. In this way, we keep the logic and UI code mostly decoupled.

The text here will assume you're using DartPad as your development environment. If you're using a local IDE of some kind, it is presumed you know enough to translate the instructions contained herein for your setup.

The Code

Without further ado, let's get to coding.

Step 1: HTML, CSS, and a Bit of Cleanup

Let's get the structure and style of the UI out of the way. In DartPad, you have three "tabs" above the left pane. Select the HTML tab, and enter this code:

HTML

<div id="main">  
  <h1>Hangman</h1>
  <img id="gallows">
  <div id="word"></div>
  <div id="letters"></div>
  <button id="new-game">New Game</button>
</div>  

So all we have here is a wrapping <div>, a title, an <image> tag to show the current image of the gallows, the area to display the word the player is trying to guess, and a section to contain the letter buttons the player will use to make guesses. Then there is also a New Game button, which will end up being visible only after a game's completion.

Next, place this code into the code pane while the CSS tab is highlighted:

CSS

#main {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

#main > * {
  margin: 10px;
}

#gallows {
  max-width: 300px;
}

#word {
  letter-spacing: .3rem;
  font-size: 2rem;
}

#letters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}

.letter-btn {
  width: 35px;
  height: 35px;
  margin: 5px 2px;
}

If there is any example code between the starting and ending curly braces of your main() function under the Dart tab, go ahead and banish it all to the ether. The cleaned-up version will look like this:

Dart

void main() {

}

Step 2: Imports

With that UI nonsense outta the way, it's time to turn our attention to the Dart tab, where all the magic happens. All but the simplest Dart programs rely on external packages of code to get the job done. Hangman needs access to two packages from Dart's comprehensive core libraries. Add these two lines to the top of the Dart code pane, above the existing main() function declaration:

Dart

import 'dart:async';  
import 'dart:html';  

Note: All the remaining code will be targeted at the Dart code pane, so we will no longer explicitly label each code block as such.

The game code will need things from Dart's async library, most notably the Stream classes that will allow asynchronous communication with other parts of the code base. We'll use the html library to insert, hide, and manipulate elements in the HTML user interface.

Step 3: The Hangman Game Model

This step will consist of several sub-steps, just to keep things organized. When you're done, you'll have a working Hangman game model, but you won't be able to play it until you hook up a user interface. As mentioned earlier, the beauty of this approach is that you can easily lift the Hangman logic, unaltered, into another project with a completely different UI.

Step 3.1: Create the Class

You need to create a skeletal HangmanGame class before you can add properties and methods to it, right? So do it! Add this code above or below your main() function, as you desire. Personally, I like main() to stay near the top of my file, but that's just me, a veteran Dart programmer and all-around great guy.

class HangmanGame {

}
Step 3.2: Hang 'Em High

We need a configurable way for the game to know how wrong a player can be before we hang him by the neck until dead. That's where this new line comes in. Add it to the top of your new class, but within the curly braces, like this:

class HangmanGame {  
  static const int hanged = 7;
}

A static const gets stored right on the class template, accessible from outside the class with HangmanGame.hanged or from within as just hanged. The hanged constant is an integer value representing how many wrong guesses the player gets before she's defeated. As a const, the value cannot be altered during the life of the program, but if you were to write a variation of the standard Hangman game, you could customize a player's lives here.

Step 3.3: Add Member Variables

The game will need to track a bit of state as the word adventure progresses. Add these variables for that purpose, below the hanged constant, but still within HangmanGame's curly braces:

HangmanGame class

final List<String> wordList;  
final Set<String> lettersGuessed = new Set<String>();

List<String> _wordToGuess;  
int _wrongGuesses;  

The first member, wordList, will be a reference to the list of words from which the game will randomly select a word for the player to guess. With this, it's possible to customize the word list for a game, perhaps for varying difficulty. Note that right now, your editor might be annoyed with you for creating but failing to initialize a final variable. Don't worry about that. We'll get to it.

The lettersGuessed variable points to a Set. This collection type will end up being more efficient than a List for keeping track of which letters the player has guessed, since it will not store duplicate values. A good UI will prevent the player from guessing the same letter more than once, but just in case the game ends up attached to a questionable UI, we'll keep things clean in our state. This final variable is immediately initialized with an empty Set, so Dart has no cause to complain about that one.

Naturally, the game will need to store the randomly chosen word the player will guess, and that's where _wordToGuess comes in. The underscore prefix tells Dart to keep the property private. Dart's privacy boundary is the library, not the class, so if you're using DartPad right now, this access restriction won't make any difference. In a larger project, you'd likely keep HangmanGame in its own file (and thus, library). Best practices FTW!

At this point, you might be wondering why _wordToGuess is typed as List<String> instead of simply as String. This is because within our game code, it will often be useful to be able to loop over the letters of the word individually, and that isn't as easy to do with a simple string.

Last, and perhaps least, we set up an integer variable, _wrongGuesses, to count the failures of our intrepid player. If that value ever reaches the value of hanged, well...time to notify next of kin.

Step 3.4: Swimming Upstream

Whenever something of significance happens in the game, the game class needs to communicate the event to the outside world. For this, Dart's streams will come in handy. Let's add streams for UI code to listen to. These definitions can be placed anywhere within the body of the class:

HangmanGame class

StreamController<Null> _onWin = new StreamController<Null>.broadcast();  
Stream<Null> get onWin => _onWin.stream;

StreamController<Null> _onLose = new StreamController<Null>.broadcast();  
Stream<Null> get onLose => _onLose.stream;

StreamController<int> _onWrong = new StreamController<int>.broadcast();  
Stream<int> get onWrong => _onWrong.stream;

StreamController<String> _onRight = new StreamController<String>.broadcast();  
Stream<String> get onRight => _onRight.stream;

StreamController<String> _onChange = new StreamController<String>.broadcast();  
Stream<String> get onChange => _onChange.stream;

Kind of a lot going on there. Let's see if we can break it down.

The game needs to announce a win or loss, so that's what the first two streams are for. In each of those cases, there isn't any data associated with the event, which is why the StreamController and Stream declarations require their event payloads to be null, signified by the use of the <Null> generic syntax. The StreamController is what the game object uses to add new events to the stream, and the associated getter exposes each controller's stream to users of the HangmanGame class. The controllers are private to the class, while the stream getters are public so external code can see them. The broadcast() named constructor is used because we want to support multiple listeners on each stream.

Take a look at this example code to see the streams in action. Do not add this code to your project!

Example: Emitting a win event

_onWin.add(null);  

Example: Listening for a win event

HangmanGame game = new HangmanGame(wordList);  
game.onWin.listen(win);  

In the emission example, we use the StreamController's add() method to announce a win, passing null since there is no pertinent data that needs to accompany a win event. The second example block shows code for creating a new instance of the HangmanGame class, then listening for events on the onWin stream. The win function (not shown) will be run whenever the game object emits a win event.

The onWrong stream emits an event each time the player guesses an incorrect letter, but the payload for that event is int. The int is the number of wrong guesses the player has made in the game so far, which might be useful to the UI code.

The onRight stream is used whenever the player correctly guesses a letter, and its payload is a String, which will be the correct letter for that guess.

Whenever the Hangman word's state changes, such as when a new word is chosen or a correct guess adds new letter clues, the game emits an onChange event along with a String containing the state of the word for display purposes. In the middle of a game, this will be some collection of correctly-guessed letters and underscores representing spaces, and at the game's conclusion, it will be the full word.

Step 3.5: Constructing Hangman

Now it's time to add a class constructor. This will get rid of any warnings you have about not initializing the wordList property.

HangmanGame class

HangmanGame(List<String> words) : wordList = new List<String>.from(words);  

This constructor doesn't need a body, but it does need to take a list of words to use in the game. When it does, it saves a copy of the List so as to avoid corrupting the original with any modifications the game might make. List's from() named constructor helps us do it. Using the : syntax, we create an initializer list that executes before any constructor body would have, and that's how we satisfy the final portion of the _wordList declaration.

Step 3.6: A Few Helpers

In this step, we're going to add a handful of getters to the HangmanGame class. These will give us access to vital game information both within the class and from without. Add this code to the class:

HangmanGame class

int get wrongGuesses => _wrongGuesses;  
List<String> get wordToGuess => _wordToGuess;  
String get fullWord => wordToGuess.join();

String get wordForDisplay => wordToGuess.map((String letter) =>  
  lettersGuessed.contains(letter) ? letter : "_").join();

// check to see if every letter in the word has been guessed
bool get isWordComplete {  
  for (String letter in _wordToGuess) {
    if (!lettersGuessed.contains(letter)) {
      return false;
    }
  }

  return true;
}

The first getter provides read-only, public access to the private field _wrongGuesses, while the second does the same for _wordToGuess in its broken-up, List form. The fullWord getter returns the word as a plain ol' string.

Using the List's map() method, wordForDisplay loops over each letter of the word and constructs a display version that features underscores in the place of unguessed letters. The map() method returns an Iterable, and join() converts that into a string.

Lastly, isWordComplete is a slightly more complex getter that checks whether the player has guessed every letter in the word, returning true if so, and false otherwise.

Step 3.7: It's a Brave New Game

Next up: The newGame() method. Hangman needs to initialize its state for a new game, so add this method to the class to do it:

HangmanGame class

void newGame() {  
  // shuffle the word list into a random order
  wordList.shuffle();

  // break the first word from the shuffled list into a list of letters
  _wordToGuess = wordList.first.split('');

  // reset the wrong guess count
  _wrongGuesses = 0;

  // clear the set of guessed letters
  lettersGuessed.clear();

  // declare the change (new word)
  _onChange.add(wordForDisplay);
}

Hopefully, the code comments help to make it clear what this method is doing, but we'll discuss just a few fine points. We treat the word list like a deck of cards, first shuffling it and then drawing the first one. Once we've got a word, we use List's split() method to create a List<String> from a simple String. Passing an empty string as a divider gives us a List with one character per element.

Since we've got a new word, we announce this change to the world with an onChange event. As a reminder, the wordForDisplay getter defined earlier generates a mix of letters and underscores to reflect the current state of the player's guesswork.

Step 3.8: Let Me Guess

The only thing left for the game class to do is handle the player's guesses. Add the guessLetter() method to your HangmanGame class:

HangmanGame class

void guessLetter(String letter) {  
  // store guessed letter
  lettersGuessed.add(letter);

  // if the guessed letter is present in the word, check for a win
  // otherwise, check for player death
  if (_wordToGuess.contains(letter)) {
    _onRight.add(letter);

    if (isWordComplete) {
      _onChange.add(fullWord);
      _onWin.add(null);
    }
    else {
      _onChange.add(wordForDisplay);
    }
  }
  else {
    _wrongGuesses++;

    _onWrong.add(_wrongGuesses);

    if (_wrongGuesses == hanged) {
      _onChange.add(fullWord);
      _onLose.add(null);
    }
  }
}

The UI code will call this function and pass in the guessed letter whenever a player presses one of the letter buttons (coming soon). When that happens, the first thing we do is store that letter in lettersGuessed. As you've already seen, other game logic depends a lot on the contents of this Set.

Lots of decisions to make in this one function. But here's how it breaks down:

  • If the player guesses a letter that is in the word, emit an onRight event, including the letter as a payload, just in case the UI wants to congratulate the player with a small fireworks display or something.
    • If the guess was correct and the players has successfully guessed all the word's letters, the game object will emit an onChange event, passing the completed word back for display. There is also an onWin event, since the player has escaped the gallows (this time).
    • If the guess was correct and the word is not yet complete, the game emits an onChange event with the updated display version of the word, now including the newly guessed letter(s).
  • If the guessed letter is not found in the word, we count the wrong guess and emit an onWrong event with the new total. The UI can use this information to change the gallows image.
    • If the guess was incorrect and the player has reached the maximum allowed wrong guesses, the game instance emits the onChange event with the full word, so the player can realize how foolish he was for not figuring it out. This is followed by an onLose event, which seals the player's doom.

It's important to remember that the HangmanGame class does not deal with the user interface in any way. Input comes in through public methods like newGame() and guessLetter(), and output is emitted in streams. Anywhere from zero to many external code entities could be listening for and acting on the output events. This pattern is what makes it possible to include the class, unchanged, in any Dart program for any platform, whether it be a command-line application, a mobile app, or maybe a web app built with the AngularDart framework.

Step 4: Words and Pictures

The game will start off with a random selection of 50 of English's most commonly used words, all between 3 and 7 letters in length. You'll also need 8 images to show the user's journey from healthy to deceased, and one more depicting an escape from the gallows for those smart and/or lucky enough to win the game. The images are publically hosted on Imgur, which will allow any app with Internet connectivity to access and display them.

The list of words is a lot to type and wouldn't easily fit into the article format, so I've helpfully defined it for you in a GitHub gist:

Get the words and image links here!

Copy the two Lists and one String, then paste them into your code above the main() function definition but below the import statements. Once done, it should look a little bit like the code below, but with lots more characters and no ellipses:

const List<String> wordList = const [...  
const List<String> imageList = const [...  
const String winImage = ...

void main() {  
    ...

The pictures for the game were generously created for us by my 6-year-old kid, so go easy on the judgment, jackass.

Note: If you are developing the game outside of DartPad for the web or Flutter, you can greatly increase the versatility of the word list using something like the english_words package. With only 50 possible words, the replay value isn't so great.

Step 5: Instantiate the Game Object

Now that you've got yourself a game class, you need to create an instance of it, initializing it with a list of words for guessing. Add this line directly above main():

HangmanGame game = new HangmanGame(wordList);  

With that in place, your UI code can communicate with the game engine via the game reference.

Step 6: The Alphabet

Think back on your days as either a child, a parent, or an office file clerk (everyone has been, or is, at least one of these). The one thing that should dominate your memory of this time is an ordered series of English glyphs we call the alphabet. Hangman needs HTML buttons representing each of the 26 letters, and to create those efficiently, we'll need a way to make the computer figure out the alphabet for us.

Sure, we could be stupid and do it one at a time like this:

<div id="letters">  
  <button>A</button>
  <button>B</button>
  <button>C</button>
  ...
</div>  

But if there's one thing computers love to do with their time, it's run loops. Turns out, ordinal sequences of characters are ideal loop fodder, so let's allow our computers to do what they were born to do, while saving ourselves some tedium. Add this helpful utility function anywhere below your main() function, but not inside the HangmanGame class:

List<String> generateAlphabet() {  
  // get the character code for a capital 'A'
  int startingCharCode = 'A'.codeUnits.first;

  // create a list of 26 character codes (from 'A' to 'Z')
  List<int> charCodes = new List<int>.generate(26, (int index) =>
    startingCharCode + index);

  // map character codes into a list of actual strings
  return charCodes.map((int code) =>
    new String.fromCharCode(code)).toList();
}

First, we need to grab the character code unit representation of a capital A. This is where the loop will start. We can't really loop over letters, of course, so we need to know the computer's internal numeric representation (or rune) for that character. Whatever that number is, B will come next, so we know we can just increment A's number to get B, then C, and so on.

Next, the List class's generate named constructor provides an easy way to grab 26 sequential character codes, starting at startingCharCode. That constructor needs the number of elements to return in the generated List, and it takes a generator function to produce each element. Since the generator function is provided the index of the current iteration, we can use that to calculate each successive element relative to the starting code.

Finally, we use the map method to convert the List of character codes into something we can use: Strings. For each numeric code, this is done with the String class's fromCharCode named constructor.

Step 7: More Helpful Getters

With Dart, you can add getters anywhere you want, even at the top level. You already have some experience adding them to the HangmanGame class, but here, you'll add some outside the class as well.

To manipulate screen elements, we'll need references to those elements, and these getters will provide them:

ImageElement get gallowsRef => querySelector("#gallows");  
DivElement get wordRef => querySelector("#word");  
DivElement get lettersRef => querySelector("#letters");  
ButtonElement get newGameRef => querySelector("#new-game");  

In each case, querySelector() from the dart:html library is used to find an HTML element by its id. There are several advantages to using getters for this. For one, gallowsRef is much nicer and cleaner than writing querySelector("#gallows") everywhere. Also, if we were ever to alter that image element's id, we might have to make changes all over our code base, but with a getter, updating it in the one place will do.

Step 8: Putting Letter Buttons on the Screen

In order for the player to make her wishes known, she'll need some letter buttons to press. The createLetterButtons() function will make use of generateAlphabet() to make this happen. Add this function to your Dart code (outside the HangmanGame class):

void createLetterButtons() {  
  // add letter buttons to the DOM
  generateAlphabet().forEach((String letter) {
    lettersRef.append(new ButtonElement()
      ..classes.add("letter-btn")
      ..text = letter
      ..onClick.listen((MouseEvent event) {
        (event.target as ButtonElement).disabled = true;
        game.guessLetter(letter);
      })
    );
  });
}

A lot of Darty goodness going on in there. The result of calling generateAlphabet() is a List of strings, each of which is a letter of the alphabet, starting with A and ending with Z. For each letter, the code will append() a brand new HTML ButtonElement. Each ButtonElement will have three properties adjusted, courtesy of Dart's amazing cascade operator (..). With cascades, you can perform multiple operations on the members of a single object: In this case, classes, text, and onClick.

First, the letter-btn CSS class is added to the button. Next, the text label of the button is set to a letter of the alphabet.

HTML elements communicate with Dart code using streams. Here, we set up a listener (or event handler) for each button's onClick events. When a button is clicked, we disable the button so the player can't click it more than once. Note that since MouseEvent's target member is typed as a generic Event, we have to cast it to ButtonElement with the as operator so the Dart analyzer knows it has a disabled attribute.

At the end of the click handler, we pass the player's guess to our instance of HangmanGame with the guessLetter() method. From there, the game object will do its magic and emit any events that result from processing the guess.

Step 9: Enabling

Since letter buttons get disabled as they're pressed, you need a way to re-enable them when it's time to start a new game. Add this function for that purpose:

void enableLetterButtons([bool enable = true]) {  
  // enable/disable all letter buttons
  lettersRef.children.forEach((Element btn) =>
    (btn as ButtonElement).disabled = !enable);
}

This function can actually enable or disable all the letter buttons. It takes an optional parameter, enable, which when true will cause the buttons to come alive. Since it's optional and defaults to true, you can make this happen by calling enableLetterButtons() without an argument, as well. Using a reference to the <div> containing the letter buttons, the code loops through each of that element's children (the buttons) and sets the disabled attribute appropriately.

Step 10: Just a Few More Helpers

The last few things the game code needs to be able to do easily are made possible by these four short functions:

void updateWordDisplay(String word) {  
  wordRef.text = word;
}

void updateGallowsImage(int wrongGuesses) {  
  gallowsRef.src = imageList[wrongGuesses];
}

void win([_]) {  
  gallowsRef.src = winImage;
  gameOver();
}

void gameOver([_]) {  
  enableLetterButtons(false);
  newGameRef.hidden = false;
}

We need to be able to update the way the word is displayed on screen whenever the game object tells us it's changed, and this is accomplished by updateWordDisplay(). Similarly, updateGallowsImage() uses the number of wrong guesses to set the HTML image element to the correct picture.

The win() and lose() functions both do fairly self-explanatory things, but the strange syntax in their parameter lists warrants a mention. These functions will be called in response to the onWin and onLose game events, and those happen to pass null as their payloads. Pretty useless, but there's no way to pass absolutely nothing, so that's how it is. Our handler functions have to be coded to be able to accept that null value, but since it serves no purpose, we use [_]. The square brackets make the parameter optional, so it's possible to call win() without passing anything if we need to. The underscore is a Dart convention signifying that the function will accept a parameter, but its value is of no interest.

Step 11: Another New Game

The HangmanGame class has a newGame() method that initializes some of its internal state, but we also need a version for the UI. And here it is:

void newGame([_]) {  
  // set up the game model
  game.newGame();

  // reset the letter buttons
  enableLetterButtons();

  // hide the New Game button
  newGameRef.hidden = true;

  // show the first gallows image
  updateGallowsImage(0);
}

Check it out! The app's newGame() function calls the game's newGame(). Neat. As the comments indicate, we also need to make sure all the letter buttons are enabled and ready to be touched, we need to get rid of the distracting New Game button, and we need to display the first image of the ominous gallows.

Step 12: Tying It All Together

Finally, we need to update main() to make use of all these nifty tools we've built:

void main() {  
  // set up event listeners
  game.onChange.listen(updateWordDisplay);
  game.onWrong.listen(updateGallowsImage);
  game.onWin.listen(win);
  game.onLose.listen(gameOver);
  newGameRef.onClick.listen(newGame);

  // put the letter buttons on the screen
  createLetterButtons();

  // start the first game
  newGame();
}

Remember all those events the HangmanGame object throws out? This is how we catch them. Every time there's an onChange event, we call updateWordDisplay(). Each of the other game events has a corresponding handler, plus we need to respond to times when the player clicks the New Game button.

Astute readers will notice that there is no listener for the onRight event. That's because, as written, this app has no use for that information, but another might.

With all of the event handlers set up, we do the one-time creation of the letter buttons, then start off the app right with a call to newGame(). That gets the ball rolling, and the game is officially playable (unless you screwed up somewhere).

Play!

Run the app!

Now that you've worked through one approach to a completely decoupled game model, you should try out all of its advantages. You can update and improve the existing UI without changing a thing in the game logic. You can build a mobile UI with Flutter or an AngularDart web app, once again using the same model. Go! Get outta here, ya freakin' lunatic!