© Seth Kenlon 2019
Seth KenlonDeveloping Games on the Raspberry Pihttps://doi.org/10.1007/978-1-4842-4170-7_8

8. Battling It Out

Seth Kenlon1 
(1)
Wellington, New Zealand
 

As of the previous chapter, both the menu and game screens are accessible when your program launches. In this chapter, you code the main game mechanics. The logic is similar to what you have already done to create your Blackjack game, so elements of this chapter will feel familiar, although there are unique features to Battlejack that pose new challenges.

The central game mechanic is this: like Blackjack, the goal is to reach 21 first. Unlike Blackjack, an excess of 21 is not a bust but still wins, and the player can “attack” the dealer’s stash of cards.

During the game, the player clicks their deck to draw a card. On their turn, the player may select cards to cancel out a card in the AI’s stash. This is an attack, and an attack may be bolstered by a powerup card if the player has one available. In the meantime, a player’s cards are tallied for their own score in an attempt to reach 21 before the AI does. If the player draws a black card, then at the start of the dealer’s turn, one black card from the player’s hand is added to the dealer’s stash.

If a joker is drawn from either deck, then it destroys the stash of the opposition.

In code, this game requires a lot of data tracking. Your code must keep track of what cards remain in each deck, what cards are in the player’s hands, which of those cards are currently selected as cards to go into battle against the dealer, which cards are in the dealer’s hand, which of the dealer’s cards have been eliminated, the location of each card on the screen, and much more. In Lua terminology, that means you need tables (“classes” or “fields” in other languages).

Card Table

In your Blackjack game, each card was its own Lua table, with a unique self.suit, self.value, and self.img all its own, which you were able to call from your main script when you needed to calculate the total score or when you needed to tell LÖVE which image to draw on the screen.

Create a card.lua file in your project directory. This file is the card table: any time a card is generated for display on screen, it inherits all the attributes contained in this file. As you might expect, each card requires a face image and a numeric value and also a color. Each card also needs to keep track of its own location on the screen, which is useful in games because by updating one item’s attributes, you can always restore or redraw the game state after major events.
Card = { }
function Card.init(c,v,f,x,y)
   -- generate card
   local self = setmetatable({}, Card)
   self.color = c
   self.value = v
   self.face  = f
   self.x = x
   self.y = y
   self.r = 0 --rotation
   return self
end

This is a slightly more complex version of your Blackjack card table. Of course, none of these values mean anything yet. They’re just empty fields that need to be populated by data you feed into the table when generating a new card.

There are still a few more fields needed. First of all, each card needs an image. This would be identical to your Blackjack game were in not for the addition of the Joker to this deck. In fact, you could render the Joker and back images basically the same as all the other cards by naming their image files as you named the face cards (0-joker.png, 0-back.png) but then you need a copy of each in the red and black directories, which is redundant data. There’s nothing wrong, necessarily, with that approach, but it wouldn’t scale were this a much larger game with really big assets, while allowing for deviations in the code is cheap, and worthwhile.

Add this block of code above the return self line of your card.lua file:
   if self.face == "back" or self.face == "joker" then
      self.img = love.graphics.newImage("img" .. d .. self.face .. ".png")
   else
      self.img = love.graphics.newImage("img" .. d .. self.color .. d .. self.value .. "-" .. self.face .. ".png")
   end

And finally, it’s always important to be able to ascertain an object’s size. A game object’s size is vital for collision detection: it’s how you tell where to draw the next object without drawing on top of the previous one, and it’s how you detect whether the player has clicked one object or another.

Lua has functions specifically for the task of determining image size, but your program is going to scale your graphics down (as in the Blackjack game) so that the screen size can be changed. Technically, you could use Lua’s size detection and multiply the result by your scale. Or, you can enter the size value as part of each card. Both do the same thing, but since the size of objects is used frequently, it’s “cheaper” to do the calculation once and enter the value as a variable than to do the math every time a user interacts with the screen.

