Advertisement
  1. Code
  2. Mobile Development
  3. Corona

Create a Dancing Hangman Game in Corona: Gameplay

Scroll to top
Read Time: 19 min
This post is part of a series called Create a Dancing Hangman Game in Corona.
Create a Dancing Hangman Game in Corona: Project Setup
Final product imageFinal product imageFinal 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.

1
function readTextFile()
2
    local path = system.pathForFile( "wordlist.txt",  	system.ResourceDirectory)
3
    local file = io.open( path, "r" )
4
    for line in file:lines() do
5
		--If targeting Windows Operating System comment the following line out
6
		-- line = string.sub(line,  1, #line - 1)
7
		 if(#line >=3 and #line <=5)then
8
			table.insert(words5,line)
9
		elseif (#line >=3 and #line<=9)then
10
			table.insert(words9,line)
11
		elseif (#line >=3 and #line<=13)then
12
			table.insert(words13,line)
13
		 end
14
    end
15
	io.close( file )
16
    file = nil
17
end

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.

1
function scene:create( event )
2
    local group = self.view
3
	readTextFile()
4
	drawChalkBoard(1,1,1)
5
    --SNIP--
6
end

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.

1
function createGuessWord()
2
    guessWord = {}
3
	local randomIndex = math.random(#words5)
4
	theWord = words5[randomIndex];
5
	print(theWord)
6
	for i=1, #theWord do
7
		local character= theWord:sub(i,i)
8
        if(character == "'")then
9
			guessWord[i] ="'";
10
		elseif(character=="-")then
11
			guessWord[i] = "-"
12
		else
13
			guessWord[i]="?";
14
		end
15
	end
16
	local newGuessWord = table.concat(guessWord)
17
	return newGuessWord;
18
end

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.

1
function createGuessWordText()
2
    local options = 
3
		{
4
            text = createGuessWord(),     
5
			x = 384,
6
	    	y = 70,
7
	    	width = 700,     --required for multi-line and alignment
8
	    	font = native.systemFontBold,   
9
	    	fontSize = 50,
10
	   	 align = "center"  --new alignment parameter
11
		}
12
	guessWordText = display.newText(options)
13
	guessWordText:setFillColor(0,0,0)
14
    scene.view:insert(guessWordText)
15
end

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.

1
function scene:create( event )
2
    --SNIP--
3
	drawGallows()
4
	createGuessWordText()
5
end

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.

1
local growText = {}
2
local growText_mt = {__index = growText}
3
4
function growText.new(theText,positionX,positionY,theFont,theFontSize,theGroup)
5
	local theTextField = display.newText(theText,positionX,positionY,theFont,theFontSize)
6
	local newGrowText = {
7
    theTextField = theTextField}
8
        if(theGroup ~=nil)then
9
    theGroup:insert(theTextField) 
10
    end                                       
11
	return setmetatable(newGrowText,growText_mt)
12
end
13
14
function growText:setColor(r,b,g)
15
  self.theTextField:setFillColor(r,g,b)
16
end
17
18
function growText:grow()
19
	transition.to( self.theTextField, { xScale=4.0, yScale=4.0, time=2000, iterations = 1,onComplete=function()
20
			local event = {
21
						name = "gameOverEvent",
22
			}
23
			self.theTextField.xScale = 1
24
			self.theTextField.yScale = 1
25
	Runtime:dispatchEvent( event )
26
end
27
	} )
28
end
29
30
function growText:setVisibility(visible)
31
  if(visible == true)then
32
  	self.theTextField.isVisible = true
33
  else
34
  	self.theTextField.isVisible = false
35
  end
36
  self.theTextField.xScale = 1
37
  self.theTextField.yScale = 1
38
end
39
40
function growText:setText(theText)
41
	self.theTextField.text = theText
42
end
43
return growText

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.

1
function createWinLoseText()
2
    winLoseText =  growText.new( "YOU WIN",display.contentCenterX,display.contentCenterY-100, native.systemFontBold, 20,scene.view)
3
    winLoseText:setVisibility(false)
4
end

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

1
function scene:create( event )
2
    --SNIP--
3
    drawGallows()
4
	createWinLoseText()
5
end

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.

1
function setupButtons()
2
    local xPos=150
3
	local yPos = 600
4
	for i=1, #alphabetArray do
5
		if (i == 9 or i == 17) then
6
			yPos = yPos + 65
7
			xPos = 150
8
		end
9
		if (i == 25) then
10
			yPos = yPos + 65
11
			xPos = 330
12
		end
13
		local tempButton = widget.newButton{
14
			label = alphabetArray[i],
15
			labelColor = { default ={ 1,1,1}},
16
			onPress = checkLetter,
17
			shape="roundedRect",
18
            width = 40,
19
   	 	    height = 40,
20
    		cornerRadius = 2,
21
    		fillColor = { default={0, 0, 0, 1 }, over={ 0.5, 0.5, 0.5, 0.4 } },
22
    		strokeColor = { default={ 0.5, 0.5, 0.5, 0.4 }, over={ 0, 0, 0, 1  } },
23
    		strokeWidth = 5
24
		}
25
		tempButton.x = xPos
26
		tempButton.y = yPos
27
		
28
    xPos = xPos + 60
29
	table.insert(gameButtons,tempButton)
30
	end
31
end

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.

1
function scene:create( event )
2
    --SNIP--
3
	createGuessWordText()
4
	createWinLoseText()
5
	setupButtons()
6
end

5. checkLetter

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

1
function checkLetter(event)
2
    local tempButton = event.target
3
	local theLetter = tempButton:getLabel()
4
	theLetter = string.lower(theLetter)
5
	local correctGuess  = false
6
	local newGuessWord = ""
7
    tempButton.isVisible = false
8
	for  i =1 ,#theWord do
9
		local character= theWord:sub(i,i)
10
		if(character == theLetter)then
11
            guessWord[i] = theLetter
12
			correctGuess = true
13
		end
14
	end
15
	newGuessWord = table.concat(guessWord)
16
	guessWordText.text = newGuessWord
17
	if(correctGuess == false)then
18
		numWrong = numWrong +1
19
        drawHangman(numWrong);
20
	end
21
	if(newGuessWord == theWord)then 
22
		wonGame = true
23
		didWinGame(true)	
24
	end
25
	if(numWrong == 6) then
26
	    for i =1 , #theWord do
27
			guessWord[i] = theWord:sub(i,i)
28
            newGuessWord = table.concat(guessWord)
29
			guessWordText.text = newGuessWord;
30
		end
31
	didWinGame(false)
32
    end
33
end

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.

1
function didWinGame(gameWon)
2
    hideButtons()
3
	winLoseText:setVisibility(true)
4
	if(gameWon == true)then
5
		winLoseText:setText("YOU WIN!!")
6
		winLoseText:setColor(0,0,1)
7
	else
8
		winLoseText:setText("YOU LOSE!!")
9
		winLoseText:setColor(1,0,0)
10
	end
11
     winLoseText:grow()
12
end

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.

1
function hideButtons()
2
    for i=1, #gameButtons do
3
    	gameButtons[i].isVisible = false
4
	end
5
end
6
7
function showButtons()
8
	for i=1, #gameButtons do
9
		gameButtons[i].isVisible = true
10
	end
11
end

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.

1
function drawHangman(drawNum)
2
    if(drawNum== 0) then
3
    	drawGallows();
4
	elseif(drawNum ==1)then
5
        drawHead();
6
	elseif(drawNum == 2) then
7
        drawBody();
8
	elseif(drawNum == 3) then
9
		drawArm1();
10
	elseif(drawNum == 4) then
11
		drawArm2();
12
	elseif(drawNum == 5) then
13
		drawLeg1();
14
	elseif(drawNum == 6) then
15
		drawLeg2();
16
	end
17
end

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.

1
function scene:show( event )
2
    --SNIP--
3
    if ( phase == "did" ) then
4
        Runtime:addEventListener( "gameOverEvent", gameOver )
5
    end
6
end

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.

1
function scene:hide( event )
2
    local phase = event.phase
3
    if ( phase == "will" ) then
4
    	Runtime:removeEventListener( "gameOverEvent", gameOver )
5
	end
6
end

Step 2: gameOver

Add the following code to gamescreen.lua.

1
function gameOver()
2
    winLoseText:setVisibility(false)
3
	if(wonGame == true)then
4
		composer.gotoScene("gameoverscreen")
5
	else
6
		newGame()
7
	end
8
end

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.

1
function newGame()
2
    clearHangmanGroup()
3
	drawHangman(0)
4
	numWrong = 0
5
	guessWordText.text = createGuessWord()
6
	showButtons()
7
end

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.

1
function clearHangmanGroup()
2
    for i = hangmanGroup.numChildren, 1 ,-1 do
3
        hangmanGroup[i]:removeSelf()
4
    	hangmanGroup[i]=nil
5
end 

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.

1
local composer = require( "composer" )
2
local scene = composer.newScene()
3
local hangmanSprite 
4
local hangmanAudio
5
function scene:create( event )
6
    local group = self.view
7
	drawChalkBoard()
8
	local options = { width = 164,height = 264,numFrames = 86}
9
	local hangmanSheet = graphics.newImageSheet( "hangmanSheet.png", options )
10
	local sequenceData = {
11
  	 {  start=1, count=86, time=8000,   loopCount=1 }
12
	}
13
	hangmanSprite = display.newSprite( hangmanSheet, sequenceData )
14
	hangmanSprite.x = display.contentCenterX
15
    hangmanSprite.y = display.contentCenterY
16
    hangmanSprite.xScale = 1.5
17
    hangmanSprite.yScale = 1.5
18
    group:insert(hangmanSprite)
19
end
20
21
function scene:show( event )
22
	local phase = event.phase
23
    local previousScene =  composer.getSceneName("previous")
24
	composer.removeScene(previousScene)
25
    if ( phase == "did" ) then
26
	    hangmanSprite:addEventListener( "sprite", hangmanListener )
27
		hangmanSprite:play()
28
        hangmanAudio = audio.loadSound( "danceMusic.mp3" )
29
		audio.play(hangmanAudio)
30
   end
31
end
32
33
function scene:hide( event )
34
	local phase = event.phase
35
	if ( phase == "will" ) then
36
		hangmanSprite:removeEventListener( "sprite", hangmanListener )
37
		audio.stop(hangmanAudio)
38
		audio.dispose(hangmanAudio)
39
		end
40
end
41
42
function drawChalkBoard()
43
    local chalkBoard = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
44
	chalkBoard:setFillColor(1,1,1 )
45
	chalkBoard.anchorX = 0
46
	chalkBoard.anchorY = 0
47
	scene.view:insert(chalkBoard)
48
end
49
50
51
52
function hangmanListener( event )
53
 	
54
if ( event.phase == "ended" ) then
55
	 	timer.performWithDelay(1000,newGame,1)
56
	end
57
end
58
59
function newGame()
60
	composer.gotoScene("gamescreen")
61
end
62
scene:addEventListener( "create", scene )
63
scene:addEventListener( "show", scene )
64
scene:addEventListener( "hide", scene )
65
66
return scene

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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.