Create a Dancing Hangman Game in Corona: Gameplay

Final product image
What You'll Be Creating

In the first part of this two-part series, we laid the foundation for transitioning between screens and for drawing the hangman using Corona's drawing API. In the second and final part of this series, we will implement the game logic, and transition to a win screen where the hangman will do a dance along with some music. Lets get started.

1. Creating the Word to Guess

Step 1: Reading a Text File

The game reads in words from a text file that contains thousands of words. We will filter the words based on their length and add them to one of three lists, words that are 5 characters long or less, words that are 9 characters long or less, and words that are 13 characters long or less.

By having three separate word lists, we could integrate a difficulty level based on word length. We will not be doing that in this tutorial, but the words will already be separated into separate lists should you decide to pursue that option on your own.

Add the following code to gamescreen.lua.

The readTextFile function reads in the text file wordlist.txt. We loop through each line of  wordlist.txt and, depending on the word's length, insert it into one of three tables, words5 , words9, or words13.

Windows and Unix-based systems handle line endings differently. On Windows, there will be an extra character, which we can remove using the string.sub method. If you are using a Windows machine, you need to add that line of code by removing the -- preceding it.

Whenever you read a file, it's important to remember that once you're done, you should close and nil out the file as shown in the above implementation.

Invoke this function in scene:create. I placed it at the very top, above the other function calls.

Step 2: CreateGuessWord

The createGuessWord function is responsible for getting a random word from the list and returning it. Add the following code to gamescreen.lua.

We use a table, guessWord, to store each letter of the word. The reason for this is that strings are immutable in Lua, meaning that we cannot change a character of the string.

We first generate a random number randomIndexwhich will be a number from 1 to however many items are in the table. We then use that number to get a word from the table.

We loop over the word and get a reference to the current character by using the string.sub method. If the current character is an apostrophe or a dash, we put that into the table guessword, otherwise we put a question mark into the table.

We then transform the table guessWord into a string newGuessWord using the table.concat method. Lastly, we return newGuessWord.

Step 3: CreateGuessWordText

The createGuessWordText function creates the Text at the top of the game area that will change depending on the player's guess.

The options table holds the various configuration options for the Text. Because the createGuessWord function returns a word as a string, we can just invoke it when setting the text property.

We create the Text by invoking the newText method  on Display, passing in the options table. We then set its color and insert it into the scene's view.

Invoke this function in scene:create as shown below.

A Word about Metatables

The Lua programming language does not have a class system built in. However, by using Lua's metatable construct we can emulate a class system. There is a good example on the Corona website, showing how to implement this.

An important thing to note is that Corona's Display objects cannot be set as the metatable. This has to do with how the underlying C language interfaces with them. A simple way to get around this is to set the Display object as a key on a new table and then set that table as the metatable. This is the approach that we'll take in this tutorial.

If you read the above article on the Corona website, you will have noticed that the __Index metamethod was being used on the metatable. The way the __Index metamethod works, is that when you try to access an absent field in a table, it triggers the interpreter to look for an __Index metamethod. If the __Index is there, it will look for the field and provide the result, otherwise it will result in nil.

2. Implementing the GrowText Class

The game has Text that will grow from small to large over a short period of time, when the user wins or loses the game. We will create this functionality as a module. By having this code as a module, we can reuse it in any project that requires this functionality.

Add the following to growtext.lua, which you created in the first part of this series.

We create the main table growText and the table to be used as the metatable, growText_mt. In the new method, we create the Text object and add it to the table newGrowText that will be set as the metatable. We then add the Text object to the group that was passed in as a parameter, which will be the scene's group in which we instantiate an instance of GrowText.

It's important to make sure that we add it to the scene's group so it will be removed when the scene is removed. Finally, we set the metatable.

We have four methods that access the Text object and perform operations on its properties.

setColor

The setColor method sets the color on the Text by invoking the setFillColor method, which takes as parameters the R, G, and B values numbers from 0 to 1.

grow

 The grow method uses the Transition library to make the text grow. It enlarges the text by using the xScale and yScale properties. The onComplete function gets invoked once the transition is complete.

