1. Code
  2. Mobile Development
  3. Corona

Build a Poker Game in Corona: Game Logic

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.
Scroll to top

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.

1
2
function enableDealButton()
3
  disableDealButton()
4
  dealButton:addEventListener('tap',doDeal)
5
  instructionsText.text = " "
6
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.

1
2
function disableDealButton()
3
  dealButton:removeEventListener('tap',doDeal)
4
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.

1
2
function enableBetButtons()
3
  betMaxButton:addEventListener('tap',betMax)
4
  betButton:addEventListener('tap',bet)
5
  instructionsText.text = "Place your Bet or Bet Max ($15)"
6
end

4. disableBetButtons

As in disableDealButton, we remove any previously added listeners in disableBetButtons.

1
2
function disableBetButtons()
3
  betMaxButton:removeEventListener('tap',betMax)
4
  betButton:removeEventListener('tap',bet)
5
end

5. enableHoldButtons

In enableHoldButtons, we loop through the holdButtons table and add a tap listener to each button.

1
2
function enableHoldButtons()
3
  for i=1, #holdButtons do
4
      holdButtons[i]:addEventListener('tap',holdCard)
5
  end
6
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.

1
2
function disableHoldButtons()
3
  for i=1, #holdButtons do
4
      holdButtons[i]:removeEventListener('tap',holdCard)
5
  end
6
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.

1
2
function generateCard()
3
  local randIndex = math.random(#deck)
4
  local tempCard = display.newImage(deck[randIndex]..".png")
5
  tempCard.anchorX, tempCard.anchorY = 0,0
6
  tempCard.isHolding = false
7
  tempCard.cardNumber  =  tonumber(string.sub(deck[randIndex],2,3))
8
  tempCard.cardSuit = string.sub(deck[randIndex],1,1)
9
  table.remove(deck,randIndex);
10
  return tempCard;
11
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.

1
2
function getCard(index)
3
  local cardPosition = 199
4
  local tempCard = generateCard()
5
  playerHand[cardIndex] = tempCard
6
  tempCard.x = cardPosition + (93*(cardIndex-1))
7
  tempCard.y = 257;
8
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.

1
2
function holdCard(event)
3
  local index = event.target.buttonNumber
4
  if (playerHand[index].isHolding == true) then
5
    playerHand[index].isHolding = false
6
    playerHand[index].y = 257
7
  else
8
    playerHand[index].isHolding = true
9
    playerHand[index].y = 200
10
  end
11
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.

1
2
function resetCardsYPosition()
3
  for i=1,#playerHand do
4
    if (playerHand[i].isHolding) then
5
      transition.to(playerHand[i], {time=200,y=257})
6
    end
7
  end
8
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.

1
2
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:

1
2
require "json"

To:

1
2
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:

1
2
system.ResourceDirectory

To:

1
2
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.

1
2
function createDataFile()
3
  gameData = saver.loadValue("gameData")
4
  if (gameData == nil) then
5
    gameData = {}
6
    gameData.numberOfCredits = 100
7
    gameData.numberOfGames = 0
8
    creditText.text = "100"
9
    gamesText.text = "0"
10
    saver.saveValue("gameData",gameData)
11
  else
12
    creditText.text = gameData.numberOfCredits
13
    gamesText.text = gameData.numberOfGames
14
   end
15
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.

1
2
function setup()
3
  math.randomseed(os.time())
4
  setupButtons()
5
  setupTextFields()
6
  createDataFile()
7
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.

1
2
function betMax()
3
  local gameData = saver.loadValue("gameData")
4
  local numberOfCredits = gameData.numberOfCredits
5
  if (numberOfCredits >= 15) then
6
    enableDealButton()
7
    betAmount = 15;
8
    betText.text = betAmount
9
    instructionsText.text = " "
10
    doDeal()
11
  else
12
    local alert = native.showAlert( "Not Enough Credits", "You must have 15 or more Credits to Bet Max", { "OK"})
13
  end
14
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.

1
2
function bet()
3
  enableDealButton()
4
  betMaxButton:removeEventListener('tap',betMax)
5
  instructionsText.text = " "
6
  local numberOfCredits = tonumber(creditText.text - betAmount)
7
  if (numberOfCredits >= 5) then
8
    betAmount = betAmount + 5
9
    betText.text = betAmount
10
  else
11
    doDeal()
12
  end
13
14
  if (betAmount == 15) then
15
    doDeal()
16
  end
17
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.

1
2
function doDeal()
3
  if(isNewGame == true) then
4
    isNewGame = false
5
    dealInitialHand()
6
  else
7
    dealNewCards()
8
  end
9
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.

1
2
function dealInitialHand()
3
  disableBetButtons()
4
  enableHoldButtons()
5
  for i=1, 5 do
6
    getCard(i)
7
  end
8
  local gameData = saver.loadValue("gameData")
9
  local currentCredits = gameData.numberOfCredits
10
  local newCredits = currentCredits - betAmount
11
  creditText.text = newCredits
12
  gameData.numberOfCredits = newCredits
13
  saver.saveValue("gameData",gameData)
14
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).

