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. In fact, there's a whole article on how you can do it: Simple Games with Flutter: Hangman.
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 v2.0.0.
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:
- Dart SDK for Web
- On Windows? Try the Dart for Windows installer.
- Dart-Up is a great option for Windows, OSX, or Linux.
- Code editor
- JetBrains WebStorm is the officially recommended Dart editor.
- See the Dart Tools page for other options (most of them free).
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 player 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 anonWin
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 guess was correct and the player has successfully guessed all the word's letters, the game object will emit an
- 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 anonLose
event, which seals the player's doom.
- If the guess was incorrect and the player has reached the maximum allowed wrong guesses, the game instance emits the
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!