Chain-linking functions

Baldyr

"Hit It"
Joined
Dec 5, 2009
Messages
5,530
Location
Sweden
I had this idea of simplifying the way I'm doing unit spawns for scenarios. So instead of doing series of if statements to trigger the spawn, I though I'd create a Spawn() class and wrap these different checks into a set of functions. Something like this:
Code:
Spawn().trigger(iTurn1, iTurn2).interval(iInterval, iModulo).player(iCiv, bHuman).landPlot(bNoUnits, bNoCities, bWithinBorders).createUnits(iCiv, tCoords, iUnitType, iNumUnits)
The function achieving the spawn would be the fifth one and the first to fourth would be different sets of checks used to trigger the last. The point of the whole exercise would of course be that these functions or checks could be used in any possible combination, depending on what needs to be done. The alternative, as I see it, would be to overload the different unit spawning functions with arguments for all these checks.

The problem is that I seem only be able to get errors like:
"trigger" instance has no argument "player"
Is there a way of chain-linking functions like this? It would almost be like using the inherit CivIV functions:
Code:
CyGlobalContext().getMap().plot(x, y).getPlotCity()
I realize that getMap() returns a CyMap instance, plot() returns a CyPlot instance and getPlotCity() returns a CyCity instance, but could I have my functions also produce an "instance" of sorts? Like a "this check i cleared" instance, or something. :rolleyes:

Is there any other way of having "modular" checks without having to type if statements for every spawn? (Not counting making functions wrapping different sets of checks.)

edit: And it would also be very useful to be able to create something like a Plot() class that would take the coordinates and then be able to stack sets of spawn functions (for cities, buildings, units, XP or whatever) on the same line (so that they all use the same CyPlot instance without having to define a variable and type a line for each function). But I'm guessing that's not gonna happen...
 
:confused: why do you not just create a longer function call?
Spawn().trigger(iTurn1, iTurn2, iInterval, iModulo, iCiv, bHuman, bNoUnits, bNoCities, bWithinBorders, iCiv, tCoords, iUnitType, iNumUnits).

And you do not compare right ;).
CyGlobalContext().getMap().plot(x, y).getPlotCity() first will give you a map, than from this instance a plot is called, then from the plot instance a city is called.
This can be done, because every of these classes do have a function for this, which return valid values.

Your exampel Spawn().trigger(iTurn1, iTurn2).interval(iInterval, iModulo).player(iCiv, bHuman).landPlot(bNoUnits, bNoCities, bWithinBorders).createUnits(iCiv, tCoords, iUnitType, iNumUnits) then would need a class "trigger",with a function "interval", which would result in another class which you need a function player. The resulting class would need a function landplot, which would need to have...etc, etc.
 
I think what you're looking to do is have each function do

Code:
return self

at the end. As long as your Spawn class defines all those functions you listed, this will work. The problem you'll have, however, is that you want execution of the entire line to cease as soon as one check fails, right?

You can do this by setting a flag in Spawn() that starts as True and gets set to False inside each function if it fails. The functions would first check that flag and not bother to check their arguments if it's already False.

Code:
class Spawn:

    def __init__(self):
        self.okay = True

    def isOkay(self):
        return self.okay

    def player(self, ePlayer, human):
        if self.isOkay():
            if gc.getPlayer(ePlayer).isHuman() != human:
                self.okay = False
        return self

    def trigger(self, turn1, turn2):
        if self.isOkay():
            turn = gc.getGame().getGameTurn()
            if turn < turn1 or turn > turn2:
                self.okay = False
        return self

This will just mean that all those extra function calls will still happen, but they'll exit immediately upon finding okay to be False.
 
This almost makes sense to me. :D

What would be the benefit of using:
Code:
if self.isOkay():
instead of simply:
Code:
if self.okay:
:confused:

As you seem to be of the enlightened ones here: What exactly does "self" actually do? I know when to use it (pretty much all of the time) but I haven't figured out exactly why it is needed in the first place. Like how come its always one of the arguments in a definitions, but you don't actually type it in. I've seen errors where it has been counted in as one of the arguments though, so I realize it is used for something.
 
"self" is used only for class methods: functions that belong to a class and operate on an object of that class. In this case, self is the instance of the Spawn class that receives the function calls. I highly recommend the free online book How to Think Like a Computer Scientist. It will describe Object Oriented Programming and introduce you to Python.

