Python Controlled War

Flintlock1415

Emperor
Joined
Feb 24, 2008
Messages
1,057
Location
MN
For my next project, I need to control what nations can declare war or peace. I also need the flexibility to:

*Change who can declare war/peace from different triggers (e.g. certain turns)
*Set a team to a different war status without any interference from what is already defined.

I plan on doing this for every Civ in my mod's scenario, but for now I'll just start with the simplest relationship: the Normans and the English. They will be at war for the entire scenario, so this should be the easiest to implement. I think this function will be my best friend for most of what I'm trying to do:

Code:
BOOL canChangeWarPeace(TeamType eTeam)
bool (int /*TeamTypes*/ eTeam)

The difficult part of this is how to structure the code. Normally I can structure it pretty well, just with a lot of syntax and operation errors. :crazyeye: I think what I want to is use canChangeWarPeace as my main function, while setting it true or false with a set of if statements. I'll try some pseudocode here, and hopefully I can get some input on what to change. (wow this is a lot harder than I thought)

PHP:
	##########################
	## Python Controlled War##
	##########################
	pPlayer = gc.getActivePlayer()
	iTeam = pPlayer.getTeam()
	iTeamNormandy = gc.getTeam()## I don't know how to get a team from an integer
	iTeamEngland = gc.getTeam()## see above
	iRivalTeam = ## I don't how to check for the team that who is the target of war/peace
	
	def canChangeWarPeace(self, bValue):
		if iTeam == iTeamNormandy and iRivalTeam == iTeamEngland:
			return false
		else:
			return true
As you can tell by the comments, I am having trouble with how to get a TeamID from only knowing the civ/leader ID, and I have know idea on how to get the team that the active player is trying to change status with.

For all I know, I'm completely misusing this function (with I really think is true the more I think about it) but I couldn't come up with any other ways to do it.

Any help or ideas are very welcome, Thanks! :)
 
Here is some information which may point you in the right direction.

1. Redefining canChangeWarPeace will not do what you want. The game provides this function as a convenience for you to call. If you redefine it for your local use, that is fine; but the game will not call your function. It will continue to call its internal definition. I cannot think of any useful reason to redefine a game provided function.

2. The game often stores objects such as a team or player and provides you access functions, but it usually passes around references as integers. This is why you will usually see object pointers like pTeam and integers like iTeam or eTeam. If you have an integer which is passed to you by some other routine, you can convert it into an object pointer with a call like

pTeam = CyGlobalContext().getTeam(iTeam)

or more usually

pTeam = gc.getTeam(iTeam)

Once you have the object pointer, then you can call the python API functions.

3. To force two AI civs to be at war, you want to call pTeam1.declareWar(iTeam2, false). This means you first need to get an object pointer for team 1, and then you need to get an integer for team 2. I assume you have CIVILIZATION_NORMAN and CIVILIZATION_ENGLAND. You can convert these into iCivilizations with getInfoTypeForString. Then you have to search all the players to see which one has these iCivilization values. Then you have to find the team for each player. Then you can make the declareWar call. Counting the loop over all players, it is probably about 20 lines of python.
 
Where do you want to place the function?

If you do not get 2 team ids (like in onGameStart, where i guess, you could use it), you would have to cycle through all teams, like:

PHP:
			for iPlayer in range(gc.getMAX_PLAYERS()):
				pPlayer = gc.getPlayer(iPlayer)
				if pPlayer.getCivilizationDescription () == "CIVILIZATION_NORMANDIE" :
				    iTeamNormandie = pPlayer.getTeam()
				    NormandiePlayer = pPlayer
				    NormandieTeam = gc.getTeam(iTeamNormandie)
				if pPlayer.getCivilizationDescription () == "CIVILIZATION_ENGLAND" :
				    iTeamEngland = pPlayer.getTeam()
				    iPlayerEngland = i
				    EnglandTeam = gc.getTeam(iTeamEngland)
            NormandieTeam.declareWar(iTeamEngland,false)
            NormandiePlayer.AI_setAttitudeExtra(iPlayerEngland,-999)


The last line sets the attidute to -999, so that they'll never want to make peace.
 
OK, 12 lines, not 20 lines. But you may also want to check that both the norman player and the english player were found, to avoid some python exceptions. And I think you may want to call setattitudeextra twice, once for each team; I don't think it is kept symmetric for you.
 
