Attitude class

Dresden

Emperor
Joined
Jul 10, 2008
Messages
1,081
Splitting off from the Foreign Advisor info topic, here's a quick version of the Attitude class proposed by EF that I wrote up today before work. It has been very minimally tested so expect bugginess. However, since EF usually has a dozen suggestions to make it better, I figured I should post early.

Spoiler :
Code:
# Implementing the new Attitude class as discussed in 
# http://forums.civfanatics.com/showpost.php?p=7176404&postcount=24

# Build the hardcoded part of the attitude modifier dict now
MODIFIER_KEYS = (
	"TXT_KEY_MISC_ATTITUDE_LAND_TARGET",
	"TXT_KEY_MISC_ATTITUDE_WAR",
	"TXT_KEY_MISC_ATTITUDE_PEACE",
	"TXT_KEY_MISC_ATTITUDE_SAME_RELIGION",
	"TXT_KEY_MISC_ATTITUDE_DIFFERENT_RELIGION",
	"TXT_KEY_MISC_ATTITUDE_BONUS_TRADE",
	"TXT_KEY_MISC_ATTITUDE_OPEN_BORDERS",
	"TXT_KEY_MISC_ATTITUDE_DEFENSIVE_PACT",
	"TXT_KEY_MISC_ATTITUDE_RIVAL_DEFENSIVE_PACT",
	"TXT_KEY_MISC_ATTITUDE_RIVAL_VASSAL",
	"TXT_KEY_MISC_ATTITUDE_SHARE_WAR",
	"TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC",
	"TXT_KEY_MISC_ATTITUDE_TRADE",
	"TXT_KEY_MISC_ATTITUDE_RIVAL_TRADE",
	"TXT_KEY_MISC_ATTITUDE_FREEDOM",
	"TXT_KEY_MISC_ATTITUDE_EXTRA_GOOD",
	"TXT_KEY_MISC_ATTITUDE_EXTRA_BAD",
)
MODIFIER_STRING_TO_KEY = {}
for sKey in MODIFIER_KEYS:
	sStr = BugUtil.getPlainText(sKey)
	if (sStr):
		# These modifier strings contain the '%d: ' prefix so we
		# need to extract just the portion in quotes.
		pMatch = re.match("^.*(\".+\")", sStr)
		if (pMatch):
			MODIFIER_STRING_TO_KEY[pMatch.group(1)] = sKey
MEMORY_TYPES_ADDED = False
#BugUtil.debug(str(MODIFIER_STRING_TO_KEY))

# Adding the memory-type modifier keys must be delayed because of
# "unidentifiable C++ exceptions" when this is done on module load.
def addMemoryTypeModifiers():
	global MEMORY_TYPES_ADDED
	if (not MEMORY_TYPES_ADDED):
		for iMemType in range(MemoryTypes.NUM_MEMORY_TYPES):
			sKey = str(gc.getMemoryInfo(iMemType).getTextKey())
			sStr = BugUtil.getPlainText(sKey)
			MODIFIER_STRING_TO_KEY[sStr] = sKey
		MEMORY_TYPES_ADDED = True
	#BugUtil.debug(str(MODIFIER_STRING_TO_KEY))