In this onComplete function, we reset the Text's xScale and yScale properties to 1 and dispatch an event. The reason we dispatch an event here is to inform the gamescreen that the Text has finished its transition, and therefore the game round is over.

This will all become clear soon, but you may want to read up on dispatchEvent in the documentation.

setVisibility

The setVisibility method simply sets the visibility of the Text, depending on whether true or false was passed in as a parameter. We also reset the xScale and yScale property to 1.

setText

The setText method is used to set the actual text property, depending on whatever string was passed in as the parameter.

Lastly, we return the growText object.

3. createWinLoseText

The createWinLoseText function creates a GrowText object that will show either "YOU WIN!" or "YOU LOSE!", depending on whether the user wins or loses a round. Add the following code to gamescreen.lua.

We invoke this function in scene:create as shown below.

4. setupButtons

The setupButtons function sets up the buttons, draws them to the screen, and adds an event listener that will call the function checkLetter. The checkLetter function is where the game's logic takes place.

We first set the initial x and y positions of the buttons. We then run a for loop over the alphabetArray, which in turn creates a button for every letter of alphabetArray. We want eight buttons per row so we check if i is equal to 9 or 17, and, if true, we increment the yPos variable to create a new row and reset the xPos to the beginning position. If i is equal to 25, we are on the last row and we center the last two buttons.

We create a tempButton by using the newButton method of the widget class, which takes a table of options. There are several ways to affect the visual appearance of the buttons. For this game, we are using the Shape Construction option. I highly suggest you read the documentation on the Button object to learn more about these options.

We set the label by indexing into alphabetArray and set the onPress property to call checkLetter. The rest of the options have to do with the visual appearance and are better explained by reading the documentation as mentioned earlier. Lastly, we insert the tempButton into the table gameButtons so we can reference it later.

If you now invoke this method from scene:create, you should see that the buttons are drawn to the screen. We cannot tap them yet though, because we have not created the checkLetter function. We'll do that in the next step.

5. checkLetter

The game's logic lives in the checkLetter function. Add the following code to gamescreen.lua.

The first thing we do is, get the letter the user has guessed by invoking the getLabel method on the button. This returns the button's label, a capitalized letter. We convert this letter to lowercase by invoking the lower method on string, which takes as its parameter the string to be lowercased.

We then set correctGuess to false, newGuessWord to an empty string, and hide the button the user has tapped, because we don't want the user to be able to tap the button more than once per round.

Next, we loop over theWord, get the current character by using the string.sub method, and compare that character to theLetter. If they are equal, then the user has made a correct guess and we update that particular letter in guessWord, setting correctGuess to true.

We create newGuessWord by using the table.concat method, and update the guessWordText to reflect any changes.

If correctGuess is still false, it means the user has made an incorrect guess. As a result, we increment the numWrong variable and invoke the drawHangman function, passing in numWrong. Depending on how many wrong guesses the user has made, the drawHangman function will draw the hangman as appropriate.

If newGuessWord is equal to theWord, it means the user has guessed the word and we update wonGame to true, calling the didWinGame function and passing in true.

If numWrong is equal to 6, it means the user has used up all their guesses and the hangman has been fully drawn. We loop through theWord and set every character in guessWord equal to the characters in theWord. We then show the user the correct word.

This bit of code should make sense by now as we have done something similar a couple of times before. Lastly, we call didWinGame and pass in false.

6. Winning and Losing

Step 1: didWinGame

The didWinGame function is called when the use either wins of loses a round.

The first thing we do is invoke hideButtons, which, as the name suggests, hides all of the buttons. We set the winLoseText to be visible, and, depending on whether the user won or lost the round, set its text and color as appropriate. Lastly, we invoke the grow method on the winLoseText.

As we saw earlier in this tutorial, once the text has finished growing, it dispatches an event. We need to use the Runtime to listen for that event. We will be coding this functionality in the upcoming steps.

Step 2: Showing and Hiding the Buttons

The showButtons and hideButtons functions show and hide the buttons by looping through the gameButtons table, setting each of the button's visibility.

Step 3: drawHangman