If your mod is based around a single scenario with the same set of players every time, I suggest writing a module to look up all those players/teams and call it in onGameStart() and onLoad().

Further, since you're building the scenario you can control the team order and don't have to bother looking them up. However, looking them up isn't hard so it may be worth it in case you create multiple scenarios with the same players but different situations.

Code:
## DarkAgesEuropeanSlugfest.py

NORMANDY_PLAYER_ID = 0
NORMANDY_PLAYER = None
NORMANDY_TEAM_ID = 0
NORMANDY_TEAM = None

ENGLAND_PLAYER_ID = 1
ENGLAND_PLAYER = None
ENGLAND_TEAM_ID = 1
ENGLAND_TEAM = None

... other civs ...

def init():
	global NORMANDY_PLAYER, NORMANDY_TEAM
	NORMANDY_PLAYER = gc.getPlayer(NORMANDY_PLAYER_ID)
	NORMANDY_TEAM = gc.getTeam(NORMANDY_TEAM_ID)
	
	global ENGLAND_PLAYER, ENGLAND_TEAM
	ENGLAND_PLAYER = gc.getPlayer(ENGLAND_PLAYER_ID)
	ENGLAND_TEAM = gc.getTeam(ENGLAND_TEAM_ID)
	
	... other civs ...

With this done you can reference these IDs and objects from your other modules or other functions in the same module without having to look them up each time.

Code:
## DarkAgesEuropeanSlugfestEventManager.py

from DarkAgesEuropeanSlugfest import *

class DarkAgesEuropeanSlugfestEventManager:

	def __init__(self, eventMgr):
		... setup events ...
	
	def onGameStart(self, argslist):
		iGameTurn = argsList[0]
		initDAES()
		
		ENGLAND_TEAM.declareWar(NORMANDY_TEAM_ID, False)
		NORMANDY_PLAYER.AI_setAttitudeExtra(ENGLAND_PLAYER_ID, -999)
		ENGLAND_PLAYER.AI_setAttitudeExtra(NORMANDY_PLAYER_ID, -999)

Also, to block war declarations you need to use CvGameUtils.canDeclareWar():

Code:
def canDeclareWar(self,argsList):
	iAttackingTeam, iDefendingTeam = argsList
	if ((iAttackingTeam == ENGLAND_TEAM_ID and iDefendingTeam == SCOTLAND_TEAM_ID) 
	or (iAttackingTeam == SCOTLAND_TEAM_ID and iDefendingTeam == ENGLAND_TEAM_ID)):
		# England and Scotland are BFFs!
		return False
	# all other war declarations are okay
	return True

However, there is no equivalent function to block a peace treaty. Hopefully changing the attitudes above will do that.
 
Ok, before I start trying this then, I have a question:
How come I can't use this function (under CyTeam in the API):
Code:
BOOL canChangeWarPeace(TeamType eTeam)
bool (int /*TeamTypes*/ eTeam)

I'm guessing its because its a bool function, rather than a void, but how do I use it then? Say I did something like this:

Code:
pTeamNormandy.canChangeWarPeace(## not sure what exactly to put in here):
     return false
besides what I commented, what is wrong with this? I guess I don't have a full understanding of how bool functions work.
 
CyTeam.canChangeWarPeace(eTeam) is a function you can call in the SDK that will tell you if the first team can declare war on or sign a peace treaty with the second team. Without changing the function in the SDK you cannot change its return value (answer).

Think of it like this: canChangeWarPeace() is like asking someone their name. You cannot change their name by asking it. You'd have to change how the person behaves (how they answer the question). You cannot say, "Dave, what is your name? Answer Bob."

Now, as davidlallen said in point 1 of his first reply, you can redefine this function at the Python layer because Python is very dynamic. But to understand why that would have no effect you need a little background on how the Python and C++ (SDK) layers interact.

When you call CyTeam.canChangeWarPeace(eTeam) the Python layer takes the CyTeam object you called it on and the parameter eTeam and sends them to the DLL.

The DLL (in all the CyFooInterface.cpp files) defines what happens next. Each CyTeam in Python holds a pointer to a CyTeam in the C++ layer, and the DLL calls CyTeam::canChangeWarPeace() through this pointer (notice the :: to denote C++ versus the . in Python).

Each CyTeam in C++ holds a pointer to its matching CvTeam object (CyTeam is said to be a wrapper for CvTeam), and CyTeam::canChangeWarPeace() calls CvTeam::canChangeWarPeace() through this pointer.

Finally, CvTeam::canChangeWarPeace() does whatever it needs to do to determine the answer and returns the result back through CyTeam, the C++-Python layer, the CyTeam in Python, and finally to your code.

Now as I said above, the function in the Python CyTeam that calls to the DLL is defined in the DLL, however you can merrily replace its definition in Python. However, other functions in the DLL that call CvTeam::canChangeWarPeace() won't see this new definition. The CyFoo objects are only used when calling from Python to C++.
 
Ok, now I see that I was screwing up the meaning of 'return'. If I understand correctly, all of the bool, int, and string functions only return a value, but don't do anything. The only functions that do do things are the void functions, and they don't return a value.

Now, I just need to try and see how far I can get with what has been suggested here. Thanks for clearing this up! :D
 
That is an unusual way to look at it, but I suppose it is correct for this set of functions. The way that I look at it is, a function which is named "get something" tells me the value of the something. A function which is named "set something" lets me set the value of the something. The function you were looking at, "can change", tells you whether the team can change or not. It does not make it change.
 
But just to be complete, there is nothing inherent in C++ or any programming language that I know of that enforces this as a rule. A function may do things and return a value. For example, it would make sense to have a changeProduction(x) to add x to the current production and return the new value. Civ doesn't do this, but it's certainly possible.

Hopefully the programmers choose good names to indicate what actually happens. And documentation can go a long way to making this clearer, but the Civ code sadly has none.
 
I do understand that this is not true for all programs, but as far as Civ is concerned with the way they set it up, it is mostly true. And I think davidlallen's logic is better than how I described it, I was just pointing out what I observed.

Anyway on to my next question:
I don't think I fully understand how Python initializes modules, classes, and variables. I've been reading through a lot of the Python documentation, which is clearing some things up for me, but not all of it.

First, I created a module, like you said, and named it CvBattleOfHastings.py. Here is how that looks:
Spoiler :
Code:
## Battle of Hastings

NORMANDY_PLAYER_ID = 0
NORMANDY_PLAYER = None
NORMANDY_TEAM_ID = 0
NORMANDY_TEAM = None

ENGLAND_PLAYER_ID = 1
ENGLAND_PLAYER = None
ENGLAND_TEAM_ID = 1
ENGLAND_TEAM = None

## Wales, Norse, Brittany, France, Ireland, Scotland, HRE, Flanders

def init():
	global NORMANDY_PLAYER, NORMANDY_TEAM
	NORMANDY_PLAYER = gc.getPlayer(NORMANDY_PLAYER_ID)
	NORMANDY_TEAM = gc.getTeam(NORMANDY_TEAM_ID)
	
	global ENGLAND_PLAYER, ENGLAND_TEAM
	ENGLAND_PLAYER = gc.getPlayer(ENGLAND_PLAYER_ID)
	ENGLAND_TEAM = gc.getTeam(ENGLAND_TEAM_ID)
As far as I can tell, this module looks good but the problems start in my CvCustomEventManager.py file. Here is the top part of that file (bolded lines are new changes, and I underlined the most problematic line for me.)
Spoiler :
Code:
## Battle of Hastings


from CvPythonExtensions import *
[B]from CvBattleOfHastings import *[/B]
import CvEventManager

gc = CyGlobalContext()
localText = CyTranslator()

class CustomEventManager(CvEventManager.CvEventManager):
	
	def onGameStart(self, argsList):
		## Allows Romney event
		CyMap().plot(63,15).setScriptData("no")
		
		##########################
		## Python Controlled War##
		##########################
		[B][U]initCvBattleOfHastings()[/U][/B]
		
		[B]NORMANDY_PLAYER.AI_setAttitudeExtra(ENGLAND_PLAYER_ID, -999)
		ENGLAND_PLAYER.AI_setAttitudeExtra(NORMANDY_PLAYER_ID, -999)
		## makes each team hate each other to prevent peace[/B]

The problem is, I don't know how to how to run the init function of the BoH module, so that NORMANDY/ENGLAND_PLAYER/TEAM all get set. Also, I don't have an __init__ function in my custom event manager, but from what I can tell is that its a very useful function for setting up globals and different variables. I will likely have more questions on this later...

Anyway if I could just get some info on how to initialize the module, that would be great!
Thanks again!
 
There is a lot to be learned by reading the working code of other mods. I have learned quite a lot from the mods distributed with BTS, such as Gods Of Old and Final Frontiers. My first mod, Fury Road (see sig) also has what I believe is relatively straightforward, well documented python code which does a lot of interesting things. But, that is just my opinion.
 
When you do

Code:
from CvBattleOfHastings import *

Python loads the CvBattleOfHastings module if it hasn't been loaded already (each module gets loaded at most one time). This executes the code that is at the top-level of the module--stuff not inside functions or classes. In this case it defines all those variables in ALL_CAPS and assigns them the values given. It does not call the init() function, but it does define it so that the name "init" points to a function in that module.

After the module loads, the import statement in this form (from ___ import *) copies all the names in the module to the importing module (your custom event handler module). These are references to the actual variables in the BoH module--not copies. Normally you must reference a name in another module using "<module>.<name>". In this case, however, you can omit the "<module>." part and reference them directly by name. It's strictly a matter of convenience and less typing; no other difference.

This is why you can type

Code:
gc = CyGlobalContext()

instead of

Code:
gc = [B]CvPythonExtensions.[/B]CyGlobalContext()

Now the problem. In your onGameStart() function you try to call a function called initCvBattleOfHastings(), but there is no such function. The BoH module has an init() function, and this is what you need to call. I had meant to rename the function to make the name clearer, but I accidentally only changed the name where I was calling the function. My bad!

The __init__() function is used for classes. When you create an object out of a class (recall that a class just defines behavior; you must instantiate an object from it to get any data, and you can do this as many times as you like with each object being a different set of data), it calls its __init__() function if it has one.

The CvCustomEventManager module was designed so that you could put your mod-specific events into a separate module and attach your event handler module to the CEM by adding just two lines to CvCEM. It's okay to do it the way you have done it, it would just make it harder for someone else to merge your mod with other mods, but not impossible.
 
I must be missing something here, because that function is still not being called. I renamed initCvBattleOfHastings() to simply init(), as you said. I'll try some debug messages tommorow, but I'd thought I'd ask if anyone can spot anything before I go to bed. :) (Just check the code in post 11.)
Thanks!
 
Are you adding your event handler to the event manager? You need something like this

Code:
self.addEventHandler("GameStart", self.onGameStart)

Are you using Dr. Elmer Jiggles's CvCustomEventManager module? Post your complete code because I just can't tell if you're setting up the events correctly.

Also note that the player ID #s I chose (0 for Normandy, 1 for England) were just guesses. I have no idea what they are in your particular scenario. You need to make sure that you assign the correct IDs or the wrong players will be fighting.
 
I'm not using Dr. EJ's module; I set up my own. I know you were guessing, but they were corrects guesses. :) Also, you'll have to explain to me what the EventHandler does. I've not needed for any of my other functions, which are working.