Add this above the return self line of your card.lua file:
   self.wide = self.img:getWidth()*scale
   self.high = self.img:getHeight()*scale

The value of the scale variable has yet to be created, but as long as it is defined before card.lua is actually used, there won’t be a problem.

Game State

Currently, entering the game state of your application sets the active STATE to game, which in turn causes the main loop to invoke your custom game.draw() function, which renders a colored background. This all happens in the game.lua file, so open that in Geany so you can make some changes.

The game.lua file is the logical place to define important game-related variables, because it’s within this file that the mechanics and game data tracking happens.

To begin with, the game logic needs access to your card class and to the configuration file that defines what cards exist. Add the top two lines of the following sample code to the top of your game.lua file.
require("card")
local inifile = require('inifile')
game = {}
If you have not already done so, use luarocks to install the inifile Lua library into your project directory.
$ cd ~/battlejack
$ luarocks install --tree=local inifile

Based on both the card.lua file and your previous experience with your Blackjack game, you can anticipate a few important variables that can be defined whether or not your code has a need for them yet.

Start by defining some environment variables. Eventually, LÖVE needs to know who is running the game so that it can store save files. Lua provides the os.getenv('HOME') function to discover a user’s home directory. In Linux, this is a folder in a directory literally called home, whereas on the Mac, it is a folder within the Users directory, and on Windows it is a location usually on the C drive. The point is, you don’t have to worry about where a user keeps their personal data files, because Lua finds that out for you.

Another thing that changes from system to system is the separator character used to delimit directories from one another. For instance, on the Raspberry Pi, as on any Linux or Mac system, the separator character is a forward slash. The location to any file or folder on the system can be predictably written out in plain text; for example, /home/pi/battlejack/img/joker.png. On Windows, however, the separator character is traditionally a backslash: C:\\My Documents\example\img\joker.png. Lua can detect this for you, too, with its package.config function.

Add the following lines to your game.lua file. The first line is already in your file and is shown for context.
game = {}
home = os.getenv('HOME')
d    = package.config:sub(1,1) -- path separator

Next, your game needs several tables to serve as decks of cards in various states of play. Create tables called hand and horde to serve as the player and AI active hands, tables called deck and ai to serve as shuffled draw decks for player and AI, a table called back containing the backside of the player and AI decks to serve as the clickable item when a player wants to draw a new card, and a grab table to serve as a staging area for cards a player is about to send into battle.

Finally, create a winner variable to mark whether or not a winner has been found. By default, set it to nil. Pull card data from your configuration file into a variable called set using the inifile function.

Here’s the code you need to add. The first two lines are for context.
home = os.getenv('HOME')
d    = package.config:sub(1,1)
hand  = {} --player hand
horde = {} --ai hand
deck  = {} --player deck
ai    = {} --ai deck
back  = {} --clickable deck icons
grab  = {} --selected for battle
winner = nil
-- parse the INI file and
-- put values into a table called set
set = inifile.parse('deck.ini')
Your game world needs a scale variable to use when scaling down large card images to something that fits on the game screen. Since checking for the appropriate scale is something that has to be done any time a player changes the screen mode between windowed and fullscreen, setting the scale needs to be a function that can be called as necessary. Create a new function called game.scaler() in game.lua and use math to determine the optimal scale factor given the current size of the screen.
function game.scaler(WIDE,card)
   slot = WIDE/6
   scale = slot/card
   pad = WIDE*0.04
   return scale
end

The first logical place to call this function is any time a new game begins. The function takes the width of the screen, which is a global variable established in main.lua, and the native size of a card graphic. If you’ve made your own graphics for this game, then you must determine the size of your graphic yourself (you can find it in GIMP, if you’re not sure). The example graphics included with the source code of this book are 790 pixels wide.