The drawHangman function takes a number as a parameter. Depending on what that number is, it draws a certain part of the hangman.

Step 4: Test Progress

It has been quite a while since we have checked our progress, but if you test now you should be able to play a few rounds. To reset the game go to File > Relaunch in the Corona Simulator. Remember, the correct word is being printed to the console so that should help you test everything is working as it should.

When the winLoseText is finished growing, we will start a new round. If the user has won the round, we will go to a new scene where the hangman will do a happy dance. If the user has lost, we will reset everything in gamescreen.lua and begin a new round.

Before we do any of that, however, we need to listen for the gameOverEvent that is being dispatched from the growText class.

7. Game Over

Step 1: Listening for gameOverEvent

Add the following to the scene:show method.

We pass the gameOverEvent as the first argument of the addEventListener method. When a gameOverEvent is dispatched, the gameOver function is called.

We should also remove the event listener at some point. We do this in the scene:hide method as shown below.

Step 2: gameOver

Add the following code to gamescreen.lua.

If the user has won the game, we invoke the gotoScene method on the composer object and transition to the gameoverscreen. If not, we call the newGame method, which resets the game and creates a new word.

Step 3: newGame

The newGame function resets some variables, sets the buttons to be visible, clears the hangmanGroup, and creates a new word.

Most of this code should look familiar to you. The clearHangmanGroup function is the only thing new here and we will look at that function in the next step.

Step 4: clearHangmanGroup

The clearHangmanGroup simply loops through the hangmanGroup's numChildren and removes them. Basically, we are clearing everything out so we can start drawing afresh.

Step 5: Test Progress

We are at a point where we can test the progress once again. If you test the game, you can lose a game and a new game should start. You can do this for as long as you wish. In the next step we will get the gameoverscreen wired up.

8. Game Over Screen

Create a new file gameoverscreen.lua and add the following code to it.

The hangmanSprite is a SpriteObject that will be used for the dancing animation. The hangmanAudio is an AudioObject that will be used to play some music while the hangman does its dance.

Step 1: Animating the Hangman

As I mentioned, the hangmanSprite is a SpriteObject instance and by having the hangmanSprite be a sprite instead of a regular image, we can animate it. The hangmanSprite has 86 separate images, each one being a different frame for the animation. You can see this by opening hangmanSheet.png in an image editor.

The options table holds the width, height, and numFrames of the individual images in the larger image. The numFrames variable contains the value of the number of smaller images. The hangmanSheet is an instance of the ImageSheet object, which takes as its parameters the image and the options table.

The sequenceData variable is used by the SpriteObject instance, the start key is the image you wish to start the sequence or animation with, and the count key is how many total images there are in the animation. The time key is how long it will take the animation to play and the loopCount key is how many times you wish the animation to play or repeat.

Lastly, you create the SpriteObject instance by passing in the ImageSheet instance and sequenceData.

We set the hangmanSprite's x and y positions, and scale it up to 1.5 its normal size by using the xScale and yScale properties. We insert it into the group to make sure it is removed whenever the scene is removed.

Step 2: Play Audio

Inside scene:show, we remove the previous scene, add an event listener to the hangmanSprite and invoke its play method. We instantiate the hangmanAudio by invoking the loadSound method, passing in danceMusic.mp3. Lastly we call the play method to start the sound playing.

In the scene:hide method, we remove the event listener from the hangmanSprite, invoke stop on the audio instance, and invoke the dispose method. The dispose method ensures that the memory that was allocated to the audio instance is released.

Step 3: Cleaning Up

In the hangmanListener, we check if it is in the end phase, and, if true, it means the animation has finished playing. We then invoke timer.performWithDelay. The timer fires after one second, invoking the newGame method, which uses composer to transition back to the gamescreen to begin a new game.

Conclusion

This was quite a long tutorial, but you now have a functional hangman game with a nice twist. As mentioned at the beginning of this tutorial, try to incorporate difficulty levels. One option would be to have an options screen and implement a SegmentedControl where the user could choose between the lists of 5, 9, and 13 letter words.

I hope you found this tutorial useful and have learned something new. Thanks for reading.

Tags:

Comments

Related Articles