class Attitude:
	""" Holds summary of attitude that this player has toward target player.
		If the two IDs are the same or the two players have not met, the
		class will be filled with generic default values. We also check to see
		that the active player has met both players, and the accessor functions
		will react differently depending on if this is the case. """
	def __init__(self, iThisPlayer, iTargetPlayer):
		pActiveTeam = gc.getTeam(gc.getActivePlayer().getTeam())
		iThisTeam = gc.getPlayer(iThisPlayer).getTeam()
		pThisTeam = gc.getTeam(iThisTeam)
		iTargetTeam = gc.getPlayer(iTargetPlayer).getTeam()
		self.iThisPlayer = iThisPlayer
		self.iTargetPlayer = iTargetPlayer
		self.iAttitudeSum = 0
		self.iAttitudeModifiers = {}
		self.bHasActiveMetBoth = CyGame().isDebugMode() or (pActiveTeam.isHasMet(iThisTeam) and pActiveTeam.isHasMet(iTargetTeam)) 
		self.eAttitudeType = AttitudeTypes.NO_ATTITUDE
		# This might be better off being something descriptive such as
		# "players have not met" or "players are the same"
		self.sAttitudeString = ""
		if (iThisPlayer != iTargetPlayer and pThisTeam.isHasMet(iTargetTeam)):
			self.eAttitudeType = gc.getPlayer(iThisPlayer).AI_getAttitude(iTargetPlayer)
			self.sAttitudeString = CyGameTextMgr().getAttitudeString(iThisPlayer, iTargetPlayer)
			addMemoryTypeModifiers()
			for sLine in self.sAttitudeString.split("\n"):
				pMatch = re.match("^.*>([-\+]\d+): (\".+\")<.*$", sLine)
				if (pMatch):
					iValue = int(pMatch.group(1))
					sString = pMatch.group(2)
					if sString in MODIFIER_STRING_TO_KEY:
						self.iAttitudeModifiers[MODIFIER_STRING_TO_KEY[sString]] = iValue
						self.iAttitudeSum = self.iAttitudeSum + iValue
		self.dump()

	def dump(self):
		""" Dumps string version of internal data to debug function """
		BugUtil.debug("""Dumping Attitde:
  iThisPlayer: %d
  iTargetPlayer: %d
  iAttitudeSum: %d
  eAttitudeType: %d
  bHasActiveMetBoth: %s
  iAttitudeModifiers: %s """ % (self.iThisPlayer, self.iTargetPlayer, self.iAttitudeSum, self.eAttitudeType,
								self.bHasActiveMetBoth, str(self.iAttitudeModifiers)))

	def hasModifier(self, sKey):
		""" Does the attitude contain given modifier? """
		if self.bHasActiveMetBoth:
			return (sKey in self.iAttitudeModifiers)
		return False

	def getModifier(self, sKey):
		""" Returns integer value of given attitude modifer. """
		if self.bHasActiveMetBoth:
			if sKey in self.iAttitudeModifiers:
				return self.iAttitudeModifiers[sKey]
		return 0

	def getTotal(self):
		""" Returns total (visible) attitude value. """
		if self.bHasActiveMetBoth:
			return self.iAttitudeSum
		return 0

	def getAttitude(self):
		""" Returns attitude type. """
		if self.bHasActiveMetBoth:
			return self.eAttitudeType
		return AttitudeTypes.NO_ATTITUDE

	def getFullText(self):
		""" Returns full diplomacy text string. """
		if self.bHasActiveMetBoth:
			return self.sAttitudeString
		return ""

	def getIcon(self):
		""" Returns smilie icon string based on attitude type. """
		if self.bHasActiveMetBoth:
			return unichr(ord(unichr(CyGame().getSymbolID(FontSymbols.POWER_CHAR) + 4)) + self.eAttitudeType)
		return ""

Not all of the current AttitudeUtils functions are implemented; for now I wanted to get enough to continue working on the advisor info screen while getting feedback on how to improve this.

You may notice the dump() method. It's something I often do when developing complex data structures to easily peek at the values. Some samples:
Spoiler :
Code:
Dumping Attitde:
  iThisPlayer: 8
  iTargetPlayer: 6
  iAttitudeSum: -11
  eAttitudeType: 1
  bHasActiveMetBoth: True
  iAttitudeModifiers: {'TXT_KEY_MISC_ATTITUDE_LAND_TARGET': -1, 'TXT_KEY_MISC_ATTITUDE_WAR': -3, 'TXT_KEY_MEMORY_SPY_CAUGHT': -2, 'TXT_KEY_MEMORY_DECLARED_WAR_ON_FRIEND': -5} 
