7
\$\begingroup\$

I have figured out how to pass functions around in Lua, and I am using the technique of inserting functions into tables in order to dynamically assign functions to the cards in a trading card game. The functions are assigned when the Card is first created, but more could easily be added later. I am still new to Lua so I would love to hear about any syntax errors I am making, or if the way I am doing anything might be dangerous in the future or violates Lua best practices.

The basic idea is that each Card has a unique ID, and each ID has a table of strings associated with it. The Card first gets this table of strings from the CardFunctionsList script, then passes them to the FunctionsList script which returns a table of the functions themselves. Then the card can run any function that has been loaded in this way, and could pass the function along to another card if need be. When called, the card passes itself into the function, and the function modifies it.

There are some problems with this approach. I do not like having to use tables full of strings in CardFunctionsList.lua, but I do not see a better way to do it. I originally just used numbers, but the readability was worse compared to this version. But if there are any typos the functions will not be loaded. Also, I think it might be better to have just one script instead of two to return the functions table, but it seems cleaner to have it separated this way. I am not sure about this. Finally, I am struggling with the names CardFunctionsList and FunctionsList, but I can't think of anything better that makes sense.

Card.lua

local Card = {}

function Card:new(cardSuit, cardType, cardId)

    local instance = setmetatable({}, { __index = self })

    instance.suit = cardSuit
    instance.id = cardId
    instance.type = cardType

    --these will be assigned by a script that is not yet complete
    instance.health = 0
    instance.strength = 0

    --these are the functions of the card
    instance.abilityList = {}
    instance:loadAbilityList(cardId)

    --this will contain things like "Taunt" or "Charge"
    instance.statusList = {}

    --diagnostic method for the prototype
    instance:doTestAbilitiesAndPrint()

    return instance
end
function Card:loadAbilityList(cardId)
    local cardFunctionsList = require "CardFunctionsList"
    local functionsList = require "FunctionsList"

    local functionNames = cardFunctionsList:functionNamesForId(cardId)
    self.abilityList = functionsList:new(functionNames)
end

--if the card does not have the ability, nothing will happen
function Card:doAbility(abilityName)
    for key,functionTable in ipairs(self.abilityList) do
        for name,func in pairs(functionTable) do
            if name == abilityName then
                amountToIncrease = 1
                func(self,amountToIncrease) --self because the card passes itself into the function
            end
        end
    end
end

function Card:doTestAbilitiesAndPrint()
    self:doAbility("addTaunt")
    self:doAbility("addCharge")
    for key,value in ipairs(self.statusList) do
        print(value)
    end
end

return Card

CardFunctionsList.lua

local CardFunctionsList = {}

--For each card ID, the names must be manually entered as a table
--they must match values in the FunctionsList script
function CardFunctionsList:functionNamesForId(id)
    if       id == 1 then return {"increaseHealth"}
      elseif id == 2 then return {"increaseStrength"}
      elseif id == 3 then return {"addTaunt"}
      elseif id == 4 then return {"increaseHealth", "addTaunt"}
      elseif id == 5 then return {"increaseStrength", "addTaunt", "addCharge"}
      elseif id == 6 then return {"increaseHealth", "increaseStrength", "addTaunt", "addCharge"}
      elseif id == 7 then return {"increaseHealth", "increaseStrength"}
      else return {} --if the id does not match anything, nothing bad will happen
    end
end

return CardFunctionsList

FunctionsList.lua

local FunctionsList = {}

--The inputted functionNames are a table created by the CardFunctionsList
function FunctionsList:new(functionNames)

    local instance = setmetatable({}, { __index = self })

    instance.functions = {}

    instance:loadFunctionsToReturn(functionNames)

    return instance.functions
end
function FunctionsList:loadFunctionsToReturn(functionNames)
    for key,name in ipairs(functionNames) do
        table.insert(self.functions, self:functionForName(name))
    end
end
function FunctionsList:functionForName(name)
    --even though the table key looks like plain text, it will accept a string to access it
    if       name == "increaseHealth" then return {increaseHealth = increaseHealth}
      elseif name == "increaseStrength" then return {increaseStrength = increaseStrength}
      elseif name == "addTaunt" then return {addTaunt = addTaunt}
      elseif name == "addCharge" then return {addCharge = addCharge}
    end
end

--Here is the list of all functions
function increaseHealth(card, amount)
    card.health = card.health + amount
end
function increaseStrength(card, amount)
    card.strength = card.strength + amount
end
function addTaunt(card)
    table.insert(card.statusList, "Taunt")