1
2
function dealNewCards()
3
  disableDealButton()
4
  disableHoldButtons()
5
  for i = 1, 5 do
6
    if (playerHand[i].isHolding == false) then
7
      local tempCard = playerHand[i]
8
      tempCard:removeSelf()
9
      tempCard = nil
10
      getCard(i)
11
    end
12
  end
13
  resetCardsYPosition()
14
  getHand()
15
end
Whenever you remove something from the screen, you should always set it to 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.

1
2
function getHand()
3
  table.sort(playerHand, function(a,b) return a.cardNumber < b.cardNumber end)
4
  local frequencies = {}
5
  for i=1, 13 do
6
    table.insert(frequencies,0)
7
  end
8
9
  for i=1,#playerHand do
10
    frequencies[playerHand[i].cardNumber] = frequencies[playerHand[i].cardNumber]  + 1
11
  end
12
13
  local numberOfPairs = 0
14
  local hasThreeOfAKind = false
15
  local hasFourOfAKind = false
16
  local winningHand = "Nothing"
17
  local cashAward = 0
18
  local isStraight = true
19
  local isRoyalStraight = false
20
  local isFlush = true
21
22
  for i=0, #frequencies do
23
    if (frequencies[i] == 2) then
24
      numberOfPairs = numberOfPairs+1
25
    end
26
27
    if (frequencies[i] == 3) then
28
      hasThreeOfAKind = true
29
    end
30
31
    if (frequencies[i] == 4) then
32
      hasFour = true
33
    end
34
  end
35
36
  if (numberOfPairs > 0) then
37
    if(numberOfPairs == 1)then
38
      winningHand = "1 pair"
39
      cashAward = 1 * betAmount
40
    else
41
      winningHand = "2 pair"
42
      cashAward =  2 * betAmount
43
    end
44
  end
45
46
  if (hasThreeOfAKind) then
47
    winningHand = "3 of A Kind"
48
    cashAward = 3 * betAmount
49
  end
50
51
  if (hasFour) then
52
    winningHand = "Four of A Kind"
53
    cashAward = 7 * betAmount
54
  end
55
56
  if (numberOfPairs == 1 and hasThreeOfAKind) then
57
    winningHand = "Full House"
58
    cashAward = 6 * betAmount
59
  end
60
61
  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
62
    isRoyalStraight = true
63
  end
64
65
  for i=1, 4 do
66
    if (playerHand[i].cardNumber+1 ~= playerHand[i+1].cardNumber) then
67
      isStraight = false
68
      break
69
    end
70
  end
71
72
  for i=1, 5 do
73
    if(playerHand[i].cardSuit ~= playerHand[1].cardSuit) then
74
      isFlush = false
75
      break
76
    end
77
  end
78
79
  if (isFlush) then
80
    winningHand = "Flush"
81
    cashAward = 5 * betAmount
82
  end
83
84
  if (isStraight) then
85
    winningHand = "Straight"
86
    cashAward = 4 * betAmount
87
  end
88
89
  if (isRoyalStraight)then
90
    winningHand = "Straight"
91
    cashAward = 4 * betAmount
92
  end
93
94
  if (isFlush and isStraight) then
95
    winningHand = "Straight Flush"
96
    cashAward = 8 * betAmount
97
  end
98
99
  if (isFlush and isRoyalStraight) then
100
    winningHand = "Royal Flush"
101
    cashAward = 9 * betAmount
102
  end
103
104
  awardWinnings(winningHand, cashAward)
105
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.

1
2
function awardWinnings(theHand, theAward)
3
  instructionsText.text  = "You got "..theHand
4
  winText.text = theAward
5
  local gameData = saver.loadValue("gameData")
6
  local currentCredits = gameData.numberOfCredits
7
  local currentGames = gameData.numberOfGames
8
  local newCredits = currentCredits + theAward
9
  local newGames = currentGames + 1
10
  gameData.numberOfCredits = newCredits
11
  gameData.numberOfGames = newGames
12
  saver.saveValue("gameData",gameData)
13
  timer.performWithDelay( 3000, newGame,1 )
14
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.

1
2
function newGame()
3
  for i=1,#playerHand do
4
    playerHand[i]:removeSelf()
5
    playerHand[i] = nil
6
  end
7
  playerHand = {}
8
  deck = {}
9
  betAmount = 0
10
  isNewGame = true
11
  createDeck()
12
  enableBetButtons()
13
  instructionsText.text = "Place your Bet or Bet Max ($15)"
14
  winText.text = ""
15
  betText.text = ""
16
  local gameData = saver.loadValue("gameData")
17
18
  if (gameData.numberOfCredits == 0)then
19
    gameData.numberOfCredits = 100
20
    saver.saveValue("gameData",gameData)
21
  end
22
23
  creditText.text = gameData.numberOfCredits
24
  gamesText.text = gameData.numberOfGames
25
end

21. Testing the Game

Update the setup function by invoking the createDeck function as shown below and test the final result.

1
2
function setup()
3
  math.randomseed(os.time())
4
  createDeck();
5
  setupButtons()
6
  setupTextFields()
7
  createDataFile()
8
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.