...
Dumping Attitde:
  iThisPlayer: 8
  iTargetPlayer: 2
  iAttitudeSum: 1
  eAttitudeType: 3
  bHasActiveMetBoth: True
  iAttitudeModifiers: {'TXT_KEY_MISC_ATTITUDE_OPEN_BORDERS': 2, 'TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC': 3, 'TXT_KEY_MISC_ATTITUDE_PEACE': 1, 'TXT_KEY_MISC_ATTITUDE_BONUS_TRADE': 1, 'TXT_KEY_MISC_ATTITUDE_RIVAL_TRADE': -4, 'TXT_KEY_MISC_ATTITUDE_RIVAL_VASSAL': -1, 'TXT_KEY_MEMORY_DECLARED_WAR_ON_FRIEND': -1} 
...
Dumping Attitde:
  iThisPlayer: 1
  iTargetPlayer: 0
  iAttitudeSum: 19
  eAttitudeType: 4
  bHasActiveMetBoth: True
  iAttitudeModifiers: {'TXT_KEY_MISC_ATTITUDE_DEFENSIVE_PACT': 2, 'TXT_KEY_MISC_ATTITUDE_SAME_RELIGION': 6, 'TXT_KEY_MISC_ATTITUDE_SHARE_WAR': 4, 'TXT_KEY_MISC_ATTITUDE_OPEN_BORDERS': 2, 'TXT_KEY_MEMORY_DECLARED_WAR': -3, 'TXT_KEY_MISC_ATTITUDE_TRADE': 4, 'TXT_KEY_MISC_ATTITUDE_PEACE': 1, 'TXT_KEY_MEMORY_GIVE_HELP': 1, 'TXT_KEY_MEMORY_TRADED_TECH_TO_US': 1, 'TXT_KEY_MEMORY_LIBERATED_CITIES': 1, 'TXT_KEY_MISC_ATTITUDE_BONUS_TRADE': 2, 'TXT_KEY_MEMORY_DECLARED_WAR_ON_FRIEND': -2}
 
That looks terrific. :goodjob:

Do the XML strings for the memory-based modifiers omit the "%d: " prefix? If not, make sure to strip it the same way you did for the others.

When this gets ported to the new core (I might commit it tonight), initialization will be easier, and you'll be able to reinitialize when the user changes their language. Currently, changing language will break this class (no modifiers will be recognized).

Random Python style nit-pickery: :mischief:

The standard way to do dump() in Python is to define the special functions __repr__ and/or __str__. The latter is called whenever you do "str(x)", and the former is called when you use the "%r" substitution in a string. This is typically used as a debug representation, while str() is used to get something for output to the end user.

Feel free to use it or not. I haven't used it much as I just learned about it a couple weeks ago. It also works better for classes with less information than this one has, but the end result is the same. The nice thing about __repr__ is that you can decide outside the class where to dump the result.

Please format Function/class docs without indentation, as per my understanding of the Python style guide.

Code:
def function(...):
    """One-sentence summary with a period.
    
    Longer documentation, special notes on parameters, return value, etc.
    """ # put closing quotes on their own line if not a 1-liner.
    for x in ...

It looks a little funky with the first sentence immediately after the """ instead of on a new line, but it makes it easier to switch a one-line docstring into a multiline one, and putting the closing """ on its own line makes it easier to add to a docstring.

I kinda breezed over it, so please correct me if I'm wrong. BTW, I love that you included docstrings!
 
Do the XML strings for the memory-based modifiers omit the "%d: " prefix? If not, make sure to strip it the same way you did for the others.
Yeah, there's a special extra XML called TXT_KEY_MISC_ATTITUDE_MEMORY which looks something like this: "%d: %s" and then the individual memory text gets thrown in for that %s so those only contain the quote string. At first I didn't realize the non-memory ones had that format and so the parser was missing those entirely....