The error I get is that 'NoneType' does not have an attribute called AI_SetAttitudeExtra. What this tells me is that init is not being called. Does the eventHandler have anything to do with that?

Anyway, here is my code:

CvBattleOfHastings.py
Code:
## Battle of Hastings

from CvPythonExtensions import *

gc = CyGlobalContext()

NORMANDY_PLAYER_ID = 0
NORMANDY_PLAYER = None
NORMANDY_TEAM_ID = 0
NORMANDY_TEAM = None

ENGLAND_PLAYER_ID = 1
ENGLAND_PLAYER = None
ENGLAND_TEAM_ID = 1
ENGLAND_TEAM = None

## Wales, Norse, Brittany, France, Ireland, Scotland, HRE, Flanders

def init():
	global NORMANDY_PLAYER, NORMANDY_TEAM
	NORMANDY_PLAYER = gc.getPlayer(NORMANDY_PLAYER_ID)
	NORMANDY_TEAM = gc.getTeam(NORMANDY_TEAM_ID)
	
	global ENGLAND_PLAYER, ENGLAND_TEAM
	ENGLAND_PLAYER = gc.getPlayer(ENGLAND_PLAYER_ID)
	ENGLAND_TEAM = gc.getTeam(ENGLAND_TEAM_ID)
CvCustomEventManager.py
Code:
## Battle of Hastings


