• Civilization 7 has been announced. For more info please check the forum here .

Sample Python Code

Kael

Deity
Joined
May 6, 2002
Messages
17,401
Location
Ohio
A little present for the folks trying to learn Python (I had never heard of Python before Civ4 was released so I'm still learning too). This is a little custom function I wrote for my mod that I thought displayed a few ways to do things that other may find interesting.

Im not the best programmer (not even a good one) but I thought if you guys learn like I do then the more examples you are exposed to the better you will be.

Please feel free to use this thread to post sample code of your own (please don't use this thread to post broken code, I want a common place people can trade working ideas).

Code:
	def FFHFireAura(self, iDmg, pUnit):

		iX = pUnit.getX()
		iY = pUnit.getY()
		iPlayer = pUnit.getOwner()
		pPlayer = gc.getPlayer(iPlayer)
		eTeam = pPlayer.getTeam()
		for iiX in range(iX-1, iX+2, 1):
			for iiY in range(iY-1, iY+2, 1):
				pPlot = CyMap().plot(iiX,iiY)
				for iUnit in range(pPlot.getNumUnits()):
					insDmg = iDmg
					pUnit = pPlot.getUnit(iUnit)
					iStr = pUnit.baseCombatStr() * 3
					iLevel = pUnit.getLevel() * 3
					insDmg = iDmg - (iStr + iLevel)
					if pUnit.isHasPromotion(gc.getInfoTypeForString('PROMOTION_MAGIC_RESISTANCE')):
						insDmg = insDmg /2
					if insDmg >= 1:
						pUnit.setDamage(pUnit.getDamage() + insDmg, True)
						if pUnit.getOwner() != iPlayer:
							p2Player = gc.getPlayer(pUnit.getOwner())
							e2Team = gc.getTeam(p2Player.getTeam())
							e2Team.declareWar(eTeam, False)

What it does: passed a dmg amount and a unit object it does dmg to all units within 1 tile of the selected unit (including the unit itself). The amount of dmg that is done is mitigated by the effected units level and strength. Lastly if the unit that is hurt isn't the players own then they will declare war on the player.

Use: In my mod I have a guy that called this function every turn. He is a very nasty guy to hang around. I will also be using it as a spell that damages everyone in the area. There is nothing to keep you from a smiliar function to perform a check, add or remove promotions, etc etc.
 
Great idea! I wish I had this sort of thing when I was learning (I also learnt with Civ 4).

Code:
def areaExp(self, pCity):
	
	### Set up counters (could do with a dictionary, but this way is simpler)
	iForest = 0
	iJungle = 0
	iHill = 0
	iCoast = 0
			
	iPlotX = pCity.getX()
	iPlotY = pCity.getY()	
		for iXLoop in range(iPlotX - 2, iPlotX + 3):
		for iYLoop in range(iPlotY - 2, iPlotY + 3):
			### Checks for tiles not in city radius
			if ((iXLoop != 2) and (iYLoop != 2)) or \
			   ((iXLoop != -2) and (iYLoop != 2)) or \
			   ((iXLoop != 2) and (iYLoop != -2)) or \
			   ((iXLoop != -2) and (iYLoop != -2)):
			
				lPlot = CyMap().plot(iXLoop, iYLoop)
			
				if lPlot.getTerrainType() == gc.getInfoTypeForString("TERRAIN_HILL"):
					iHill += 1
				if lPlot.getTerrainType() == gc.getInfoTypeForString("TERRAIN_COAST"):
					iCoast += 1
				if lPlot.getFeatureType() == gc.getInfoTypeForString("FEATURE_FOREST"):
					iForest += 1
				if lPlot.getFeatureType() == gc.getInfoTypeForString("FEATURE_JUNGLE"):
					iJungle += 1
What it does: Checks all the squares in a cities workable radius (the fat cross), and counts how maiy of certain features there are in this radius,

Use: In my dynamic experience mod I used this to allocate data to units on creation, based upon the area around where they were created, with the idea that units being made in cities surrounded by forests would be more likely to get a woodsman promotion, for example.

Things to note: The range function does not include the upper value, so the upper value in the code has to be one greater than the actual maximum value needed. I hadn't noticed this when I first wrote the code.
 
Quitos said:
thanks. i have a silly question. where to run this functions :)

I import a python file that I create all of these custom functions. The logic of the functions are useful to those wrestling with Python, its good to see sample code that works. But these aren't helpful as cut and pastes to be dropped into files and gain the functions. More work than that is required. Functions need to be written or altered to use them.

So any of the files in the Assets/Python/ directory could use them, though if your not working on programming in python I don't think these will be very useful to you.
 
Great Apple and Kael,

Why does both of your code seem to go one farther to the right than to the left when you are both doing something in a fixed radius aroung a point?

Kael has:
for iiX in range(iX-1, iX+2, 1):
for iiY in range(iY-1, iY+2, 1):

and Great Apple has:
for iXLoop in range(iPlotX - 2, iPlotX + 3):
for iYLoop in range(iPlotY - 2, iPlotY + 3):

Both seem to go farther to the right than to the left.

Roger Bacon
 
RogerBacon said:
Great Apple and Kael,

Why does both of your code seem to go one farther to the right than to the left when you are both doing something in a fixed radius aroung a point?

Kael has:
for iiX in range(iX-1, iX+2, 1):
for iiY in range(iY-1, iY+2, 1):

and Great Apple has:
for iXLoop in range(iPlotX - 2, iPlotX + 3):
for iYLoop in range(iPlotY - 2, iPlotY + 3):

Both seem to go farther to the right than to the left.

Roger Bacon

I have no idea. I just know when I first wrote it to be (iX-1, iX+1, 1) it didn't cover the complete area so I had to extend it an additional step to the east. I don't know if the range function doesn't act on the last step or if there is something unusual about the coordinates that the getX() and getY() functions return.

I suspect the range function doesn't kick on the last pass. I have seen units skipped on processing when they are the last unit in the range.
 
Kael said:
I import a python file that I create all of these custom functions. The logic of the functions are useful to those wrestling with Python, its good to see sample code that works. But these aren't helpful as cut and pastes to be dropped into files and gain the functions. More work than that is required. Functions need to be written or altered to use them.

So any of the files in the Assets/Python/ directory could use them, though if your not working on programming in python I don't think these will be very useful to you.

i am starting to learn modding... i learned some Python, and my problem is how to build the files that will be called by the game.. now i am trying to dissect some simple mods to figure it out... not successful yet
 
The Great Apple said:
Things to note: The range function does not include the upper value, so the upper value in the code has to be one greater than the actual maximum value needed. I hadn't noticed this when I first wrote the code.

- Basically what Kael said. It doesn't include the final value.
 
The Great Apple said:
- Basically what Kael said. It doesn't include the final value.

Wow, that's good to know. I hope they change that in the next version of Python.

Roger Bacon
 
RogerBacon said:
Wow, that's good to know. I hope they change that in the next version of Python.

Roger Bacon
I doubt it. Just add a +1 in the high value.

Also you don't need all 3 argumants. The only one you really need is the high one.

range(10) will return:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 
Well, as many of you here will know, I have had my fair share of problems with the Python language. However I did get THIS one to work:

Code:
def onBeginPlayerTurn(self, argsList):
		'Called at the beginning of a players turn'
		iGameTurn, iPlayer = argsList

		player = gc.getActivePlayer()
		if (player.isCivic(gc.getInfoTypeForString("CIVIC_SLAVERY"))):
			for i in range(player.getNumCities()):
				player.getCity(i).setFreeSpecialistCount(gc.getInfoTypeForString("SPECIALIST_SLAVE"), 1)
		else:
			for i in range(player.getNumCities()):
				player.getCity(i).setFreeSpecialistCount(gc.getInfoTypeForString("SPECIALIST_SLAVE"), 0)

What it Does: Looks to check what Civic you are in and-if it matches the one it is looking for-then it grants a free specialist of the type mentioned in every city.

Use Not Currently using it in this form (as I wanted a different way to make slavery more....interesting. However, in modified form, it will allow certain Civics to grant you quite unique benefits (like Theocracy might grant you a free priest).

Yours,
Aussie_Lurker.
 
RogerBacon said:
Great Apple and Kael,

Why does both of your code seem to go one farther to the right than to the left when you are both doing something in a fixed radius aroung a point?

Kael has:
for iiX in range(iX-1, iX+2, 1):
for iiY in range(iY-1, iY+2, 1):

and Great Apple has:
for iXLoop in range(iPlotX - 2, iPlotX + 3):
for iYLoop in range(iPlotY - 2, iPlotY + 3):

Both seem to go farther to the right than to the left.

Roger Bacon

I've been programming for over a decade and using python for a few years, so I apologize if this gets too technical.

The reason it does this is because the following code snippet:

Code:
for i in range([B]start[/B], [B]end[/B]):
  ...blah...

Will actually end up looking like this: (pseudocode)

Code:
set i to [B]start[/B]
run the code in ...blah...
check if i is [COLOR="Red"]less than[/COLOR] [B]end[/B]
if it is, add [B]step[/B] to i (default step is 1)
go back up to run the code in ...blah...
...etc...

The reason it does this is that most things in computer programming are on scales from 0 to n-1 where n is the number of values.

So... if you want to check 4 values, you would usually check the values 0, 1, 2, 3 (still four values, just starting with 0 instead of 1)

If you're interested in the why's of the 0-index system, do some google-work.

Hope this helps.
 
vbraun said:
I doubt it. Just add a +1 in the high value.

Also you don't need all 3 argumants. The only one you really need is the high one.

range(10) will return:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

You need all three when you aren't starting at 0.
 
I made my first array to make some of my increasingly complex checks easier (and to keep them standard). This is what I added to cvEventManager.py:

Up in the top of the file, before the globals are defined:

Code:
livingUnitCombats =	[	gc.getInfoTypeForString('UNITCOMBAT_ARCHER'),
				gc.getInfoTypeForString('UNITCOMBAT_MELEE'),
				gc.getInfoTypeForString('UNITCOMBAT_RECON'),
				gc.getInfoTypeForString('UNITCOMBAT_DWARF'),
				gc.getInfoTypeForString('UNITCOMBAT_ELF'),
				gc.getInfoTypeForString('UNITCOMBAT_ADEPT'),
				gc.getInfoTypeForString('UNITCOMBAT_DISCIPLE'),
				gc.getInfoTypeForString('UNITCOMBAT_MOUNTED')	]

Then in any of the functions I just do the following:

Code:
		if pPlayer.getCivics(gc.getInfoTypeForString('CIVICOPTION_LABOR')) == gc.getInfoTypeForString('CIVIC_SLAVERY'):
[b]			if pLoser.getUnitCombatType() in livingUnitCombats:[/b]
				iRnd = CyGame().getSorenRandNum(100, "Bob")
				if iRnd <= 25:
					CyInterface().addMessage(pWinner.getOwner(),True,25,'Slave captured.','AS2D_DISCOVERBONUS',1,'Art/Interface/Buttons/Units/Slave.dds',ColorTypes(8),pWinner.getX(),pWinner.getY(),True,True)
					newUnit = pPlayer.initUnit(gc.getInfoTypeForString('UNIT_SLAVE'), pWinner.getX(), pWinner.getY(), UnitAITypes.NO_UNITAI)

It seems to work out pretty well and keeps me from having to go back and change all my checks whenever I add a new unitcombat. Instead I just add it to the appropriate arrays and the checks start working with it on their own.
 
Code:
# Returns the distance between city A and city B
def getCityDistance(self, objCityA, objCityB):
[TAB]' int - the distance between objCityA and objCityB'
[TAB]return self.getPlotDistance(objCityA.plot(), objCityB.plot())
[TAB][TAB]
[TAB][TAB]
# Calculates the distance between two plots using the formula:
# MAX((X2-X1), (Y2-Y1))
def getPlotDistance(self, objPlotA, objPlotB):
[TAB]' iDist - the distance between objPlotA and objPlotB'
[TAB]dX = abs(objPlotB.getX() - objPlotA.getX())
[TAB]dY = abs(objPlotB.getY() - objPlotA.getY())
[TAB]dist = max(dX,dY)

[TAB]# if you want it in one line to avoid variable assignments: 
[TAB]# return max(abs(objPlotB.getX()-objPlotA.getX()), abs(objPlotB.getY()-objPlotA.getY()))
[TAB]return int(dist)


What it does: Gets the distance between two plots or two cities


On another note, please include comments in your example codes in your posts, it will make it easier for new modders to understand your code. (I have made an effort to do this within the mercenaries mod.)

EDIT: Updated to use 12monkeys code instead
 
TheLopez said:
Code:
# Returns the distance between city A and city B
def getCityDistance(self, objCityA, objCityB):
     ' int - the distance between objCityA and objCityB'
     return self.getPlotDistance(objCityA.plot(), objCityB.plot())
          
          
# Calculates the distance between two plots using the formula:
# |   ______________________  | = length of shortest route 
# |_ V (X2-X1)^2 + (Y2-Y1)^2 _|   between objPlotA and objPlotB
#
def getPlotDistance(self, objPlotA, objPlotB):
     ' iDist - the distance between objPlotA and objPlotB'
     dX = pow(objPlotB.getX()-objPlotA.getX(),2)
     dY = pow(objPlotB.getY()-objPlotA.getY(),2)
     dist = math.floor(math.sqrt(dX+dY))

     # if you want it in one line to avoid variable assignments: 
     # return floor(math.sqrt(pow(objPlotB.getX()-objPlotA.getX(),2)+pow(objPlotB.getY()-objPlotA.getY(),2)))
     return int(dist)


What it does: Gets the distance between two plots or two cities


On another note, please include comments in your example codes in your posts, it will make it easier for new modders to understand your code. (I have made an effort to do this within the mercenaries mod.)

If you want to calculate the theoretical distance between two plots by pythagoras than this method is OK. But it will be of no use, if you want to calculate the distance in plots to move.

Example : you're target plot is away 4 x-plots and and 3 y-plots. Using your function the distance is 5 plot. In fact the distance is 4 plots, because it doesn't matter if you're moveing to straight or diagonal.

So the correct calculation for the distance should be :

Code:
dist = max(abs(FromPlot.getX()-ToPlot.getX()), abs(FromPlot.getY()-ToPlot.getY()))
 
12monkeys said:
If you want to calculate the theoretical distance between two plots by pythagoras than this method is OK. But it will be of no use, if you want to calculate the distance in plots to move.

Example : you're target plot is away 4 x-plots and and 3 y-plots. Using your function the distance is 5 plot. In fact the distance is 4 plots, because it doesn't matter if you're moveing to straight or diagonal.

So the correct calculation for the distance should be :

Code:
dist = max(abs(FromPlot.getX()-ToPlot.getX()), abs(FromPlot.getY()-ToPlot.getY()))

Hmmm.... good point, fixing my post :), thanks 12monkeys
 
Please keep posting stuff here. The tutorials have taught me the basics of python and how to do things like merge existing mods, but they dont actually help you learn how to create new stuff.

These examples are more useful than any of the tutorials. Please post many more when you get the chance.
 
Ok, will do. I'll put up my cannotTrain function from the CvGameInterface.py file:

Code:
def cannotTrain(argsList):
	pCity = argsList[0]
	eUnit = argsList[1]
	bContinue = argsList[2]
	bTestVisible = argsList[3]
	ePlayer = pCity.getOwner()
	pPlayer = gc.getPlayer(ePlayer)

	if (not pPlayer.isHuman() and eUnit == gc.getInfoTypeForString('UNIT_WARRIOR')):
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_BARRACKS'), False, False, False):
			return True
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_ARCHERY_RANGE'), False, False, False):
			return True

	if (not pPlayer.isHuman() and eUnit == gc.getInfoTypeForString('UNIT_SCOUT')):
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_HUNTING_LODGE'), False, False, False):
			return True
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_STABLES'), False, False, False):
			return True

	if eUnit == gc.getInfoTypeForString('UNIT_EIDOLON'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ORDER'):
			return True
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_RUNES_OF_KILMORPH'):
			return True

	if eUnit == gc.getInfoTypeForString('UNIT_PALADIN'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL'):
			return True
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_OCTOPUS_OVERLORDS'):
			return True

	if eUnit == gc.getInfoTypeForString('UNIT_HIGH_PRIEST'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL'):
			return True

	if eUnit == gc.getInfoTypeForString('UNIT_DEMON_SUMMONER'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ORDER'):
			return True

	if eUnit == gc.getInfoTypeForString('UNIT_ROYAL_GUARD'):
		if pPlayer.getCivics(gc.getInfoTypeForString('CIVICOPTION_GOVERNMENT')) != gc.getInfoTypeForString('CIVIC_HEREDITARY_RULE'):
			return True

	if eUnit == gc.getInfoTypeForString('UNIT_BEAST_OF_AGARES'):
		if pCity.getPopulation() <= 5:
			return True

	return False

What it does: cannotTrain is run through everytime the game gives you the option to build units. If the function returns True then you "cannotTrain" the given eUnit. Returning False means you can train the unit.

This function is very valuable because it allows you to tie the ability to build units to virtualy any variable that exists in the game. There are similiar functions for researching Techs and Buildings too.

Use:

Lets go through the functions one by one. The first stops the AI from being able to build Warriors if they are able to build Barracks or Archery Ranges. It does not effect human players and I needed it to keep the AI from building lower class units when it should have been upgrading its cities to be able to produce the higher class units:

Code:
	if (not pPlayer.isHuman() and eUnit == gc.getInfoTypeForString('UNIT_WARRIOR')):
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_BARRACKS'), False, False, False):
			return True
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_ARCHERY_RANGE'), False, False, False):
			return True

The next does exactly the same thing for Scouts:

Code:
	if (not pPlayer.isHuman() and eUnit == gc.getInfoTypeForString('UNIT_SCOUT')):
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_HUNTING_LODGE'), False, False, False):
			return True
		if pPlayer.canConstruct(gc.getInfoTypeForString('BUILDING_STABLES'), False, False, False):
			return True

The following check doesn't allow the player to build Eidolon's if they have either The Order or Runes of Kilmorph as their state religion. Notice that you can already require a religion for a unit in the XML but by using this function you can get much more grnaular with it (in this case allowing the unit to be built by 3 of the 5 religions in the game).

Code:
	if eUnit == gc.getInfoTypeForString('UNIT_EIDOLON'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ORDER'):
			return True
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_RUNES_OF_KILMORPH'):
			return True


The next does the same for Paladins, it keeps them by being built if the players State religion if the Ashen Veil or Octopus Overlords.

Code:
	if eUnit == gc.getInfoTypeForString('UNIT_PALADIN'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL'):
			return True
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_OCTOPUS_OVERLORDS'):
			return True

A single block that keeps civs with the Ashen Veil state religion from building High Priests.

Code:
	if eUnit == gc.getInfoTypeForString('UNIT_HIGH_PRIEST'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL'):
			return True

A single block that keeps civs with the Order state religion from building Demon Summoners.

Code:
	if eUnit == gc.getInfoTypeForString('UNIT_DEMON_SUMMONER'):
		if pPlayer.getStateReligion() == gc.getInfoTypeForString('RELIGION_THE_ORDER'):
			return True

The following doesn't allow the player to build royal Guard units unless their Government civic is set to Hereditary Rule. Notice that I couldn't make it "if the players has Hereditary Rule return False" because if the players civic was anything else it would have kept going through the cannotTrain function and eventually returned False anyway. I have to trap on anything that isn't allowed, not the things that are.

Code:
	if eUnit == gc.getInfoTypeForString('UNIT_ROYAL_GUARD'):
		if pPlayer.getCivics(gc.getInfoTypeForString('CIVICOPTION_GOVERNMENT')) != gc.getInfoTypeForString('CIVIC_HEREDITARY_RULE'):
			return True

The last one just requires that the building city be a certain size before the unit is allowed to be built. I do this because when the unit comes into play it kills 4 population in the city.

Code:
	if eUnit == gc.getInfoTypeForString('UNIT_BEAST_OF_AGARES'):
		if pCity.getPopulation() <= 5:
			return True

Thats everything Im blocking on, but by no means is it the limit to which you can block on things. Have fun!
 
Thanks Kael. This kind of stuff helps immensely. The hardest part about learning python I think is that its hard to create something totally new unless you've seen some examples of something similiar first, then you can understand how some of the strings and functions are suppose to operate.

I know your busy with FfH but check back here a few times a month if you can and post more if possible. Same for anyone else.
 
Top Bottom