When this gets ported to the new core (I might commit it tonight), initialization will be easier, and you'll be able to reinitialize when the user changes their language. Currently, changing language will break this class (no modifiers will be recognized).
Cool. Hadn't even thought of that but yeah, it'd break in its current form.

The standard way to do dump() in Python is to define the special functions __repr__ and/or __str__. The latter is called whenever you do "str(x)", and the former is called when you use the "%r" substitution in a string. This is typically used as a debug representation, while str() is used to get something for output to the end user.

Feel free to use it or not. I haven't used it much as I just learned about it a couple weeks ago. It also works better for classes with less information than this one has, but the end result is the same. The nice thing about __repr__ is that you can decide outside the class where to dump the result.
Interesting. I'll definitely consider one of those; I also need to add the full attitude string to the dump output anyway since I forgot that.

Please format Function/class docs without indentation, as per my understanding of the Python style guide.

Code:
def function(...):
    """One-sentence summary with a period.
    
    Longer documentation, special notes on parameters, return value, etc.
    """ # put closing quotes on their own line if not a 1-liner.
    for x in ...

It looks a little funky with the first sentence immediately after the """ instead of on a new line, but it makes it easier to switch a one-line docstring into a multiline one, and putting the closing """ on its own line makes it easier to add to a docstring.

I kinda breezed over it, so please correct me if I'm wrong. BTW, I love that you included docstrings!
That sounds reasonable. I figured there was probably some style guideline since my indentations looked a little weird but hadn't bothered looking them up. Being someone who liberally comments his code, I kinda naturally took to the docstring concept. :D

As an aside, there's a bug in the current getAttitudeCount function that will mistakenly interpret some leadernames as attitude modifiers. The players in the UNGY-04 SG were wondering why the (non-BUG) glance screen was showing a +7 instead of +11 and when I looked into it, I saw that the line "Pleased with UNGY-04" was resulting in a -4 attitude modifier being counted in. Doing the count at the same time we are pulling out the modifier strings avoids that issue although I should figure out how to fix the other if I do another Unofficial Patch release. EDIT: This has apparently be known for some time but it was new to me :p
 
As an aside, there's a bug in the current getAttitudeCount function that will mistakenly interpret some leadernames as attitude modifiers.

I fixed this while working on the core by adding a colon which cannot be put into a neader's name. The comment I had put in before was to split the string into lines and then make sure the number starts each line, but "^" didn't work in the regular expression.

Code:
ltPlusAndMinuses = re.findall ("[-+][0-9]+: ", sAttStr)
 
Cool. My first thought was to add a colon to the re.findall pattern and then strip the colon when converting to ints, but then I was worried that the leader name could still be picked up if it was really crazy. Knowing that a leader name can't have a colon alleviates that concern.
 
Question regarding the AttitudeUtils from the newest SVN update. I figured it fit better here than in the other svn/core topics.

I noticed there are now attitude constants such as AttitudeUtils.FURIOUS; is there a good reason to use those instead of the built-in defines such as AttitudeTypes.ATTITUDE_FURIOUS?
 
I noticed there are now attitude constants such as AttitudeUtils.FURIOUS; is there a good reason to use those instead of the built-in defines such as AttitudeTypes.ATTITUDE_FURIOUS?

Good question. When I saw your previous posting of the attitude class, I noticed the AttitudeTypes and checked the file. I recall seeing my constants and coming up with a good reason for them, but I can't remember what that reason is now. They aren't being used, so go ahead and ditch them.

At some point in the future, we should probably move the hard-coded TXT keys to init.xml for modders, but let's worry about that later. :)
 
Okay, I'm finally back on this as it's step one of the new info screen. The current plan is to finish up the Attitude class but leave the other functions intact. Any calls to redundant functions could then be transitioned over afterwards.

When this gets ported to the new core (I might commit it tonight), initialization will be easier, and you'll be able to reinitialize when the user changes their language. Currently, changing language will break this class (no modifiers will be recognized).
How does one reinitialize on language change? Is the init() method for the module automatically called or is there an event that needs to be hooked?