from CvPythonExtensions import *
from CvBattleOfHastings import *
import CvEventManager

gc = CyGlobalContext()
localText = CyTranslator()

class CustomEventManager(CvEventManager.CvEventManager):
	
	def onGameStart(self, argsList):
		## Allows Romney eventto occur once
		CyMap().plot(63,15).setScriptData("no")
		
		##########################
		## Python Controlled War##
		##########################
		init()
		
		NORMANDY_PLAYER.AI_setAttitudeExtra(ENGLAND_PLAYER_ID, -999)
		ENGLAND_PLAYER.AI_setAttitudeExtra(NORMANDY_PLAYER_ID, -999)
		## makes each team hate each other to prevent peace
The only thing I omitted was my other functions (onUnitPillage, onChangeWar) because they have no relevance here.
 
Each of those functions (onGameStart, onUnitPillage, etc) are called event handlers because they are called to handle the event to which they are tied (GameStart, unitPillage, etc). From your code and that your other event handlers are working, I'd say you modified CvEventInterface.py to replace CvEventManager with your CustomEventManager. In that case, you don't need the addEventHandler() calls because CvEM does that already. End result: this shouldn't be the problem.

That error indeed means that the variables aren't being set. However, I think that perhaps I do not understand how "from __ import *" works. I think now that it actually copies the names and their current values from one module to another. When you call init(), it only modifies the original values in the other module, not the copies in the CEM module.

Change the import to be a regular import:

Code:
import BattleOfHastings

And now insert "BattleOfHastings." in front of all references to variables/functions in that module:

Code:
[B]BattleOfHastings.[/B]init()

[B]BattleOfHastings.[/B]NORMANDY_PLAYER.AI_setAttitudeExtra([B]BattleOfHastings.[/B]ENGLAND_PLAYER_ID, -999)
[B]BattleOfHastings.[/B]ENGLAND_PLAYER.AI_setAttitudeExtra([B]BattleOfHastings.[/B]NORMANDY_PLAYER_ID, -999)

If you don't like all those module prefixes, you can use the previous method (from ___ import *) by using functions to access all of the variables.

Code:
ENGLAND_PLAYER_ID = 0
ENGLAND_PLAYER = None
...

[B]getEnglandPlayerID():
    return ENGLAND_PLAYER_ID[/B]

[B]getEnglandPlayer():
    return ENGLAND_PLAYER[/B]
...

and

Code:
init()

[B]getNormandyPlayer()[/B].AI_setAttitudeExtra([B]getEnglandPlayerID()[/B], -999)
[B]getEnglandPlayer()[/B].AI_setAttitudeExtra([B]getNormandyPlayerID()[/B], -999)

Yet another style option is to combine the two modules. If these are the only two Python modules that operate on these variables a lot, this makes sense without causing too much confusion. Just move all the code from CEM to BoH.

As long as I'm showcasing different coding styles, the fourth option is to move the code that works on all these players to the BattleOfHastings module itself while leaving the CEM in its own module.

Code:
ENGLAND_PLAYER_ID = ...

def init():
    ...

def startBattleOfHastings():
    NORMANDY_PLAYER.AI_setAttitudeExtra(ENGLAND_PLAYER_ID, -999)
    ENGLAND_PLAYER.AI_setAttitudeExtra(NORMANDY_PLAYER_ID, -999)

and

Code:
init()
startBattleOfHastings()

Take your pick. :)
 
I like the fourth option best. It worked like a charm and using modules makes it a lot easier for me to keep the Custom event manager nice and short. I think learning about modules now will also help me out when I try learning to make my own programs (again.)

Thanks for all of the info, and putting up with my endless questions! :D
 
Good choice. I left that to last thinking most people wouldn't want to do it that way, but that's really the "best" method IMHO. It allows the event manager to do what it needs to do (tell your mod to do stuff) and lets the BoH module do that it needs to do (do things related to the BoH like start initial wars). :goodjob:
 
Top Bottom