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.