In the first part of this tutorial, we set up the project and created the game's interface. We also created and implemented a function to create a deck of cards. In this second tutorial, we will create the game logic.
Sample Application
If you want to run the sample application of this tutorial, make sure to include images for the cars as I explained in the previous tutorial. Don't forget to include and update the dataSaver library mentioned in this tutorial.
1. enableDealButton
Update the implementation of the enableDealButton function as shown below.
function enableDealButton()
disableDealButton()
dealButton:addEventListener('tap',doDeal)
instructionsText.text = " "
end
We first call disableDealButton, which removes any previously added listeners, and add a tap listener, which invokes doDeal. The addEventListener method accepts an event and a callback. There are a number of events you can listen for, depending on the context in which you are calling it.
2. disableDealButton
As I mentioned in the previous section, in disableButton we remove any previously added listeners.
function disableDealButton()
dealButton:removeEventListener('tap',doDeal)
end
3. enableBetButtons
In enableBetButtons, we add tap listeners to betMaxButton and betButton, and give the player some instructions on how to place their bet.
function enableBetButtons()
betMaxButton:addEventListener('tap',betMax)
betButton:addEventListener('tap',bet)
instructionsText.text = "Place your Bet or Bet Max ($15)"
end
4. disableBetButtons
As in disableDealButton, we remove any previously added listeners in disableBetButtons.
function disableBetButtons()
betMaxButton:removeEventListener('tap',betMax)
betButton:removeEventListener('tap',bet)
end
5. enableHoldButtons
In enableHoldButtons, we loop through the holdButtons table and add a tap listener to each button.
function enableHoldButtons()
for i=1, #holdButtons do
holdButtons[i]:addEventListener('tap',holdCard)
end
end
6. disableHoldButtons
In the disableHoldButtons function, we also loop through the holdButtons table, but we remove previously added listeners instead of adding new listeners.
function disableHoldButtons()
for i=1, #holdButtons do
holdButtons[i]:removeEventListener('tap',holdCard)
end
end
7. generateCard
The implementation of generateCard requires a bit more explanation. We first generate a random number from 1 to the length of the deck table. We then create a temporary card using deck["randIndex]..".png" and store a reference in tempCard. What deck["randIndex]..".png" does, is grab a random element from the deck table, which would be something like c1 or h5 and adds .png to it. Because Lua is a dynamic language, we can add new properties to the objects. In this example, we add a isHolding property, which tells us whether the player is holding the card, a cardNumber property by getting a substring of the chosen deck element, and we do the same for the cardSuit property. Finally, we remove the chosen element from the deck table and return the array.
function generateCard() local randIndex = math.random(#deck) local tempCard = display.newImage(deck[randIndex]..".png") tempCard.anchorX, tempCard.anchorY = 0,0 tempCard.isHolding = false tempCard.cardNumber = tonumber(string.sub(deck[randIndex],2,3)) tempCard.cardSuit = string.sub(deck[randIndex],1,1) table.remove(deck,randIndex); return tempCard; end
8. getCard
In getCard, we set the cardPosition, which is the x coordinate of the first card in the game's interface. We generate a card, add it to the playerHand table, and pass in a cardIndex variable, which will be a number between 1 and 5, representing one of the five cards. This enables us to position the cards in the proper order in the playerHand table. We set the position of each card by using an offset of (93 * (cardIndex - 1)). This means that the cards are 93 pixels apart from one another.
function getCard(index) local cardPosition = 199 local tempCard = generateCard() playerHand[cardIndex] = tempCard tempCard.x = cardPosition + (93*(cardIndex-1)) tempCard.y = 257; end
9. holdCard
In holdCard, we first obtain a reference to the button that was pressed by using its buttonNumber property. This enables us to check whether the card is in the playerHand table. If it is, we set isHolding to false and update the card's y coordinate. If the card isn't in the playerHand table, we set isHolding to true and update the card's y coordinate. If the player chooses to hold the card, it's y coordinate is decreased, which means the card is moved up slightly.
function holdCard(event)
local index = event.target.buttonNumber
if (playerHand[index].isHolding == true) then
playerHand[index].isHolding = false
playerHand[index].y = 257
else
playerHand[index].isHolding = true
playerHand[index].y = 200
end
end
10. resetCardsYPosition
In resetCardsYPosition, we loop through the playerHand table and see if any of the cards are being held. The ones that are, are moved back to their original position using the Transition library. The Transition library makes it very easy to move objects around and tween their properties.
function resetCardsYPosition()
for i=1,#playerHand do
if (playerHand[i].isHolding) then
transition.to(playerHand[i], {time=200,y=257})
end
end
end
11. Persisting Data Across Sessions
We want our game to be able to persist values or data across sessions. We can build a solution ourselves using Corona's io library, but in this tutorial we are going to use a third party solution. Arturs Sosins has made a handy little module for persisting data across game sessions.
Download the library and add the two files it contains to your project. To make use of the library, add the following line at the top of main.lua.
saver = require("dataSaver")
To make the library work in our project, we need to make a few minor changes to dataSaver.lua. Open this file and change require "json" to local json = require "json".
Change:
require "json"
To:
local json = require "json"
The second change we need to make is change all the occurrences of system.ResourceDirectory to system.DocumentsDirectory as shown below.
Change:
system.ResourceDirectory
To:
system.DocumentsDirectory
12. createDataFile
To set up a data store, insert the following code snippet below the setupTextFields function. Make sure to include the function definition as we didn't stub this function in the previous tutorial.
function createDataFile()
gameData = saver.loadValue("gameData")
if (gameData == nil) then
gameData = {}
gameData.numberOfCredits = 100
gameData.numberOfGames = 0
creditText.text = "100"
gamesText.text = "0"
saver.saveValue("gameData",gameData)
else
creditText.text = gameData.numberOfCredits
gamesText.text = gameData.numberOfGames
end
end
In createDataFile, we first attempt to load the gameData key from the saver into the gameData variable. The loadValue method returns nil if the key doesn't exist. If it doesn't exist, we initialize the gameData table, add numberOfCredits and numberOfGames properties, update the respective text fields, and save the gameData table by invoking saveValue on saver. If the key does exist, then we've already done this and we can populate the text fields with the correct values.
In the next step, we invoke the createDataFile function in the setup function as shown below.
function setup() math.randomseed(os.time()) setupButtons() setupTextFields() createDataFile() end
13. betMax
In betMax, we start by loading our data into gameData. If the number of credits is greater than or equal to 15, we go ahead and call doDeal. Otherwise, the player does not have enough credits to bet the maximum of 15 credits and we show the player an alert.
function betMax()
local gameData = saver.loadValue("gameData")
local numberOfCredits = gameData.numberOfCredits
if (numberOfCredits >= 15) then
enableDealButton()
betAmount = 15;
betText.text = betAmount
instructionsText.text = " "
doDeal()
else
local alert = native.showAlert( "Not Enough Credits", "You must have 15 or more Credits to Bet Max", { "OK"})
end
end
14. bet
In the bet function, we enable the deal button and remove the listener from betMaxButton. Since the player is doing a regular bet, she cannot play a maximum bet at the same time. We need to check if the number of credits is greater than or equal to 5 to make sure that the player isn't trying to bet more credits than they have left. If betAmount is equal to 15, we call doDeal as they've bet the maximum amount.
function bet()
enableDealButton()
betMaxButton:removeEventListener('tap',betMax)
instructionsText.text = " "
local numberOfCredits = tonumber(creditText.text - betAmount)
if (numberOfCredits >= 5) then
betAmount = betAmount + 5
betText.text = betAmount
else
doDeal()
end
if (betAmount == 15) then
doDeal()
end
end
15. doDeal
The doDeal function coordinates the dealing of the cards. If it's a new game, we deal the initial hand. Otherwise, we deal the player new cards.
function doDeal()
if(isNewGame == true) then
isNewGame = false
dealInitialHand()
else
dealNewCards()
end
end
16. dealInitialHand
We disable the bet buttons in dealInitialHand and enable the hold buttons. We invoke getCard five times, which generates the initial five cards. We then load gameData, update currentCredits, calculate newCredits, update credit text field, and save gameData.
function dealInitialHand()
disableBetButtons()
enableHoldButtons()
for i=1, 5 do
getCard(i)
end
local gameData = saver.loadValue("gameData")
local currentCredits = gameData.numberOfCredits
local newCredits = currentCredits - betAmount
creditText.text = newCredits
gameData.numberOfCredits = newCredits
saver.saveValue("gameData",gameData)
end
17. dealNewCards
In dealNewCards, we check whether the player is holding a card. If they are, then we get a reference to the current card, call removeSelf, set it to nil, and get a new card by invoking getCard(i).
function dealNewCards()
disableDealButton()
disableHoldButtons()
for i = 1, 5 do
if (playerHand[i].isHolding == false) then
local tempCard = playerHand[i]
tempCard:removeSelf()
tempCard = nil
getCard(i)
end
end
resetCardsYPosition()
getHand()
end
nil to make sure it is setup for garbage collection properly.
18. getHand
The getHand function determines the player's hand. As you can see below, the function is pretty involved. Let's break if down to see what's going on. Start by implementing the getHand function as shown below.
function getHand()
table.sort(playerHand, function(a,b) return a.cardNumber < b.cardNumber end)
local frequencies = {}
for i=1, 13 do
table.insert(frequencies,0)
end
for i=1,#playerHand do
frequencies[playerHand[i].cardNumber] = frequencies[playerHand[i].cardNumber] + 1
end
local numberOfPairs = 0
local hasThreeOfAKind = false
local hasFourOfAKind = false
local winningHand = "Nothing"
local cashAward = 0
local isStraight = true
local isRoyalStraight = false
local isFlush = true
for i=0, #frequencies do
if (frequencies[i] == 2) then
numberOfPairs = numberOfPairs+1
end
if (frequencies[i] == 3) then
hasThreeOfAKind = true
end
if (frequencies[i] == 4) then
hasFour = true
end
end
if (numberOfPairs > 0) then
if(numberOfPairs == 1)then
winningHand = "1 pair"
cashAward = 1 * betAmount
else
winningHand = "2 pair"
cashAward = 2 * betAmount
end
end
if (hasThreeOfAKind) then
winningHand = "3 of A Kind"
cashAward = 3 * betAmount
end
if (hasFour) then
winningHand = "Four of A Kind"
cashAward = 7 * betAmount
end
if (numberOfPairs == 1 and hasThreeOfAKind) then
winningHand = "Full House"
cashAward = 6 * betAmount
end
if (playerHand[1].cardNumber == 1 and playerHand[2].cardNumber == 10 and playerHand[3].cardNumber == 11 and playerHand[4].cardNumber == 12 and playerHand[5].cardNumber == 13 )then
isRoyalStraight = true
end
for i=1, 4 do
if (playerHand[i].cardNumber+1 ~= playerHand[i+1].cardNumber) then
isStraight = false
break
end
end
for i=1, 5 do
if(playerHand[i].cardSuit ~= playerHand[1].cardSuit) then
isFlush = false
break
end
end
if (isFlush) then
winningHand = "Flush"
cashAward = 5 * betAmount
end
if (isStraight) then
winningHand = "Straight"
cashAward = 4 * betAmount
end
if (isRoyalStraight)then
winningHand = "Straight"
cashAward = 4 * betAmount
end
if (isFlush and isStraight) then
winningHand = "Straight Flush"
cashAward = 8 * betAmount
end
if (isFlush and isRoyalStraight) then
winningHand = "Royal Flush"
cashAward = 9 * betAmount
end
awardWinnings(winningHand, cashAward)
end
We start by calling table.sort on the playerHand table. The sort method does an in-place sort, it uses the operator to determine whether an element of the table should come before or after another element. We can pass a comparison function as the second argument. In our example, we check whether the cardNumber property is less than the previous one. If it is, then it swaps the two elements.
We then create a frequencies table, populate it with thirteen zeros (0), loop through the playerHand table, and increment each index's number if the playerHand contains that number. For example, if you had two three's and three fives, then the frequencies table would be 0, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0.
In the next step we declare and set a number of local variables that we need in a few moments. We then loop through the frequencies table and check the numbers to see if the index contains a 2, which means we have a pair, a 3, which means we have three of a kind, or a 4, which means we have four of a kind. Next, we check the number of pairs, three of a kind, and four of a kind, and update the winningHand and cashAward values accordingly.
To check for a Royal Straight, we need to check if the first card is equal to an ace and the remaining cards would be ten, Jack, Queen, and King. To check for a regular Straight we loop through the playerHand and check if each subsequent cardNumber is one greater than the previous. To check for a Flush, we check if all the cards cardSuit keys were equal to the first card's cardSuit key.
At the end of getHand, we invoke awardWinnings.
19. awardWinnings
In awardWinnings, we show the player what hand they have in their hand and update the gameData settings. We save gameData and invoke newGame with a delay of three seconds.
function awardWinnings(theHand, theAward)
instructionsText.text = "You got "..theHand
winText.text = theAward
local gameData = saver.loadValue("gameData")
local currentCredits = gameData.numberOfCredits
local currentGames = gameData.numberOfGames
local newCredits = currentCredits + theAward
local newGames = currentGames + 1
gameData.numberOfCredits = newCredits
gameData.numberOfGames = newGames
saver.saveValue("gameData",gameData)
timer.performWithDelay( 3000, newGame,1 )
end
20. newGame
In newGame, we go through and reset all the variables, create a new deck of cards, and check if gameData.numberOfCredits is equal to zero. If it is, then the player has expended all their credits so we award them 100 more credits. Finally, we update the text fields.
function newGame()
for i=1,#playerHand do
playerHand[i]:removeSelf()
playerHand[i] = nil
end
playerHand = {}
deck = {}
betAmount = 0
isNewGame = true
createDeck()
enableBetButtons()
instructionsText.text = "Place your Bet or Bet Max ($15)"
winText.text = ""
betText.text = ""
local gameData = saver.loadValue("gameData")
if (gameData.numberOfCredits == 0)then
gameData.numberOfCredits = 100
saver.saveValue("gameData",gameData)
end
creditText.text = gameData.numberOfCredits
gamesText.text = gameData.numberOfGames
end
21. Testing the Game
Update the setup function by invoking the createDeck function as shown below and test the final result.
function setup() math.randomseed(os.time()) createDeck(); setupButtons() setupTextFields() createDataFile() end
Conclusion
In this tutorial, we created a fun and interesting Poker game. We did not implement the button to cash out yet, but you are free to do so in your game. I hope you learned something useful in this tutorial. Leave your feedback in the comments below.
Comments