Why use self.isOkay()? That's a deeper question. Here it doesn't seem like it adds any value--only makes it longer to type. Where it becomes useful is a month from now when you make "this object is okay and should spawn the unit(s)" determined by more than a single boolean field called "okay". Perhaps you need to ask a helper object or combine two booleans into one.

Code:
class Spawn:
    def __init__(self):
        self.allowed = True
        self.disallowed = False

    def isOkay(self):
        return self.allowed and not self.disallowed

That example is a bit contrived and wouldn't really work here, but it demonstrates the use of the function. If you hadn't used the function, you would now need to change every function that used "self.okay". You may miss one, or you may just hate doing repetitive work. Either way, the function means you can change the definition in one place and pick up the change everywhere.

This concept is called encapsulation: hiding implementation details that might change behind functions that don't. Making it easier to do is one of the primary reasons for OOP to exist. The other reasons are more complicated but explained in that online book as well as many others.
 
I actually understood what you said about encapsulation, and I was betting on the answer beeing a matter of doing things proper. I've already pretty much started doing my own stuff with encapsulation, but this is a good example of why I should keep doing more of it!

As for the self issue, I believe that I kinda get it now, but thanks for the link! :goodjob:

Now, if I only had the time and the stamina to actually get this working, as I have many ideas... :rolleyes: Man, programming is fun! :king:
 
I myself was very much into programming growing up (on the family C64) but never took the step up to assembler. Mostly because we couldn't afford the hardware but also since math in elementary school made it all to obvious that I wasn't gonna go anywhere with the programming stuff. I still struggle with math, so that hampers my code also. But I must say, programming is just as fun now as it was back then! :king:

As for the subject at hand, I haven't had the opportunity to do any code since I started this thread, but have done some planning. Right now there would probably be three classes:

The Trigger class for doing checks against game turns/years, turn intervals, random turns, and the human player (as I wouldn't wanna give the human player free stuff in most cases). The functions in this class would be used in if statements to produce boolean returns. Like:
Code:
Trigger.checkYears(iYear1, iYear2).randomTurn(iInterval).isTrigger()
(I'm thinking that the last function is necessary to get the line to return a "True/False" value.)

The SpawnUnit class for spawning units, checking map tiles and areas, and chain-linking optional functions to enable more settings. Like:
Code:
SpawnUnit(iCiv).checkPlayer(bHuman).triggerTurn(iTurn).area(tTL, tBR).checkArea(bLand, bDomestic, bForeign).makeUnit(iUnitType, iNumUnits, AIType).setUnitLevel(iLevel, iXP)
The SpawnCity class for spawning cities and setting optional variables and chain-linking optional functions to enable more settings. Like:
Code:
SpawnCity(iCiv).triggerYear(iYear).cityPlot(iX, iY, bForce).foundCity(iPopulation).setCityName(name).setCulture(iCityCulture, iPlotCulture).setUnitType(iUnitType).createGarrison(iNumUnits)
I guess both the SpawnUnit and the SpawnCity class would have to have their own sets of encapsulated trigger functions. And the SpawnCity class would have its own functions for adding units to the spawn. But would it be possible to "borrow" functions from other classes and add them on the same line? (I'm looking at PyHelpers.py to figure out how this could be done, but I'm not even sure how that module works... :rolleyes:)
 
Looking at PyHelpers I'm beginning to realize that every class should have its own line, but that the previous line could carry over an instance from the previous one. Like:
Code:
bTrigger = Trigger(iCiv).checkYears(iYear1, iYear2).isTrigger()
pCity = SpawnCity(iCiv).trigger(bTrigger).cityPlot(iX, iY, bForce).foundCity(iPopulation)
SpawnUnit(iCiv).getGarrisonUnit(pCity, bConscript).makeUnit(iUnitType, iNumUnits, AIType)
...and so on. There would still be no if statements what so ever, even if the code is now on three separate lines.

Or maybe I could just nestle different classes into one another, like:
Code:
SpawnUnit(iCiv).city(trigger(checkYears(iYear1, iYear2)).cityPlot(iX, iY, bForce).foundCity(iPopulation)).setGarrisonUnit(iUnitType, bConscript).createGarrison(iNumUnits)
This would be rather messy though - and this was the most basic example if could think of! :eek:

But regarding PyHelpers, I still don't entirely get how the Python translator (or whatever) knows where to find PyCity.GetCy() just because it is used on a PyPlayer instance (or whatever). Like when you use PyPlayer(iPlayer).getCityList(), then loop the references (or whatever) in the list and call on GetCy() without ever mentioning the PyCity class. :confused: How is PyCity automatically subordinate to PyPlayer? :confused:

I suppose it has to do with the fact that the list was created with a PyPlayer function - do all the references populating the list carry some kind of identification of the PyPlayer instance (self.player) or simply the CyPlayer instance? (Because the init definition clearly states that PyCity needs both a player instance and a city instance.)

If so, could I have the Trigger class create an instance of sorts that the SpawnCity class functions would be able to use, that in turn creates an instance that the SpawnUnit class functions then would be able to use? All on one single line, like:
Code:
Trigger(iCiv).checkYears(iYear1, iYear2).cityPlot(iX, iY, bForce).foundCity(iPopulation).createGarrison(iNumUnits, iUnitType, bConscript)
 
...and looking at the PyInfo class I realize that classes can be subordinate to each-other. As far as I can tell the PyInfo class is used to define the stuff in InfoDictionary for the subordinate classes. (It strikes me as strange that the PyInfo class lacks an init line and only lists the functions.)

Am I correct if I assume that the the PyInfo classes are used like:
Code:
strDescription = PyInfo().getInfo(strInfoType, strInfoName).BonusInfo(bonusID).getDescription()
Because this would be exactly the type of stacking I was thinking about!

How would a call directly to a "helper" class from another .py file look like? "CustomUtils.Spawn().SpawnUnits()" (abbreviated simply as "spawnUnits" at the start of the file)?
 
I still don't get, why you want to add X classes, when one simple function could do it.
There is an actual reason, or so I believe, but it should be sufficient to say that the easy way is something I've already done, so where would the fun be in that? :p

I'm trying to learn this programming stuff, and the only way forward is to keep upping the difficulty. Every once in a while it seems like I double my knowledge, and this is gearing up to be one of those occasions. Now if someone just could point me in the general direction I need to go next...
 
But this thing here will need 6 or more classes, which will only have one function eacg, which will include only 2 or 3 civ 4 python commands.
Imho the work is really unneccessary.
Obviously, there would be a bigger picture that you're missing. Because I haven't bored you with it.

But please indulge me if you know anything of use to me. I might just learn something, and I don't believe that would be such a terrible thing.

Anyone can of course join in, but just make sure to dumb it down for me. :lol:
 
What would really help here is one or two examples in English of how you would use this.

One thing I'm thinking is that you may want to define all these triggers once when the mod loads and have them checked at some interval by the game. If you will have a smallish number of triggers (100-200) it would be far better to do it this way than to create/destroy/create/destroy/... them every turn.

A really good example of an API that makes good use of method chaining is the PHPUnit and EasyMock (Java) mock object libraries. In short a mock object library allows you to create "fake" objects in a software system and define what functions you expect to be called while doing testing of connected "real" objects. These expectations say what the method name is, what parameters it expects, how many times it should be called (and/or when), and what it should do when called.

Code:
mock.expects(once())
    .method("save")
    .withArguments("4000BC.Civ4Save")
    .will(raiseException("FileNotFoundException"))

mock.expects(exactly(5))
    .method("randNum")
    .withArguments(any())
    .will(returnValue([42, 25, 38, 94, 3]))

Here the initial expects() call creates an object of type Expectation that has all the other methods: method(), withArguments(), will(). The class that contains this code provides the other support functions: once(), exactly(), returnValue(), etc. These could easily live on the Expectation class.

Your class will be a little different. I think your main class should not be Spawn but Trigger. A Trigger has Constraints (exact turn, turn range, turn interval, player is human, player is AI, city has pop at least X, city is healthy, etc) that control when it can be triggered, and it has Actions that are performed when it's triggered (spawn unit, create city, etc).

That's a good place to start, I think. I'll take a closer look at your previous few posts when I get a larger block of time.
 
What would really help here is one or two examples in English of how you would use this.
What I'm actually trying to do, is create a set of easy-to-use functions for scenario makers. Like for spawning units and cities randomly or on set dates. I've already done this, however, and others have tested some of the code successfully. The idea originally evolved from me trying to help others create historical events.

The functions I've made are things that I believe would be useful for a scenario maker with little to none programming skills. Essentially what I would have wanted to have myself before I learned to make my own code. Because not everyone gets into modding with the intention of learning how to do code, but rather to reach the design objectives of the scenario at hand.

So, as an example I have like a dozen or so custom functions for spawning units. Basically one function for every purpose, so that the amount of arguments doesn't add up beyond anything that could be called user friendly. (I could, of course, just make one single function for all purposes taking like 30 different arguments, but that's not what I was trying to do.)

The thing is, however, that even with such custom functions the would-be scenario maker would have know a little Python - in order to make if statements that trigger these functions or limit them in different ways, and also figure out the whole indentation thing (which is something that some will really struggle with). So I thought I'd just make all lines of code except my own stuff redundant, by enabling the non-programmer scenario designer to stack functions on one single line. There wouldn't be different functions for different types of spawns or other events either, as each function only adds another optional feature.

Because sometimes the most basic city or unit spawn would be enough. (Like spawning barbarians on an uninhabited continent or something.) But once you really get into designing your scenario you find out that you need to add limitations and checks to these spawns, or to achieve more elaborate spawns (like presetting buildings in cities or set the religion present). This is why the whole thing would have this modular approach - if I can manage to figure out how to do it.

This sure is a challenge for me (I've yet to figure out PyHelpers :rolleyes:) but that is what makes it interesting. :king:

One thing I'm thinking is that you may want to define all these triggers once when the mod loads and have them checked at some interval by the game. If you will have a smallish number of triggers (100-200) it would be far better to do it this way than to create/destroy/create/destroy/... them every turn.
I'm afraid you lost me on the mock objects, but I do appreciate the effort! :goodjob:

Your class will be a little different. I think your main class should not be Spawn but Trigger. A Trigger has Constraints (exact turn, turn range, turn interval, player is human, player is AI, city has pop at least X, city is healthy, etc) that control when it can be triggered, and it has Actions that are performed when it's triggered (spawn unit, create city, etc).
Yeah, I'm glad you say this because this is what I was gonna try this weekend. :goodjob:

But I would also wanna be able to skip the triggers/checks if there is some other code triggering or validating the spawns. But I guess I could just have the initial Trigger() call return a boolean value if no actual checks are included on the line, or something. (I'll try to figure something out, mostly by trial and error.) This way I could still use if statements instead of triggers if the trigger class fails to deliver exactly the checks that would be needed.

Also, I need to be able to use the trigger functions on their own, like in an if statement:
Code:
if (Trigger(iCiv).turn(iTurn)):
 
What you are describing sounds similar to the random event system. Are you familiar with that? If not, please read solver's guide. Maybe you can do what you want by making some sdk changes, which will make the random event system more flexible. Certainly, the number of modders who "could" add new xml is larger than the number who "could" use pre-canned python routines.
 
What you are describing sounds similar to the random event system. Are you familiar with that? If not, please read solver's guide. Maybe you can do what you want by making some sdk changes, which will make the random event system more flexible. Certainly, the number of modders who "could" add new xml is larger than the number who "could" use pre-canned python routines.
Oh, I'm sure you're right. But I'm sticking to Python until I feel comfortable with this programming stuff. I'm not about to do anything involving the SDK at the moment, even if being able to do my own random events would rank high on the list of my goals for the programming thing. :king:

At the moment I'm trying to figure out how classes that are subordinate to each-other work. Like, do they share the class instance and therefore any set variables (accessible with self). Or do I need to use the global command?

Too bad time is scarce... :p
 
The How to Think Like a Computer Scientist book would help you a lot. You should never need to use the globals() function. If class B is a subclass of class A (what you are calling subordinate), it has access to the fields and functions of B via self.

Code:
class A:
    def __init__(self):
        self.foo = 5

class B:
    def __init__(self):
        self.bar = 2

a = b()
print a.foo
> 5
print a.bar
> ERROR!

b = B()
print b.foo
> 5
print b.bar
> 2

What I meant when asking for an example is a concrete example from a scenario:

  • On turn 25, every human player spawns a Warrior in every one of their cities.
  • On turn 281, the English player spawns 5 Redcoats in their capital.
  • Every 20th turn, the barbarians spawn 3 Archers somewhere on the map.
I'm a coder--not a scenario maker. I can make up some trivial examples like the above, but it helps to see more complicated examples that you're planning.
 
Back
Top Bottom