Also, I'm guessing that the function that builds the modifier string -> key dictionary should be called from the module's init() function, yes?
 
How does one reinitialize on language change? Is the init() method for the module automatically called or is there an event that needs to be hooked?

Also, I'm guessing that the function that builds the modifier string -> key dictionary should be called from the module's init() function, yes?

Follow what ColorUtils does as it has the same needs.

  1. The user is allowed to specify their own list of colors in init.xml to override the list in ColorUtils.
  2. It cannot access the color values when the module is loaded.
  3. It needs to rebuild its maps when the user changes the language.
Code:
[B]<!-- #1, 2 -->[/B]
<init module="ColorUtil" function="init">
	<arg name="colors" type="tuple">
		"COLOR_RED",
		...
	</arg>
</init>
[B]<!-- #3 -->[/B]
<event type="LanguageChanged" module="ColorUtil" function="createColors"/>

For 1, init(colors) accepts a list of color XML keys. It then calls createColors() to build the maps.

For 2, init.xml has an <init>. This is where the user can override the colors.

For 3, init.xml has an <event> for "LanguageChanged" (defined by BUG).

Hmm, looks like I did 1 and 2 already. Did I sleep? Was I awake? ;) So for 3 you'll need to split out the creation of the maps into a separate function (like createColors() in ColorUtil) and add an event to init.xml to call it. Also, init() should call it, again just like CU.

I remembered the reason for the attitude constants. AttitudeTypes.ATTITUDE_FOO is an object. It can be cast to an int, but I'm not sure how dependable that is when used in a map. When used as an array index in []s, it must be converted to an int. However, if any of the internal data structures in AttitudeUtils use maps (dictionaries) instead, I think they might cause problems.

Again, they aren't being used yet, so this is probably a moot point. It's probably best to use the Civ enum constants with maps and avoid ints and lists entirely.
 
Thanks. Following the ColorUtil example seems to work great.

What should the default Attitude Category be for when people haven't met and such. I had been using AttitudeTypes.NO_ATTITUDE (-1) but I was thinking AttitudeTypes.CAUTIOUS has its advantages too.
 
What should the default Attitude Category be for when people haven't met and such.

I think -1 (NO_ATTITUDE) is best because it allows the caller to detect that they've made an error. AIs that have not met the player should have no attitude toward them. If the caller doesn't detect this on their own before calling (and thus not calling), then at least they can check the return value.

In fact, it allows callers to ignore checking first and simply calling the function and checking for NO_ATTITUDE upon return. The text function could return "" as well. It's nice if you design your API to make it easy to deal with special cases, especially when you can make them magically disappear in a way. :)
 
I fixed this while working on the core by adding a colon which cannot be put into a neader's name. The comment I had put in before was to split the string into lines and then make sure the number starts each line, but "^" didn't work in the regular expression.

Code:
ltPlusAndMinuses = re.findall ("[-+][0-9]+: ", sAttStr)
Revisiting this, the above regexp breaks in French because for some reason all the French diplo text has a space between the number and the colon; it's the only language that does that. Dunno why, but it caught me on my attitude parsing; I was thinking there was some kind of complex unicode problem and trying all kinds of weird stuff and it turned out to be a damned extra space. :rolleyes:
Code:
ltPlusAndMinuses = re.findall ("[-+][0-9]+\s?: ", sAttStr)
seems to work and the presence of the extra space doesn't bother the int() conversion. Hopefully the attitude utils will be submittable tomorrow or Monday and I'll make the change then if necessary.
 
For some reason all the French diplo text has a space between the number and the colon.

Bah, I've noticed this about all their other text but didn't think of it here. Good catch. Leave it to the French! I wonder if it was the translators who did this out of personal preference or if the "rules of style" for French writers puts a space before all colons.
 
Top Bottom