Building Hangman
Tuesday, December 26, 2023
Recently, Jon Fincher published an interesting Python tutorial describing steps to build a hangman game for the command line in Python. It provides for a nice demo of different programming language features including taking user input, printing to the screen, storing game state, and performing some logic until the game is completed.
A game in progress might look like this – with hangman as the word being guessed:
┌───┐
│ │
O │
─┼─ │
/ │ │
│ │
│
│
│
│
└──────┘
Your word is: _ a n _ _ a n
Your guesses: a e r s n
I thought it would be fun to show how to build a similar hangman game in Factor, using similar steps.
Step 1: Set Up the Hangman Project
We need to create the hangman
vocabulary to store our work. We can use the
scaffold-vocab
word to create a new vocabulary. It will prompt for which
vocab-root to place the new vocabulary into.
IN: scratchpad USE: tools.scaffold
IN: scratchpad "hangman" scaffold-vocab
And then open the vocab in your favorite text editor:
IN: scratchpad "hangman" edit-vocab
We can start the file with all these imports, which we will be using in the implementation below and two symbols that we will use to hold the state of our game.
USING: combinators.short-circuit io io.encodings.utf8 io.files
kernel make math multiline namespaces random sequences
sequences.interleaved sets sorting unicode ;
IN: hangman
SYMBOL: target-word ! the word being guessed
SYMBOL: guesses ! all of the guessed letters
Step 2: Select a Word to Guess
Let’s create a vocab:hangman/words.txt
file containing all of the possible
word choices. The original tutorial had a list of words that you can reference
– you are welcome to copy my file or create your own:
IN: scratchpad USE: http.client
IN: scratchpad "https://raw.githubusercontent.com/mrjbq7/re-factor/master/hangman/words.txt"
"vocab:hangman/words.txt" download-to
Now we can add this word to read the file into memory and then choose a random line.
: random-word ( -- word )
"vocab:hangman/words.txt" utf8 file-lines random ;
Step 3: Get and Validate the Player’s Input
The user will use the
readln word to
provide input, and we will validate it by making sure the line contains a
single character that is not already in our guesses
:
: valid-guess? ( input -- ? )
{
[ length 1 = ]
[ lower? ]
[ first guesses get ?adjoin ]
} 1&& ;
Reading the player input is then just looping until we get a valid guess:
: player-guess ( -- ch )
f [ dup valid-guess? ] [ drop readln ] do until first ;
Step 4: Display the Guessed Letters and Word
We can display the guessed letters as a sorted, space-separated list:
: spaces ( str -- str' )
CHAR: \s <interleaved> ;
: guessed-letters ( -- str )
guesses get members sort spaces ;
And the target word is also space-separated with blanks for letters we have not guessed, or the actual letters if they have been guessed successfully:
: guessed-word ( -- str )
target-word get guesses get '[
dup _ in? [ drop CHAR: _ ] unless
] map spaces ;
Step 5: Draw the Hanged Man
We first calculate the number of wrong guesses, by set difference between the guesses and our target word:
: #wrong-guesses ( -- n )
guesses get target-word get diff cardinality ;
Displaying the “hanged man” requires a bit more lines of code that the rest of the program, using the number of wrong guesses to pick which to output:
CONSTANT: HANGED-MAN {
[[
┌───┐
│ │
│
│
│
│
│
│
│
│
└──────┘
]] [[
┌───┐
│ │
O │
│
│
│
│
│
│
│
└──────┘
]] [[
┌───┐
│ │
O │
─┼─ │
│ │
│ │
│
│
│
│
└──────┘
]] [[
┌───┐
│ │
O │
─┼─ │
/ │ │
│ │
│
│
│
│
└──────┘
]] [[
┌───┐
│ │
O │
─┼─ │
/ │ \ │
│ │
│
│
│
│
└──────┘
]] [[
┌───┐
│ │
O │
─┼─ │
/ │ \ │
│ │
─┴─ │
/ │
│ │
│
└──────┘
]] [[
┌───┐
│ │
O │
─┼─ │
/ │ \ │
│ │
─┴─ │
/ \ │
│ │ │
│
└──────┘
]]
}
: hanged-man. ( -- )
#wrong-guesses HANGED-MAN nth print ;
Step 6: Figure Out When the Game Is Over
The game is lost when the player has too many wrong guesses:
: lose? ( -- ? )
#wrong-guesses HANGED-MAN length 1 - >= ;
The game is won when the word has no unknown letters:
: win? ( -- ? )
target-word get guesses get diff null? ;
And the game is over when it is won or lost:
: game-over? ( -- ? )
{ [ win? ] [ lose? ] } 0|| ;
Step 7: Run the Game Loop
It is frequently useful in Factor to build helper words that, for example, set up some of the state that our program will use and then run a provided quotation:
: with-hangman ( quot -- )
[
random-word target-word ,,
HS{ } clone guesses ,,
] H{ } make swap with-variables ; inline
And then we can use that to build and run the game:
: play-hangman ( -- )
[
"Welcome to Hangman!" print
[ game-over? ] [
hanged-man.
"Your word is: " write guessed-word print
"Your guesses: " write guessed-letters print
nl "What is your guess? " write flush
player-guess target-word get in?
"Great guess!" "Sorry, it's not there." ? print
] until
hanged-man.
lose? "Sorry, you lost!" "Congrats! You did it!" ? print
"Your word was: " write target-word get print
] with-hangman ;
One last thing we can do is set this word as the main entry point of our vocabulary:
MAIN: play-hangman
Next Steps
Well, that’s kind of fun. You can run this in the listener:
IN: scratchpad "hangman" run
Or at the command-line:
$ ./factor -run=hangman
The source code is available on my GitHub.