end
function addCharge(card)
    table.insert(card.statusList, "Charge")
end

return FunctionsList

From here in order to add new gameplay to the game, a card ID entry must be created in CardFunctionsList.lua that returns a table of strings. Then entries must be made for those strings in the else-if in FunctionsList.lua, and the actual functions added to that script.

One goal is for a person with possibly very little coding knowledge to be able to create new cards for the game by editing the scripts directly, so that is something that should be considered as far as the structure and readability of the code.

\$\endgroup\$
  • \$\begingroup\$ You can use tables instead of most of your elseif chains. I also think you're going a bit too heavy with the OOP -- it's not always needed, and can clutter your code/make things more complex. I can create some examples for you if you'd like. You should also be careful with globals in FunctionsList.lua. \$\endgroup\$ – Advert Aug 26 '14 at 17:37
  • \$\begingroup\$ You should definitely post the table version of the elseif chains as an answer! When you mean globals in FunctionsList.lua, do you mean it should be local card.strength = card.strength + amount? \$\endgroup\$ – bazola Aug 26 '14 at 17:40
  • \$\begingroup\$ Sorry, apparently the part where I pointed to the code got left out. I meant this part: function increaseHealth(card, amount) ... and the other ones. You could change them (or a mod could, since you want to support that), and then you would start adding the modified (or replaced) function instead of your original one. I wouldn't make them local, though -- you could put them into a table, and keep that table in FunctionsList. Here's an example of a table switch (which will replace an elseif chain that checks the same value) lua-users.org/wiki/SwitchStatement \$\endgroup\$ – Advert Aug 27 '14 at 0:54
6
\$\begingroup\$

This part of your code:

for name,func in pairs(functionTable) do
    if name == abilityName then
        amountToIncrease = 1
        func(self,amountToIncrease) --self because the card passes itself into the function
    end
end

Can be greatly simplified simply by checking if the index you are looking for exists or not:

local ability = functionTable[abilityName]
if ability then
    amountToIncrease = 1
    ability(self,amountToIncrease) --self because the card passes itself into the function
end

Even though the code for assigning strength and health to a card is not complete, it seems like you are assigning functions/abilities on one place, assigning strength/health at another place... And I'm not so sure about the methods to assign a card some abilities that you add when you create the card. It seems to me like it would be better to not call the addTaunt method but instead to assign the card taunt through the constructor. You seem to be grouping properties together instead of cards. You're defining all functions for all cards at one place, will you then define all strengths and healths for all cards at another place? It would be better to "group by card".


Right now you are mapping cardId --> function name in CardFunctionsList:functionNamesForId(id) and then you are mapping functionName --> Actual function. When I started refactoring this to skip the name I realized that you really need it that way, because of the doAbility function. A comment about that would be helpful so that I don't try refactoring it again :)

One thing I noted about the functions/abilities thingy though is that your FunctionsList only contains one thing: instance.functions. Why not simply use the instance directly?

FunctionsList:functionForName always returns a table. I don't see the usefulness of this. You can make it return a single function and violà, it will have reduces table nesting.

I managed to re-write your FunctionsList code to this:

function FunctionsList:new(functionNames)
    local instance = {}
    self:loadFunctionsToReturn(instance, functionNames)
    return instance
end

function FunctionsList:loadFunctionsToReturn(tabl, functionNames)
    for key,name in ipairs(functionNames) do
        tabl[name] = self:functionForName(name)
    end
end

function FunctionsList:functionForName(name)
    --even though the table key looks like plain text, it will accept a string to access it
    if       name == "increaseHealth" then return increaseHealth
      elseif name == "increaseStrength" then return increaseStrength
      elseif name == "addTaunt" then return addTaunt
      elseif name == "addCharge" then return addCharge
    end
end

And then you of course have to make the related changes in Card.lua, primarily the doAbility function which I wrote like this:

function Card:doAbility(abilityName)
    local ability = self.abilityList[abilityName]
    if ability then
        amountToIncrease = 1
        ability(self,amountToIncrease) --self because the card passes itself into the function
    end
end

A bit related to your question is this:

The basic idea is that each Card has a unique ID, and each ID has a table of strings associated with it.

When I read that I instantly thought , which works a little like this. Your code isn't really ECS though, but I just wanted to throw this out there.

\$\endgroup\$
  • \$\begingroup\$ +1 for shoehorning ECS in there :P and yeah, bazola make sure you lexically scope your variables just like in any other language. \$\endgroup\$ – Dan Pantry Aug 27 '14 at 18:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.