The game.scaler() function assumes that approximately six cards should fit horizontally across the screen (WIDE/6). This calculation renders the width of each card in pixels, which itself is important enough to keep in a global variable called slot, which keeps cards from overlapping when being drawn. The slot value divided by the unscaled size of a card provides a decimal number by which all cards may be scaled to fit six across the screen.

Add this line of code to your game.new() function:
scale = game.scaler(WIDE,790)

With these functions and variables in place, you can at least draw the most rudimentary of cards screen.

Setting the card table is an activity that needs to happen at the beginning of each new round, so it deserves its own function. Create one now, called game.setup(), and use your card library to generate cards to represent the player deck and the AI deck.
function game.setup()
   -- create GUI deck for player
   card = Card.init("c","v","back",pad,HIGH-slot-(pad*2))
   back[#back+1] = card
   -- create GUI deck for ai
   card = Card.init("c","v","back",WIDE-(slot/2)-pad,slot-(pad))
   back[#back+1] = card
   -- draw table background
   ground = love.graphics.newQuad(0,0,WIDE,HIGH,150,150)
   tile   = love.graphics.newImage('img' .. d .. 'tile.jpg')
   tile:setWrap('repeat','repeat')
end

The first card you generate is the “top” of the draw deck for the player. Since it’s the back of a card, it has no color or value, so you pass in the "c" and "v" dummy values, which the card library ignores once it sees that the card being generated is of the back type. The card’s X position is set at the left plus the value of pad, a variable set in main.lua to provide padding around the edges of the screen. The card’s Y position is calculated from the height of the screen.

The card generated is added to the back table. Then the same variable is used to generate a second card to represent the AI’s draw deck, using new X and Y values so that the card’s placement is in the top right (across the virtual table) instead of the bottom left. This second card is also added to the back table.

Finally, a proper tabletop is defined. Currently, the game mode only renders a flat color in the background, but Battlejack deserves something more exciting. Rendering an array based on a texture or pattern is pretty common for a game engine, and LÖVE provides the love.graphics.newQuad function to map tiles across a given space. Specifically, this code defines a quad of the width and height of the screen, with tiles sized 150×150 pixels (which happens to be the size of the tile pattern included with the source code of this book). The tile is defined using the d variable to ensure compatibility with whatever system the game is running on, and the tile mode is set to wrap seamlessly across all available space.

Currently, nothing calls the game.setup() function, so trigger it at the end of the game.new() function, since a user starting a new game certainly expects their game to be set up.
   game.setup()

Of course, nothing is actually drawn unless it appears in the love.draw() function, which in this program is aliased to the draw() function of whatever STATE the user is in. For game play, the user is in the game state, so you must add your draw commands to game.draw().

Clear out the code currently in the game.draw() function, replacing it with this:
function game.draw()
   love.graphics.setColor(1,1,1)
   -- set background
   love.graphics.draw(tile,ground,0,0)
   --hand player
   card = back[1]
   love.graphics.draw(card.img,card.x,card.y,0,scale,scale,0,0)
   --horde ai
   card = back[2]
   love.graphics.draw(card.img,card.x,card.y,0,-1*scale,-1*scale,card.img:getWidth()/2, card.img:getHeight()/2)
end

After setting the color to white to ensure that everything is drawn at 100% opacity, the tiles are drawn to create the tabletop. Order is important here, so the tabletop must be drawn before cards are drawn, or else the cards will be drawn “under” the table.

To draw the two card decks, you populate a temporary card variable with one object from the back table and use that data to draw the graphic. Since the back table contains relatively little data, manually pulling out an entry is simple (the contents of the other tables that you have created are far more dynamic and require for loops).

Launch your game to verify that all of your code is correct so far. Correct any errors and make adjustments as needed, and then it’s time to set up the deck creation functions.

Deck Building

Like any card game, a key element of Battlejack is the random nature of the game elements. Functionally, that means your program needs a reliable set of methods that you can use repeatedly to create freshly randomized decks of cards based on the card definitions you have set forth in your card definition INI file.

To create decks for Battlejack, you must fill two distinct tables with card definitions: one filled with red cards and the other with black cards. However, it’s part of the game design that there are six black “mole” cards inserted into the player deck, so six cards must be stolen from one deck and inserted into the other. Additionally, a Joker must be placed in each deck but not accidentally stolen when the black “mole” cards are inserted into the player deck. And finally, each deck must be shuffled to ensure unpredictability.

In your game.lua file, create four new functions: game.setsplit(), game.mole(), game.joker(), and game.shuffle().

First, to create two distinct tables containing card definitions for each color, you need to know which part of the INI file to use. For lack of better terminology, call this a stack (as in a stack of cards). You also need to know which deck you are building: red or black. Essentially, you need to determine whether the deck is meant for a human player or for the computer, so call this attribute human. Finally, you need a table to build the deck into, and you need to know the number of stacks to put into each deck, because it would be a very quick game were there only one copy of each card.
function game.setsplit(stack,human,tbl,n)
   for count = 1, n do
      for i,card in pairs(set[stack]) do
     if human == 1 then
        color="red"
     else
        color="black"
     end
     tbl[#tbl+1]=color .. "," .. card
      end
   end
   return tbl
end

This code uses a for loop to repeat the same action for as many times (n) as you proscribe when calling the function. The action that it takes is another for loop that iterates over the set variable, which contains the contents of the card section of your INI file, assigns a color to the card definition, and then places the card definition into whatever table you have told it to build into.

The table is returned at the end of the function, so you call the function as a constructor method, with its results placed into a destination of your choosing. For now, call the function in the game.setup() function .
-- create sets
deck = game.setsplit("card",1,deck,2)
ai   = game.setsplit("card",0,ai,2)

Next, you need to steal six cards from the black deck. If you do that immediately after building the decks, however, the same black cards are stolen every time because no randomness has been introduced into the decks. For this reason, you need to develop the shuffle method first.

As you might expect, asking a computer to do something randomly requires the use of Lua’s math.random() function . This needs a random seed, and the easiest ever-changing source of numbers in a computer is its clock. Activate a random seed at the top of your game.new() function.
math.randomseed(os.time())
To cause a table to “shuffle” its order, you take the table into a function, determine the number of items it has in it, and then take a random number between 1 and the number of items in the table. Take the current item and swap it with the random numbered item. Repeat this until each item has traded places with some other item.
function game.shuffle(tbl)
   local len = #tbl
   for i = len, 1, -1 do
      local j = math.random( 1, i );
      tbl[i], tbl[j] = tbl[j], tbl[i];
   end
   return tbl;
end
Now that you have a way to shuffle decks, it’s safe to steal cards from one to sabotage the other. For the game.mole() function , you first shuffle the AI deck to ensure that you’re grabbing random cards. Then you take the first number of cards (the rules say six, but in case the function is used for some other purpose later, use n to signify a configurable number) and insert those cards into your target deck. Once inserted into the new deck, remove the stolen entry from the source table.
function game.mole(src,tgt,n)
   -- shuffle
   src = game.shuffle(src)
   for count = 1, n do
      tgt[#tgt+1] = src[count]
      table.remove(src,count)
   end
end
The last function inserts a Joker card into a deck. It’s a straightforward table append.
function game.joker(tbl,human)
   if human == 1 then
      color="red"
   else
      color="black"
   end
   tbl[#tbl+1] = color .. ",joker,0"
   return tbl;
end
Now that the functions to build decks exist, you must use them. It’s reasonable to first call these functions in the game.setup() function, since that is presumably called any time that a new game is started. Add this code to your setup:
-- create sets
deck = game.setsplit("card",1,deck,2)
ai = game.setsplit("card",0,ai,2)
-- steal cards from black
game.mole(ai,deck,6)
-- insert joker
deck = game.joker(deck,1)
ai   = game.joker(ai,0)
-- shuffle
deck = game.shuffle(deck)
ai = game.shuffle(ai)
If you launch your game now, it runs successfully but very quietly. For temporary insight into the inner workings of the game, add this block of code to the end of the setup() function :
   print("deck -----------------")
   for i,card in pairs(deck) do
      print(card)
   end
   print("ai -----------------")
   for i,card in pairs(ai) do
      print(card)
   end

Launch the game and look at the terminal to see a text list of the cards in each deck.

The next step is to transform all of this setup work into a playable game.

Playable Cards

Playing Battlejack is a three-step process: the user must draw a card, the user must select cards to use in an attack, and then the user must choose a target for an attack. That means your code needs functions to visually produce a card into the player’s hand, to mark cards as selected, and finally, to resolve a battle.

Create a new function called game.cardgen() that accepts a deck as an argument. This function’s job is to parse the next available card definition from a deck to use your card library to create a card object with all the necessary attributes (image, width, height, position, and so on), and to add it to the table representing the player’s hand or the AI horde.

As cards are drawn from a deck, they must be removed from that deck or else the same card would be drawn for eternity. For that reason, the deck table becomes populated with nil entries as the game progresses, so your function must be programmed to skip over empty entries.

When a valid entry is found, the text must be parsed. Card data (as you were able to see when you wrote the temporary introspection for your last functions) is separated by commas; for example, black,goblin,5 or red,arcanist,3. To extract information from this, you use the Lua match function, which permits you to provide a regular expression representing the pattern of text you expect to see in each card entry of a deck. Specifically, you tell LÖVE to expect any text followed by a comma, and then any text followed by another comma, and finally, any text. Each component found by match is placed into a unique variable, which is passed to your card library.
function game.cardgen(src)
   local count = 0
   while src[count] == nil do count = count+1 end
   local c,f,v = src[count]:match("([^,]+),([^,]+),([^,]+)")
   card = Card.init(c,v,f,nil,nil)
   src[count] = nil
   if src == deck then
      hand[#hand+1] = card
      card.y = HIGH-(pad*2)-slot
   else
      horde[#horde+1] = card
      card.y = pad/4
   end
   return card
end
Now you need something to trigger your new function. Clicking the back of the player’s deck should produce one card for the player, and one card for the AI. Unlike in your Blackjack game, there are several clickable objects in this game, so your mousereleased function needs to be able to detect exactly what clicked. This is best done with a dedicated click detection function that analyzes the X and Y coordinates of a click, compares it with the dimensions of some given object, and determines whether or not the X and Y of the click falls within the boundaries of the object.
function game.clicker(x,y,tgt)
   return (
        x < tgt.x + tgt.wide and
        x > tgt.x and
        y < tgt.y + tgt.high and
        y > tgt.y
    )
    -- returns True or False
end

Notice that this function is a little different than any of the functions you’ve written so far; it returns either true or false, depending on the results of its calculation. This is a convenience that lets you use the result of a call to the function as a sort of switch; if it returns false, then you know that there’s no reason to continue analyzing a click.

Now when the player releases mouse button 1, look at both cards in the back table . Check whether either deck was clicked, but restrict the check to the lower half of the screen (rendering clicks in the AI deck meaningless). If so, generate one card for the player and one card for the AI.
function game.mousereleased(x,y,btn)
   if btn == 1 then
      --take a card
      for i,obj in pairs(back) do
     if game.clicker(x,y,obj) and y > HIGH-slot-pad then
        card = game.cardgen(deck)
        card = game.cardgen(ai)
     end --if
      end --for
   end --if
end
As usual, nothing is actually drawn to screen unless it’s accounted for in the draw() function; so add two new for loops (one for each hand) to your draw loop. The first two lines are for context.
 -- set background
love.graphics.draw(tile,ground,0,0)
-- draw cards
for i,obj in pairs(horde) do --ai
   obj.x = WIDE-(slot*i)-slot-pad
   love.graphics.draw(obj.img,obj.x,obj.y,0,scale,scale,0,0)
end
for i,obj in pairs(hand) do --player
   obj.x = pad+(slot*i)
   love.graphics.draw(obj.img,obj.x,obj.y,obj.r,scale,scale,0,0)
end

Launch the game and draw some cards.

Battle

Sending cards into battle is also all about click detection. Since the user can select more than one card to combat an enemy card, you must mark selected cards as selected until an enemy target is clicked. You already have a table called grab, and so it serves as a kind of extension of the player’s hand, containing any cards that have been clicked in preparation for battle. Of course, clicking an already grabbed card causes it to be deselected.

To detect whether a card is selected or not, you need a function to check for the presence of a specific card (the one that a player has clicked) in a table (the grab table). Create a function called game.isselected() and use it to cycle through a table in search of a specific card.
function game.isselected(src,tgt)
   for k,v in pairs(tgt) do
      if v==src then
     return k
      end
   end
end

Now that you have the ability to detect whether a card has been grabbed yet, you can process mouse clicks. For the action of grabbing a card and adding it to the grab table, you can use a the LÖVE mousepressed function, simply to avoid overloading the mousereleased function with too many checks.

The logic is simple. If mouse button 1 is pressed, check to see whether the object clicked is in the player’s hand table. If it is, but it is not in the grab table, then change its position slightly to show that it is selected, and add it to the grab table. If it’s already in the grab table, then move it back in line with the other cards and remove it from the table.
function love.mousepressed(x,y,btn)
   if btn == 1 then
      for i,obj in pairs(hand) do
     if game.clicker(x,y,obj) and not game.isselected(obj,grab) then
        obj.y = obj.y - (slot*2*scale)
        grab[#grab+1] = obj
     elseif game.clicker(x,y,obj) and game.isselected(obj,grab) > 0 then
        obj.y = HIGH-(pad*2)-slot
        k = game.isselected(obj,grab)
        grab[k] = nil
     end
      end
   end
end

Launch the game and try selecting some cards from your hand.

Visual Effects

Selecting cards for battle should be an exciting prospect in the game. After all, it’s the central mechanic; without this, the game is basically Blackjack. While elevating the card on the table is pragmatically effective, it’s not very flashy.

One way to “sweeten” the act of selecting cards for battle is to add a simple visual effect. You can imagine, for instance, that in a fantasy battle, warriors chosen to go out onto the front line might glow with a magical aura. In video game design terms, that translates to a particle effect.

Particle effects are relatively expensive, so you don’t want to over-use them, especially on a relatively weak platform like the Pi. But as an indicator that something is “hot” and ready for battle, it’s justified.

To set up a particle effect, you must point LÖVE to a graphic that is to be used as the actual particles. This particle serves as the raw material for the effect, and there are a few important attributes to set to keep the effect from devouring your processor and spreading particles all over the screen.

Add the following particle setup code to the top matter of your game.lua file. The first line is for context.
set = inifile.parse('deck.ini')
local mana = love.graphics.newImage('img' .. d .. 'part.png')
parti = love.graphics.newParticleSystem(mana, 12)
parti:setParticleLifetime(2,5) -- Particles live span min,max
parti:setEmissionRate(4)
parti:setSizeVariation(1)
parti:setLinearAcceleration(-12,-12,12,0) --xmin,ymin,xmax,ymax
parti:setColors(255,255,255,255,255,255,255,0) --Fade
To use the effect, add it to your draw function. Its placement is important, since you probably want it to be rendered under the player cards so that the particles appear to be rising up from within or behind the selected cards.
for i,obj in pairs(grab) do
   local count = 1
   while count < obj.wide/mana:getWidth() do
      love.graphics.draw(parti,obj.x+(mana:getWidth()*count+1),obj.y+(pad/3))
      count = count+1
   end
end
for i,obj in pairs(hand) do -- this line for context

Since the particle graphic itself is quite small, this code uses a while loop to place a particle seed along the top edge of any card in the grab table.

Finally, use the LÖVE update function to detect and update changes in the particles. It only needs to take action if the grab table is not empty.
function love.update(dt)
   if #grab > 0 then
      parti:update(dt)
   end
end
Launch your game again and select some cards for battle to see the effect Figure 8-1.
../images/467765_1_En_8_Chapter/467765_1_En_8_Fig1_HTML.jpg
Figure 8-1.

Particle effects used to highlight a selection

Resolving Conflict

To settle the outcome of a card battle, you must compare the player’s cards selected for battle against the AI card being targeted. This happens only if 1 or more cards is present in the grab table, and only when a card in the AI horde has been clicked.

There’s also one important exception to any attack: if the card is a Joker, then all cards in the horde are wiped out.

To obliterate an entire hand, you can use a new function. Call it game.blast() and make it clear out whatever table it is provided.
function game.blast(tgt)
   local count = #tgt
   for i=0, count do tgt[i]=nil end
end
This function is useful not only for a Joker attack, but also as a way to make sure a player is starting with an empty hand, horde, grab, and other tables, at the start of a new game. In fact, why not add summary blasts to the game.new() function now.
function game.new()
   game.blast(deck)
   game.blast(ai)
   game.blast(hand)
   game.blast(horde)
   game.blast(back)
   game.blast(grab)
   winner   = nil
   scale = game.scaler(WIDE,790)
   -- start new game
   STATE = game
   math.randomseed(os.time())
   game.setup()
end

If you launch your game and draw a few cards, and then press Esc to bring up the menu, and then start a new game, you find that a new game is now, finally, truly a new game.

Resolving battle is still incomplete. Aside from the blast function, it mostly happens within the mousereleased function.

Order is important now. Currently, your mousereleased function checks whether the mouse button is button 1 and then takes action. But the button is always button 1, so new qualifiers are required. For instance, if the mouse button is 1 and there are cards currently grabbed, then check to see if it was a card in the horde table that was clicked. If so, then it’s time to battle. If not, then check that the click was button 1 and that a card in the back table was clicked, and draw a card.

In addition to removing the attacked horde cards, the player’s resources require adjustment. The cards used in the attack are also removed from the player hand. However, this must only happen if the attack is successful, since a player could attempt to attack without enough power to actually remove cards from the field. You must decide through play testing whether a successful attack must be greater than a black card, or if a card that is equal to or greater than a black card is victorious. The sample code here allows for cards both equal in value and greater in value to defeat the opponent.

Create a new function called game.postbattle() to perform the menial task of removing cards from a table.
function game.postbattle(src,tgt)
   for i,card in ipairs(src) do --remove grabbed cards
      k = game.isselected(card,src)
      src[k] = nil
      k = game.isselected(card,tgt)
      table.remove(tgt,k)
   end
end
And then perform the checks and balances of battle in the mousereleased function.
function game.mousereleased(x,y,btn)
   local attack = 0
   if btn == 1 and #grab > 0 then
      for i,obj in pairs(horde) do     --examine each card in horde
     if game.clicker(x,y,obj) then --get horde card that got clicked
        for i,card in pairs(grab) do --check value of grabbed cards
           attack >= attack+tonumber(card.value) --add value to total attack
           if card.face == "joker" then
          game.blast(horde)
          game.postbattle(grab,hand)
           end --if
        end --for
        if attack > tonumber(obj.value) then
           -- remove from horde
           k = game.isselected(obj,horde)
           table.remove(horde,k)
           game.postbattle(grab,hand)
        end --if
     end
      end
   elseif btn == 1 then
      --take a card
      for i,obj in pairs(back) do
     if game.clicker(x,y,obj) and y > HIGH-slot-pad then
        card = game.cardgen(deck)
        card = game.cardgen(ai)
     end --if
      end --for
   end --elseif
end

Launch your game and take out the opposition. There’s no win or lose condition yet, and there may be a few crashes, but the basic mechanic and game play is complete. Your game is now firmly in alpha.