There are a few small bugs in Battlejack, and a few opportunities for a better user experience. This chapter fixes the bugs and adds some features to make the game flow better.
The first bug you may not have noticed yet: if you click the mouse on the menu screen, the game crashes. This is caused by the main.lua file forwarding any detected mouse press or release to STATE.mousepressed() or STATE.mousereleased() , and finding no corresponding menu.mousepressed() or menu.mousereleased() function .
Another noticeable bug is the lack of feedback from the game. The progress of the game is very difficult to follow without a running tally of each player’s score, and without the declaration of who has won and who has lost.
To print the current totals of each hand, you must calculate a running total that updates as frequently as each hand changes. The two functions that update most frequently are the draw() and update(dt) functions , and there’s not necessarily any reason to use one over the other. However, since the draw() function is busy drawing cards and hands, put the calculation in the update function.
Notice that the values of handval and hordeval are each reset at the beginning of each update. This ensures that the total score is recalculated with every update rather than compounded upon itself. The total for each hand is the sum of each card in each hand, using the tonumber Lua method to translate the value of each card from a string into an integer.
Drawing the tally on screen is done the same way as drawing any text on screen, and for added effect you can add a graphical element to suggest a magical glow. Sample graphics are included in the code files for this book.
Try playing the game now to test the new functions, taking note of bugs or missing features.
Git Commit
A lot of progress has been made up to this point, so it makes sense to commit the changes to Git, just in case changes you make later render the game unplayable by mistake.
Open git-cola and look in the Status pane to see which files you have changed but not yet committed. A Git commit is like a snapshot, so even though you have committed an earlier version of a file, you must take a new snapshot of any changes since the original commit.
In git-cola, right-click the updated files in the Status pane and select “Stage selected”.
In the future, you should commit code whenever you make a significant change that has not broken your codebase. It’s a good habit to get into, and in case of disaster, it can save you hours of work.
Leveling Up
That balances the game, and catching it early prevents you from having to write the code associated with the AI playing a Joker card. If you decide to add it back later because you want the extra challenge, you can use the game.blast function when the player draws a Joker card, and you can write the necessary code.
Balancing a game isn’t just crippling the opposition, but also bolstering the player. The human player in Battlejack is up against a relentless march of cards that are drawn but never spent. Tipping the scales in the player’s favor has two important effects: it makes the player feel more powerful as they achieve victories, and it assures the player that all their hard work is paying off.
The level can be printed on screen using the usual love.graphics.printf function, which you’ve already used in both Blackjack and Battlejack. However, the player doesn’t really need a constant reminder of what level they’re on, so it’s a good idea to create some marker variable to signal the level text to disappear. You could use the Lua os.clock function to measure the passage of time, but in a turn-based game like Battlejack, time is more or less relative compared to when the player clicks, so it’s more meaningful to use clicks as a measure of “time” than actual time.
Each mouse release, check to see whether a winner has been declared and either increment the level or start a new round. This check must be performed before anything else so that as soon as there’s a valid winner, game play stops.
This code refers to a new function that doesn’t exist yet: game.sleep. This function causes the game to “sleep” to ensure that the player isn’t missing important information. Without a brief pause, the game would go from “You win” to “Level 1” messages in the blink of an eye.
You can play the game now to see the latest improvements. Remember to commit your changes with Git, as long as everything works as expected.
Powerup
The game increases the player’s levels but so far doesn’t actually reward them with anything substantial. Now that the level mechanism is in place, you can use it to add new cards into the player’s deck any time they gain a level. This gives the player the feeling of growing power, and a sense of accomplishment.
First, you need some way to alert the player of their accomplishment. You could print a message on the game screen, but for something as significant as an hard-earned powerup, a special message screen seems more important and means less clutter in the game play area.
On the message screen, you can display the new cards added to the deck after each victory. It can do double duty as an alert message when the player draws a black card from their own deck, underscoring that their luck has changed for the worst.
You need a few new tables and variables for this mechanism. Since there are sets of cards involved, you need new tables, earn for earned bonuses and up for powerups. Since the winner variable is set back to nil with each new game, you need a new persistent variable called lastwon to represent whether the player won the last round or not.
You also need to create a new file called msg.lua to serve as your message screen.
This introduces two new variables: an earncard and an upcard. These variables don’t exist yet, but you will create them before calling the new msg screen. They will contain the card or cards that have been generated from either a victory or an unfortunate hand.
You also need a method for inserting new cards into the player’s deck. You have already created a method to insert a Joker card into a deck, and while it seemed like a sensible function at the time, you can see now that it could be expanded into a generic method for inserting any card type into a deck. An additional argument is required so that the function can differentiate between a normal card and a bonus or powerup card, and the function name should change to better reflect its new generic purpose.
And then add the bonus and powerup cards to the deck. These cards are only added after level 0 has been won, and the player is only alerted to new additions that have been won from the previous victory. Once the cards have been earned, they are silently added to the deck. This means that a message screen is only shown when lastwon is set to 1. It also means that which cards are added can be controlled according to the current value of the level variable. That is, starting at level 1, all cards in the earn and up tables, from 1 to 1, are added. At level 2, all cards from 1 to 2 are added. At level 3, all cards from 1 to 3 are added, and so on. When you have no further cards to add, the limit is capped to the highest number of cards defined in the earn and up sets of deck.ini.
The other purpose for the message screen is to alert the player when a traitor has been discovered among their ranks, or in mechanical terms, they have drawn a black card from their own red deck.
This requires an if statement in the game.mousereleased function , when a new card is drawn from the player deck. When a card is generated, you assign it to the card variable, and since card generation uses your own card.lua code, you know that you can look at its color by looking at card.color. If the color is black, then you can trigger a few actions. First, a message should be given to the player, telling them of the bad news. Then, the card must be placed into the AI hand and removed from its default destination of the player’s hand.
Try playing a few rounds to see how the new bonus and powerup cards perform.
Powerup Double Draw
Now that the game is more complex and more complete, play testing is a little difficult. You have to play longer, and press more keys or click more things to find the bugs. Don’t let that deter you, though; there are bugs to fix yet.
One notably missing feature is the way powerup cards are treated. First of all, they’re counting toward the total of the player’s hand, but they’re meant to be powerups, not just another card. Contrariwise, the powerup cards count as one card draw, meaning that in a sense they penalize the player because the game’s most urgent mechanic is the need for more cards. If a “powerup” card takes up a turn but adds nothing to the player’s points stash, then the player is arguably better off without the powerup.
The way to make a powerup feel like a powerup is to give the player a second card any time a powerup card is drawn. That way, a powerup card adds temporary ammunition to the player’s hand but doesn’t cost them any points toward their score.
The logic here is fairly simple, but for the one exception when a player draws a bonus card and then a black card. The code first checks for a bonus card. If the card is a bonus card, then a new card is immediately drawn. If that card is a black card, then that card is silently moved into the AI’s hand and removed from the player’s hand. This is done without a message to avoid too many alerts. You can change this, if you prefer verbosity, but during play testing pay close attention to how often the game is interrupted so that you don’t annoy your players.
Font and UI Consistency
There’s another bug hidden in the code that you might not have discovered yet. Because the game uses two different fonts and font sizes, going back to the menu screen after a game has started results in unreadable menu options. The fix for this is to monitor font settings closely. Specifically, you can enforce the “default” font for each game state in the activate functions.
This resolves any inconsistencies in the user interface.
Garbage Collection
The final bug to squash is a rather serious crash that you only see after several rounds. The crash renders an out of memory error, which is caused by the vast amounts of data being moved in and out of this game. This is a niche problem caused by this type of game; usually, Lua is fully capable of managing memory, but with all the cards and graphics that Battlejack cycles through, it’s difficult for Lua to know what information we expect to have access to.
Part of memory management is called garbage collection. Most modern programming languages have it built in, although some low-level languages like C and C++ do not. Although built upon C, Lua has automated garbage collection but allows for manual memory management when needed.
Garbage collection, as its name suggests, is a signal you can send to Lua to assure it that it’s safe to cycle through old variables and clear them from memory. There are a few places that you can expect this to be safe: at the beginning of a new game, there’s certainly no reason to keep information from previous rounds, and when the player uses the Joker card, which only happens once per round, it can be safely assumed that very old data is no longer required. This is mostly guess work, of course; you have no way of monitoring exactly what Lua is keeping track of in the recesses of its memory allotment, but the Lua collectgarbage() function is a trigger for Lua to run a garbage collection cycle sooner than its default schedule. You’re still leaving it up to Lua to decide what to remove from memory, and Lua is smart enough to know that, for instance, the lastwon variable is still important and must not be erased, while the structure of the previous decks are safe to discard.
For most Lua programs, you won’t run into memory management problems. However, if you do, you now know how to prompt Lua to review its resources and clear out unused data.
Homework
Before moving on, commit your changes to Git.
Play a few rounds of Battlejack to get a feel for difficulty. Is it challenging enough? Is it too difficult? What adjustments can you make?
You can leverage your new level system to adjust difficulty. For instance, you might want to start the game with fewer traitor cards in the red deck, and then ramp up the number as the player progresses.
if level >= 4 and level < 6 thengame.mole(ai,deck,4)elseif level >= 6game.mole(ai,deck,6)elsegame.mole(ai,deck,2)end
Or you might want to do the opposite, such that the game appears to become easier as the player progresses. This is an alternate theory of game design, in which to give the player the illusion of increasing power, you “nerf” the enemies.
if level < 2 thengame.mole(ai,deck,6)elsegame.mole(ai,deck,4)end
Try the game again to see how it progresses.
Think of some other ways to help the balance of power in Battlejack and try them out.
It’s not easy, but it is possible to exhaust a full deck with no winner or loser. There’s no code to handle this event, so the game crashes.
Invent a reliable way for the game to respond to empty draw decks. There are several ways to do this. You could declare a winner based on the state of the game when the decks are exhausted (closest to 21 wins). You could hold a final death-match to decide the winner, in which the player and AI each draw a card from a fresh deck; the best card wins. Or you could just create fresh decks and continue the game seamlessly.