Foreign Advisor Info Screen

Dresden

Emperor
Joined
Jul 10, 2008
Messages
1,081
This topic is about improving the Foreign Advisor's Info screen. Original discussion occurred in the Bug Reporting topic but it has grown enough that it needs its own place. I suppose I should get a mod to actually split the posts, but instead I'm probably just going to quote stuff.

Current Suggestions for improvement:
  • If playing with Random Personalities option on, Favorite Civics should be originally unlisted. This prevents both erroneous information (the current situation) and spoiler information (the easy fix to the current situation). As the game progresses, we can scan diplomacy text for the favorite civic modifier and eventually figure out what most people's favorites are, displaying possibilities along the way.
  • The active player's info can be placed at the top of the list so you can compare what civics you are running with everyone else's without having to look at F3 (or remember :p)
  • Some form of highlighting can be added so that if you share religion or civics with a given AI it is marked on the screen. Also if you are running an AI's known favorite civic.
  • The trade route info can be expanded so that situations that prevent trade are differentiated from simply not having any. These situations include: war, not having open borders, civic limitations (e.g. one or both of you run mercantilism), map limitations (e.g. no road), tech limitations (e.g. no Astronomy). The more information we can easily apply the better.
  • Cosmetic changes to pretty it up. We probably want to include a true header row at the top (like with the tech trade screen) and ensure that everything lines up properly. IconGrid is an option but since most stuff will be a set size it may be unnecessary.

Thanks to EF's help & guidance I've been coding this up for submission; progress will be posted here and any suggestions are welcome.
 
Quoting previous development; some editing done.

The Foreign Advisor uses the LeaderType to display the favorite civic on the Info tab. However, if you are playing with random personalities, the true favorite civic would come from the PersonalityType. So in this case it's usually displaying the wrong favorite civic.
...
Although a reasonable alternative would be to check for the Random Personalities option (assuming you can) and put in a placeholder like a big question mark icon or simply not display the favorite civic then.

I like this. It is certainly better than displaying false information as happens now.

Even better would be to change it to the actual civic once the user has seen the information. For example, if you switch to Bureaucracy and suddenly get a +1 diplomatic bonus from Monty, you know that's his favorite civic (and will be all game). Unfortunately, the game doesn't give any sort of event for this.

We could check the diplo modifiers every time the player changes civics (and the AIs too, if my memory is correct that they must be running it as well to get the modifier). This would be quite easy, save for detecting the actual diplo modifier entry given that they aren't available programmatically.

Attached is EDIT: Committed by EF was a foreign advisor (based on rev 1122) which refuses to display the favorite civic if random personalities is on; a similar thing should probably be done for the standard advisor in the unofficial patch. That will take care of removing erroneous information while further discussion on enhancements continues below. ;)

Detecting the modifier isn't too hard if we can make the assumption that everything between the quotes of the defined string will be listed in the diplo summary. For example I ran the following check in a BeginPlayerTurn handler on an in-progress game where I shared the favorite civic of 3 AIs (Hannibal, Boudica, and Suryalphabet) and did not share it with 5 others.
Spoiler :
Code:
                    CvUtil.pyPrint(" -- Testing Against Player %d - %s" %(iOtherPlayer, pOtherPlayer.getName()))
                    szDiplo = CyGameTextMgr().getAttitudeString(iOtherPlayer, iPlayer)
                    pMatch = re.match("^.*(\".+\")", localText.getText("TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC",(0,)))
                    for szMatch in pMatch.groups():
                        if szMatch in szDiplo:
                            CyInterface().addMessage(iPlayer, False, gc.getEVENT_MESSAGE_TIME(), "You are running the favorite civic of %s" %(pOtherPlayer.getName()), None, InterfaceMessageTypes.MESSAGE_TYPE_INFO, None, gc.getInfoTypeForString("COLOR_BLUE"),0,0,False,False)
                            CvUtil.pyPrint("      YES!")
                        else:
                            CvUtil.pyPrint("      NO!")
I'm using a regexp to extract the part of the modifier text that's between the quotes and then running a substring test between that and the full diplo string. And the results:
Spoiler :
Code:
PY:DEBUG Running Favorite Civic Test for Player 0 - SpreadCultureTesting
PY: -- Testing Against Player 1 - Huayna Capac
PY:      NO!
PY: -- Testing Against Player 2 - Boudica
PY:      YES!
PY: -- Testing Against Player 3 - Hammurabi
PY:      NO!
PY: -- Testing Against Player 4 - Elizabeth
PY:      NO!
PY: -- Testing Against Player 5 - Zara Yaqob
PY:      NO!
PY: -- Testing Against Player 6 - Ragnar
PY:      NO!
PY: -- Testing Against Player 7 - Suryavarman II
PY:      YES!
PY: -- Testing Against Player 8 - Hannibal
PY:      YES!
PY: -- Testing Against Player 18 - Barbarian
PY:      NO!
If that modifier appears after a civics change we then have to see which civics match (you are right that both must run it) and assume that the favorite is one of those. If more than one matches, further civics changes must be monitored to whittle it down but we could probably list all the choices as "possibles" until then.

This would make an excellent addition to AttitudeUtils.py:

Code:
def hasModifier(attitudeText, xmlKey) -> boolean

if AttitudeUtils.hasModifier(szDiplo, "TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC"):
    ...

And I bet we could find good uses for one that splits the text apart into all of the modifiers:

Code:
def getModifiers(attitudeText) -> dict { string xmlKey: int modifier }

Assuming the attitude modifier doesn't appear immediately, you could track all civics switches over time . . . hmm, actually you couldn't really. Say you switch from A to B, 10 turns later from X to Y (different categories), and 10 turns later you get a modifier. Is B or Y their favorite civic?

In any case, you're right that we can narrow it down to a list of possibles. I believe the modifier disappears immediately upon a switch away, so that would help too. Also, will the AI ever ask you to switch to a non-favorite civic, or can we assume that a request to switch civics will always be to a favorite?
...
On a slightly related note, I had originally added a feature to the EFA for my personal copy of Ruff's mod (may the hard drive that held it rest in peace): each civic that you shared in common with an AI was highlighted (just need to call setSelected() (IIRC) on the GFC checkbox. I also added the player as the first row on that INFO page.

Also, I put a * next to an AI's religion if you shared it and a * next to their favorite civic if you shared that.

Finally, for trade I hid the 0 if you could not possibly trade with them (not connected or at war). Now that we have a war icon, we could even add the reason instead of simply hiding the 0 value.

Dreseden said:
So how do you store and retrieve data to keep track of this stuff?

EmperorFool said:
SdToolkit, already included in BUG for UnitNaming and Reminders. You can register a "mod" key (e.g. "FavoriteCivics") with SdToolkit and then attach any structured data you want (from a simple int to a complex class with lists and dictionaries and other data types).

We'd need to track the set of possible favorite civics for each AI as well as the history of civics changes for the player and any AIs for whom we are unsure of their favorite.

You can start by skipping the saving of data and just code up the checks. Go ahead and check each turn as well for the modifier change as I think it's not immediate. I do know that it grows over time, but if it turns out to show up immediately, this gets a whole lot easier. :)

I'm pretty sure the attitude modifiers appear and disappear immediately. When I was testing to verify that the favorite civics were not what was shown when running Random Personalities, I was using CyPlayer::setCivics in the Python Console to change myself back & forth between OR and Theocracy and then hovering over the scoreboard names for the other leaders. +1s were appearing and disappearing right away.

And I'm fairly certain that they will only ask you to switch to their favorite civic. At least, I've never had one ask me otherwise.

Small bummer with the favorite civic attitude change detection: there is no Civ event when a player changes civics. A message is displayed on-screen, but no Python event is triggered. :(

Looks like you'll have to stick to checking each turn (BeginActivePlayerTurn seems like a good spot). You can check when the INFO page is drawn to catch when you've changed your own civics to a favored one.

Getting there: (links to full-size)

Still need to work in the save/load stuff with Sdtoolkit. The main question is how can we pretty it up? This screen has always been a little ugly to me and this makes it worse. :p I'm thinking a smaller text string (at least in English) than "Possible Favorite Civics" will help both in lining up with the ones we've discovered and for staying on the screen for those folks that don't play at my beloved widescreen resolution. Also, should the guys that we can't even guess at say anything like "unknown" or just remain blank?

Agreed on both points. If you wanna go nuts, you could use IconGrid to rework the entire screen. This would give you the ability to line up columns and put headers where they belong: at the top of the list.

There are two downsides to IconGrid that I'm aware of: 1) no scrollwheel :( and 2) the civic icons can't be highlighted as I had done in my other version of this screen. I don't think either of those is enough to stop from switching to it. Take a look at Ruff's Sit-Rep page for sample code and of course the EFA tech/resource trade pages. You only need to use IconGrid_BUG to get stacked bars, but it wouldn't hurt to use it anyway.
...
I'm sure we could talk NikNaks into drawing a question mark icon for us. It would be useful in the BUG MA too when you don't know the strategic advantages of an AI (rather than the current "N/A" icon).

I would suggest that you take a look at the glance screen (top panel for headings, big panel for AI civics - thus auto scroll bar). Which screens like this where the contents are largely uniform (ie always 5 icons for civics), the icongrid is not required. Using panels with different styles will give you a screen that looks much better than the current version.

@Dresden - Let's move this discussion to a new thread. Can you please start a thread about the new Info page when you next post?

Also, I found the files that had my fixes for Ruff's EFA. EDIT: RuffMod Changes.zip (86.3 KB) I seem to have found two versions, and I'm not sure how. In any case, take a look at EFA in New and From CFC, comparing them to each other and/or Old (original). The files in From CFC I found in a private message I sent to someone here that wanted the fixes. The ones in New came from somewhere else (I don't remember).

There are two main fixes, pretty well separated in the file. First is what I mentioned above: player is first Info row, hide 0 trade when cannot trade, * after same religion and * after favorite civic if player is running it.

The other is a fix to the Glance page when the active player is not player zero (scenarios). This page has changed more than the Info page and will be harder to integrate easily.

I'd love to see them all ported to BUG, but obviously do what is fun for you. :)

You can ignore the MainInterface file as it's fix is already in BUG.
 
Anyone know of a way to detect when the AI makes a demand and what the details of that demand are? The easiest way for a person playing to find out an AI's favorite civic is when the AI leader demands you adopt it, but I don't know a good way to tell programmatically when that happens.
 
At first it looked like the answer was no, but I dug a little deeper, and it turns out that we can do quite a bit. Check out Python/EntryPoints/CvDiplomacyInterface.py and Python/CvDiplomacy.py in the Civ4 assets.

The latter has a huge switch statement checking for each type of AI comment/request. I'm thinking we could create events for the various AI demands which would be very useful for Autolog

Montezuma demands that Dresden declare war on Isabella, but Dresden declines.

Code:
ePlayerAsker = self.diploScreen.getWhoTradingWith()
ePlayerAskee = gc.getGame().getActivePlayer()

### Demand ###

# If the AI is pressuring us to switch to their favorite civic
elif (self.isComment(eComment, "AI_DIPLOCOMMENT_CIVIC_PRESSURE")):

	[B][color=green]CvEventInterface.getEventManager().fireEvent("demandCivic", ePlayerAsker, ePlayerAskee, eCivic)[/color][/B]
	# We can accept their demands
	self.addUserComment("USER_DIPLOCOMMENT_REVOLUTION", -1, -1)
	# Or reject them...
	self.addUserComment("USER_DIPLOCOMMENT_NO_REVOLUTION", -1, -1)

### Response ###

# If we adopt their favorite civic
elif (self.isComment(eComment, "USER_DIPLOCOMMENT_REVOLUTION")):
	[B][color=green]CvEventInterface.getEventManager().fireEvent("acceptCivic", ePlayerAsker, ePlayerAskee, eCivic)[/color][/B]
	diploScreen.diploEvent(DiploEventTypes.DIPLOEVENT_REVOLUTION, -1, -1)
	self.setAIComment(self.getCommentID("AI_DIPLOCOMMENT_THANKS"))

# If we refuse to adopt their favorite civic
elif (self.isComment(eComment, "USER_DIPLOCOMMENT_NO_REVOLUTION")):
	[B][color=green]CvEventInterface.getEventManager().fireEvent("denyCivic", ePlayerAsker, ePlayerAskee, eCivic)[/color][/B]
	diploScreen.diploEvent(DiploEventTypes.DIPLOEVENT_NO_REVOLUTION, -1, -1)
	self.setAIComment(self.getCommentID("AI_DIPLOCOMMENT_CIVIC_DENIED"))

To avoid cluttering up the event list, we could combine the demand/accept/deny events into a single event with a parameter which specifies which of the three it is.

Bah, I knew I should have started in the smaller file. :crazyeye:

Looking at CvDiplomacyInterface.py, I see that we could make this far more general and easier to code. There are two functions for when the AI makes a request and the user clicks a response (both use a unique comment identifier for all requests and responses).

I can whip up something quick to handle the civic event so you can use it ASAP. Then I'll build out the rest of the events. Anyone want to put these into Autolog?

Also, I was thinking about what you'd need to store. I think you'll need just a single set of "possible" eCivic identifiers for each AI. At the start of every player turn you can check the diplo modifiers for each pair of players. If any has the diplo modifier, remove all civics that they aren't both running from the set of the AI running the favorite civic. Otherwise, remove all civics the AI is running from its set.

Spoiler So I don't spoil the fun :
Code:
bAnyChanged = False
allPossibles = get dict from sdtoolkit
for each AI eAI:
    if not none and alive and not human:
        possibles = allPossibles[eAI]
        for each player ePlayer:
            if not none and alive and not eAI:
                if eAI has diplo modifier toward ePlayer:
                    # running favorite
                    for each civic ePlayer is running:
                        if eAI not running civic and civic in possibles:
                            remove civic from possibles
                            bAnyChanged = True
                else:
                    # not running favorite
                    for each civic ePlayer is running:
                        if civic in possibles:
                            remove civic from possibles
                            bAnyChanged = True
if bAnyChanged:
    put allPossibles back into sdtoolkit
 
Thanks EF. That DiplomacyInterface stuff indeed looks very useful.

As for the actual tracking of changes, that part already works. :p I even tested it on MadScientist's every-AI-is-sury game and was able to determine the favorite civic of 4 out of 5 opponents from a single save. I actually thought I was done until I remembered the demand possibilites...

For sdtoolkit I use a multilevel dict. The first level is an entry for each player; the second level contains a single integer for storing the Favorite Civic (which has already become unnecessary and will probably get cut) and two lists: one for civics which are possibilities and one for civics which have been definitely ruled out.

The check goes like this, complete with ridiculous amounts of debugging print statements:

Spoiler :
Code:
def doUpdate ():
	""" Goes through the current diplomacy situation to determine potential favorite civics for each civ. """
	if isDetectionNecessary():
		global gFavoriteCivicData
		pActivePlayer = gc.getActivePlayer()
		if (not pActivePlayer) or pActivePlayer.isNone() or (not pActivePlayer.isHuman()):
			return
		for iPlayer in range(gc.getMAX_PLAYERS()):
			pPlayer = gc.getPlayer(iPlayer)
			if ( pPlayer and (not pPlayer.isNone()) and pPlayer.isAlive() 
				 and (not pPlayer.isBarbarian()) and (not pPlayer.isHuman())
				 and (gc.getTeam(pActivePlayer.getTeam()).isHasMet(pPlayer.getTeam())) ):
				CvUtil.pyPrint("Checking Player %s (ID %d)" %(pPlayer.getName(),iPlayer))
				if not isFavoriteCivicKnown(iPlayer):
					for iOtherPlayer in range(gc.getMAX_PLAYERS()):
						pOtherPlayer = gc.getPlayer(iOtherPlayer)
						if ( pOtherPlayer and (iOtherPlayer != iPlayer) and (not pOtherPlayer.isNone()) 
							 and pOtherPlayer.isAlive() and (not pOtherPlayer.isBarbarian())
							 and gc.getTeam(pActivePlayer.getTeam()).isHasMet(pOtherPlayer.getTeam()) 
							 and gc.getTeam(pPlayer.getTeam()).isHasMet(pOtherPlayer.getTeam()) ):
							CvUtil.pyPrint(" -- Testing against Player %s (ID %d)" %(pOtherPlayer.getName(),iOtherPlayer))
							bFoundPossibleFavorite = AttitudeUtils.hasModifier(iPlayer, iOtherPlayer, "TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC")
							for iCategory in range(gc.getNumCivicOptionInfos()):
								iCivic = pPlayer.getCivics(iCategory)
								if (iCivic == pOtherPlayer.getCivics(iCategory)):
									if bFoundPossibleFavorite:
										CvUtil.pyPrint("     -- Players share %s's favorite civic!" %(pPlayer.getName()))
										CvUtil.pyPrint("         -- (%d) %s: (%d) %s might be it" %(iCategory, gc.getCivicOptionInfo(iCategory).getText(), iCivic, gc.getCivicInfo(iCivic).getText()))
										addPossible(iPlayer,iCivic)
									else:
										CvUtil.pyPrint("     -- Players do not share %s's favorite civic." %(pPlayer.getName()))
										CvUtil.pyPrint("         -- (%d) %s: (%d) %s is NOT it" %(iCategory, gc.getCivicOptionInfo(iCategory).getText(), iCivic, gc.getCivicInfo(iCivic).getText()))
										addNotPossible(iPlayer,iCivic)
								else:
									if bFoundPossibleFavorite:
										CvUtil.pyPrint("     -- Players share %s's favorite civic!" %(pPlayer.getName()))
										CvUtil.pyPrint("         -- (%d) %s: (%d) %s is NOT it" %(iCategory, gc.getCivicOptionInfo(iCategory).getText(), iCivic, gc.getCivicInfo(iCivic).getText()))
										addNotPossible(iPlayer,iCivic)
				# using the direct length rather than the getNumPossibleFavoriteCivics() accessor
				iNumFavorites = len(gFavoriteCivicData[iPlayer]['iPossibleFavorites'])
				if iNumFavorites == 1:
					CvUtil.pyPrint("We found the favorite civic for %s (ID %d)!" %(pPlayer.getName(),iPlayer) )
					iCivic = gFavoriteCivicData[iPlayer]['iPossibleFavorites'][0]
					iCategory = gc.getCivicInfo(iCivic).getCivicOptionType()
					CvUtil.pyPrint("-> It's  (%d) %s in the category (%d) %s" %(iCivic, gc.getCivicInfo(iCivic).getText(), iCategory, gc.getCivicOptionInfo(iCategory).getText()))
					gFavoriteCivicData[iPlayer]['iFavoriteCivic'] = iCivic
					gFavoriteCivicData[iPlayer]['iNotFavorites'] = []
					gFavoriteCivicData[iPlayer]['iPossibleFavorites'] =  []
				elif iNumFavorites == 0:
					CvUtil.pyPrint("Don't have any possibilities at all yet!")
				else:
					CvUtil.pyPrint("Still too many possibilities.")
				dataDump(iPlayer)
		dataDump()


Your pseudo-code does show me I should be explicitly saving the data immediately if it changes though. :)
 
Your pseudo-code does show me I should be explicitly saving the data immediately if it changes though. :)

In Reminders I load the queue in the OnLoad event and save it in OnPreSave. There's no reason to save it every time it changes. It's not a huge deal, and eventually I'll rewrite SdToolkit to do the same thing for all mods, but I tend to optimize up-front since I don't have an analysis tool for Python.

A couple random notes while looking over your code. I hope you don't mind. I'm a) trying to maintain a coding standard for BUG code (this will be the start of it), b) using it as a way to go through the code so I understand it, and c) love giving coding advice without being asked. ;)

1. You only need to declare a variable as a global if you are going to assign it a new value. You can freely modify the object it points to (a list or set or some other mutable object) without declaring it global.

2. gc.getActivePlayer() always returns a human. It is the human that is sitting at the computer. I typically even skip the check for None and isNone() with it because if those fail, the game has clearly gone off into the weeds.

This is a matter of saving you typing and making code easier to read. It won't cause any problems or slowdown.

3. Python deals with "not" in if-tests well. No need to wrap "not x" in ()s. I find this easier to read as matching up ()s can be a PITA. Also, comparison operators have higher precedence than boolean operators (and or not), so you have more opportunities to trim ()s.

4. If you're going to use the players' teams throughout a block of code, it improves readability to assign them to variables. Ah, and after doing the search/replace, I see you only used them twice and once. It's still good for shortening the overall line lengths. That's my story and I'm sticking to it!

5. Please use BugUtil.debug() instead of CvUtil.pyPrint(). This way all debugging can be centrally controlled, and eventually you will be able to enable/disable it from the options screen.

6. It looks like you print out that you've found an AI's favorite civic each time doUpdate() is called once you've found it.

7. Make sure to skip all possibilities where the same player is checked against itself.

8. Always put spaces around binary operators, including % between the string and its replacement values. Always put a space after a comma.

9. Consider replacing the string keys used in the dictionaries with integer constants. This will save space and time when packing into the saved game.

POSSIBLE_FAVORITES = 0
FAVORITE_CIVIC = 1

10. Using global variables like this can get dicey, but for something like this it works okay. If you want to expand your Python knowledge, consider creating a class to hold the data for each AI with functions for modifying the data. (next post)

Code:
'def doUpdate ():
	""" Goes through the current diplomacy situation to determine potential favorite civics for each civ. """
	if isDetectionNecessary():
		iActivePlayer = gc.getGame().getActivePlayer()
		pActivePlayer = gc.getActivePlayer()
		pActiveTeam = gc.getTeam(pActivePlayer.getTeam())
		if not pActivePlayer or pActivePlayer.isNone() or not pActivePlayer.isHuman():
			return
		for iPlayer in range(gc.getMAX_PLAYERS()):
			if iPlayer == iActivePlayer:
				continue
			pPlayer = gc.getPlayer(iPlayer)
			iTeam = pPlayer.getTeam()
			pTeam = gc.getTeam(iTeam)
			if ( pPlayer and not pPlayer.isNone() and pPlayer.isAlive() 
				 and not pPlayer.isBarbarian() and not pPlayer.isHuman()
				 and pActiveTeam.isHasMet(iTeam) ):
				BugUtil.debug("Checking Player %s (ID %d)" % (pPlayer.getName(), iPlayer))
				if not isFavoriteCivicKnown(iPlayer):
					for iOtherPlayer in range(gc.getMAX_PLAYERS()):
						if iOtherPlayer == iPlayer:
							continue
						pOtherPlayer = gc.getPlayer(iOtherPlayer)
						iOtherTeam = pOtherPlayer.getTeam()
						if ( pOtherPlayer and not pOtherPlayer.isNone()
							 and pOtherPlayer.isAlive() and not pOtherPlayer.isBarbarian()
							 and pActiveTeam.isHasMet(iOtherTeam) and pTeam.isHasMet(iOtherTeam) ):
							BugUtil.debug(" -- Testing against Player %s (ID %d)" % (pOtherPlayer.getName(), iOtherPlayer))
							bFoundPossibleFavorite = AttitudeUtils.hasModifier(iPlayer, iOtherPlayer, "TXT_KEY_MISC_ATTITUDE_FAVORITE_CIVIC")
							for iCategory in range(gc.getNumCivicOptionInfos()):
								iCivic = pPlayer.getCivics(iCategory)
								if (iCivic == pOtherPlayer.getCivics(iCategory)):
									if bFoundPossibleFavorite:
										BugUtil.debug("     -- Players share %s's favorite civic!" % (pPlayer.getName()))
										BugUtil.debug("         -- (%d) %s: (%d) %s might be it" % (iCategory, gc.getCivicOptionInfo(iCategory).getText(), iCivic, gc.getCivicInfo(iCivic).getText()))
										addPossible(iPlayer, iCivic)
									else:
										BugUtil.debug("     -- Players do not share %s's favorite civic." % (pPlayer.getName()))
										BugUtil.debug("         -- (%d) %s: (%d) %s is NOT it" % (iCategory, gc.getCivicOptionInfo(iCategory).getText(), iCivic, gc.getCivicInfo(iCivic).getText()))
										addNotPossible(iPlayer, iCivic)
								else:
									if bFoundPossibleFavorite:
										BugUtil.debug("     -- Players share %s's favorite civic!" % (pPlayer.getName()))
										BugUtil.debug("         -- (%d) %s: (%d) %s is NOT it" % (iCategory, gc.getCivicOptionInfo(iCategory).getText(), iCivic, gc.getCivicInfo(iCivic).getText()))
										addNotPossible(iPlayer, iCivic)
				# using the direct length rather than the getNumPossibleFavoriteCivics() accessor
				iNumFavorites = len(gFavoriteCivicData[iPlayer]['iPossibleFavorites'])
				if iNumFavorites == 1:
					BugUtil.debug("We found the favorite civic for %s (ID %d)!" % (pPlayer.getName(),iPlayer) )
					iCivic = gFavoriteCivicData[iPlayer]['iPossibleFavorites'][0]
					iCategory = gc.getCivicInfo(iCivic).getCivicOptionType()
					BugUtil.debug("-> It's  (%d) %s in the category (%d) %s" % (iCivic, gc.getCivicInfo(iCivic).getText(), iCategory, gc.getCivicOptionInfo(iCategory).getText()))
					gFavoriteCivicData[iPlayer]['iFavoriteCivic'] = iCivic
					gFavoriteCivicData[iPlayer]['iNotFavorites'] = []
					gFavoriteCivicData[iPlayer]['iPossibleFavorites'] =  []
				elif iNumFavorites == 0:
					BugUtil.debug("Don't have any possibilities at all yet!")
				else:
					BugUtil.debug("Still too many possibilities.")
				dataDump(iPlayer)
		dataDump()
 
Here's a class to track the info for a single player. The main data structure is simply a dictionary that maps player ID -> FavoriteCivic instance. You could even replace the object with the civic ID once it is determined.

Spoiler :
Code:
NO_CIVIC = -1
NO_TECH = -1
class FavoriteCivic:
	def __init__(self, iPlayer):
		self.iPlayer = iPlayer
		self.eFavorite = NO_CIVIC
		# All non-default civics start as possible
		# If default civics can be favorites too, replace everything below with
		# self.possibles = set(range(gc.getNumCivicInfos()))
		self.possibles = set()
		for eCivic in range(gc.getNumCivicInfos()):
			info = gc.getCivicInfo(eCivic)
			# Is this the correct way to check for a category's default civic?
			if info and info.getTechPrereq() != NO_TECH:
				self.possibles.add(eCivic)
	
	def getPlayer(self):
		return self.iPlayer
	def getFavorite(self):
		return self.eFavorite
	def isKnown(self):
		return self.eFavorite != NO_CIVIC
	
	def isPossible(self, eCivic):
		return self.possibles and eCivic in self.possibles
	def removePossible(self, eCivic):
		if self.isPossible(eCivic):
			self.possibles.remove(eCivic)
			if len(self.possibles) == 1:
				# Found it
				self.eFavorite = self.possibles.pop()
				self.possibles = None
 
On a side note, I'm very close to completing the new BUG core (I can tell b/c I'm actively looking for things to distract me ;)), and adding the diplomacy events to the old BUG will be a PITA, especially since I'll just have to redo it in a couple days.

So, can you go ahead and test with the code snippet I posted above (modifying CvDiplomacy.py directly)? Instead of calling fireEvent(), just call functions in your new module directly.

If you want to go ahead and start using the new core (brave soul) for testing, checkout


and I'll post the starter DiplomacyEvents.py file. I won't commit it until we're all done.
 
In Reminders I load the queue in the OnLoad event and save it in OnPreSave. There's no reason to save it every time it changes. It's not a huge deal, and eventually I'll rewrite SdToolkit to do the same thing for all mods, but I tend to optimize up-front since I don't have an analysis tool for Python.
Okay. I had been doing that before (since I adapted the code from Reminders) but when I saw your pseudo-code it just seemed like a good idea to save more often.

continued... said:
A couple random notes while looking over your code. I hope you don't mind. I'm a) trying to maintain a coding standard for BUG code (this will be the start of it), b) using it as a way to go through the code so I understand it, and c) love giving coding advice without being asked. ;)
Considering that this is a submission to BUG and you have much more experience in this area, you have every right to make these kinds of comments. And I'm glad to have them.

continued... said:
1. You only need to declare a variable as a global if you are going to assign it a new value. You can freely modify the object it points to (a list or set or some other mutable object) without declaring it global.
Check. Thanks.

continued... said:
2. gc.getActivePlayer() always returns a human. It is the human that is sitting at the computer. I typically even skip the check for None and isNone() with it because if those fail, the game has clearly gone off into the weeds.
I figured I didn't need to check if the active player was human, but I was in a groove with all the sanity-checking. ;)

continued... said:
This is a matter of saving you typing and making code easier to read. It won't cause any problems or slowdown.

3. Python deals with "not" in if-tests well. No need to wrap "not x" in ()s. I find this easier to read as matching up ()s can be a PITA. Also, comparison operators have higher precedence than boolean operators (and or not), so you have more opportunities to trim ()s.
I like to be very explicit with my parentheses in terms of if tests because that way is easier for me to read. I've always done this in every language. If this isn't a big deal stylistically, I'd like to keep it that way.

continued... said:
4. If you're going to use the players' teams throughout a block of code, it improves readability to assign them to variables. Ah, and after doing the search/replace, I see you only used them twice and once. It's still good for shortening the overall line lengths. That's my story and I'm sticking to it!
:lol: Yeah the only reason I used teams was for the isHasMet() checks. I generally don't bother with variables if I'm using a function that sparsely.

continued... said:
5. Please use BugUtil.debug() instead of CvUtil.pyPrint(). This way all debugging can be centrally controlled, and eventually you will be able to enable/disable it from the options screen.
Will do. Although 90% of these prints were intended to be removed before final submission.

continued... said:
6. It looks like you print out that you've found an AI's favorite civic each time doUpdate() is called once you've found it.
Yeah, that's legacy from previous iterations of the code. See above about overkill print statements in early development. :)

continued... said:
7. Make sure to skip all possibilities where the same player is checked against itself.
I thought I had with the (iOtherPlayer != iPlayer) test but I'll certainly keep that in mind.

continued... said:
8. Always put spaces around binary operators, including % between the string and its replacement values. Always put a space after a comma.
Check. I'm sometimes sloppy with spaces following commas.

continued... said:
9. Consider replacing the string keys used in the dictionaries with integer constants. This will save space and time when packing into the saved game.

POSSIBLE_FAVORITES = 0
FAVORITE_CIVIC = 1
I was intending to ask if my long-winded keynames would be spacehogs. Didn't think about using integer constants. :blush:

continued... said:
10. Using global variables like this can get dicey, but for something like this it works okay. If you want to expand your Python knowledge, consider creating a class to hold the data for each AI with functions for modifying the data. (next post)
I was worried about the global and had done it that way because I wasn't sure of the mechanics of accessing it from outside places like the Foreign Advisor. It's very similar to a class the way it's written as there are a bunch of accessor functions but it wasn't formalized. If it is a class, how does the foreign advisor access the current instance to run a check? Right now it imports FavoriteCivicDetector and then calls FavoriteCivicDetector.isFavoriteCivicKnown(iPlayer) and similar functions.
 
So, can you go ahead and test with the code snippet I posted above (modifying CvDiplomacy.py directly)? Instead of calling fireEvent(), just call functions in your new module directly.
That's fine with me.

continued... said:
If you want to go ahead and start using the new core (brave soul) for testing, checkout


and I'll post the starter DiplomacyEvents.py file. I won't commit it until we're all done.
Yeah, after reading the topic ruff started I think I'd rather sit this one out. I cause enough problems on my own :p
 
Okay. I had been doing that before (since I adapted the code from Reminders) but when I saw your pseudo-code it just seemed like a good idea to save more often.

Yes, I got a little carried away with the psuedo-code. But you were right to start from the Reminders code. There's no point in pushing the data back and forth to/from script data unless you're actually saving the game, and you always get an OnPreSave event first.

And I'm glad to have them.

Cool, great attitude. :)

I figured I didn't need to check if the active player was human, but I was in a groove with all the sanity-checking. ;)

And honestly, keep on your grove. You'll be more productive if you follow your instincts when they're correct.

I like to be very explicit with my parentheses in terms of if tests because that way is easier for me to read.

Given that a lot of the mods we've merged into BUG have used varying styles, I'm certainly not going to push a single style. One guideline I do want people to follow is to keep the same style as the original author if you're editing a small piece of large code.

Different styles across the mod are fine, but having 5 styles in the same module makes it tough to edit.

:lol: Yeah the only reason I used teams was for the isHasMet() checks. I generally don't bother with variables if I'm using a function that sparsely.

I just got used to creating variables in other languages. In most languages it's free (they're stuck in a register), and I always wind up using it more than I expected. The reason I do it for the player and team by default is because it has to create a Python wrapper object around the C++ one (AFAIK) each time you call the function to get the team.

Will do. Although 90% of these prints were intended to be removed before final submission.

The good thing about using BugUtil.debug is that you don't have to remove your debug statements to make them disappear. They are great for debugging later when you make changes. But feel free to comment them out or remove them entirely as you see fit.

Yeah, that's legacy from previous iterations of the code.

What I meant was that you could "continue" the loop once you detect that the AI already has a known favorite. Instead, the print is done and the size of the possible set is checked for 1. Ah, I see that you are resetting it to an empty list. I missed that the first time and thought you'd reset the sets and assign the known civic each time through.

I thought I had with the (iOtherPlayer != iPlayer) test.

Oops, I saw that, but I also checked iOtherPlayer to iActivePlayer, but that's not right. You want to check that case. My bad. (fixed)

I was worried about the global and had done it that way because I wasn't sure of the mechanics of accessing it from outside places like the Foreign Advisor.

I didn't mean to remove the use of global variables totally. It makes perfect sense to yank the data structure out of SdToolkit and store it in a single global variable. But instead of a bunch of module-level functions that operate on the variable's parts, build the data structure out of classes as I did.

In my proposal, you'd have a global dictionary that mapped player IDs to objects (instances of the FavoriteCivic class). For example, here's how you would initialize the data:

Code:
g_favoritesByPlayer = None

def onGameStart():
    global g_favoritesByPlayer
    g_favoritesByPlayer = {}
    for iPlayer in range(gc.getMAX_PLAYERS()):
        pPlayer = gc.getPlayer(iPlayer)
        if pPlayer and not pPlayer.isNone() and pPlayer.isAlive() and not pPlayer.isBarbarian() and not pPlayer.isHuman():
            g_favoritesByPlayer[iPlayer] = FavoriteCivic(iPlayer)

doUpdate would then look like this:

Code:
...
for iPlayer in ...:
    # we have a living AI, get its FavoriteCivic object (data structure)
    favorite = g_favoritesByPlayer[iPlayer]
    if not favorite.isKnown():
        # still looking for favorite civic
        for iOtherPlayer in ...:
            # we have a living player that isn't the AI
            bFoundPossibleFavorite = ...
            for iCategory in ...:
                iCivic = ...
                if favorite.isPossible(iCivic):
                    if (iCivic == pOtherPlayer.getCivics(iCategory)):
                        if bFoundPossibleFavorite:
                            # not necessary as they all start as possible
                        else:
                            favorite.removePossible(iCivic)
                    else:
                        if bFoundPossibleFavorite:
                            favorite.removePossible(iCivic)
        if favorite.isKnown():
            # finally found the single civic
 
5. Please use BugUtil.debug() instead of CvUtil.pyPrint(). This way all debugging can be centrally controlled, and eventually you will be able to enable/disable it from the options screen.
We have a global debug print code? Guess I better start using it instead of the ones that are declared at the top of each of the files I am playing with.
 
We have a global debug print code? Guess I better start using it instead of the ones that are declared at the top of each of the files I am playing with.

Oh you didn't know? I thought we had talked about it already; maybe that was Alerum. I guess that explains all those extra print functions I've replaced. :)
 
Note, using SVN revision 1183 as a base; the one with the old core. I want to finish this sucker before I mess with the new core. :p

So, err, how is one supposed to use BugUtil.debug()? It looks like it's supposed to get some printing options from the INI but the only debug-related thing I saw in the INI file is this guy:
Code:
###########################################################################
[DEBUG]

# show debug messages (1 = yes, 0 = no)
ShowMessages = 1
I've tried at both 1 and 0 and neither seemed to have any effect. Ahhh. reading over previous posts I see that little qualifier
eventually you will be able to enable/disable it from the options screen.

Alright then, so is the proper current procedure to do something like the following while testing:
Code:
import BugUtil
BugUtil.printToFile = True
And then remove/comment out the printToFile assignment when finished?
 
I just modify the values directly in BugUtil locally. The new core will get its own INI with these and other settings (screen, file, log all events, etc).
 
Boy do I hate GUIs :p. It's the one are of programming that has always been almost incomprehensible to me.

How does everything line up in the current Info screen? Is it simply because everything is attached (eventually) to the same main panel so the game just magically handles this behind the scenes?

What I would like to do is have a static header, then the scrollable main panel underneath, and be able to line everything up nicely. However when the header is separate, the "column" headings don't line up with the icons and such in the player bars. And when it isn't separate, then the header will scroll off the screen...

Any ideas? Should I try going with IconGrid instead?
 
Boy do I hate GUIs :p. It's the one are of programming that has always been almost incomprehensible to me.

That may be the case, but Civ is worse because you can't see any of the magic that happens under the hood.

How does everything line up in the current Info screen? Is it simply because everything is attached (eventually) to the same main panel so the game just magically handles this behind the scenes?

Good question. At first I thought it worked simply because all of the parts are the same width, but that's not the case for the total trade value. Therefore, my suspicion is that this type of panel lines up columns for you. Columns being just the elements as they are added.

For example, try leaving off the total trade for one player and see what happens (I haven't; I'm curious).

Should I try going with IconGrid instead?

Even though I recommended it first, I would prefer to stay away from it simply because the mousewheel doesn't work with IG. Also, it's slower. As a first cut, just place the headers using manual X and Y coordinates without putting them into their own panel (or maybe you can use a panel too, but don't just add them willy-nilly like the items in each row are added).

Another option is to draw the whole thing like the Glance tab -- each item is placed manually.

But, it's more important that you can move forward and get what you want done, so if you want to use IG, go ahead. :)
 
Good question. At first I thought it worked simply because all of the parts are the same width, but that's not the case for the total trade value. Therefore, my suspicion is that this type of panel lines up columns for you. Columns being just the elements as they are added.

For example, try leaving off the total trade for one player and see what happens (I haven't; I'm curious).
Good idea. That does confirm that the game is lining everything up on its own. I didn't take a screenshot, but when I did this, that player's first civic was right in line with everyone else's trades and his second civic was perfectly aligned with everyone else's first, etc.

continued... said:
Even though I recommended it first, I would prefer to stay away from it simply because the mousewheel doesn't work with IG. Also, it's slower. As a first cut, just place the headers using manual X and Y coordinates without putting them into their own panel (or maybe you can use a panel too, but don't just add them willy-nilly like the items in each row are added).

Another option is to draw the whole thing like the Glance tab -- each item is placed manually.

But, it's more important that you can move forward and get what you want done, so if you want to use IG, go ahead. :)
I think manually placing the header text might be the best option. Given that the game is lining up the "body" on its own, it shouldn't be hard to figure out where to place stuff. And the body won't have any text so there won't be any issues with translations moving things out of whack.
 
Alrighty, I have finally managed to understand how the game is laying this thing out. Apparently, everytime you attach something to an existing panel, there is a minimum width of around 60px applied. If the thing you attach is bigger than that, then the space is automatically expanded, but if it is smaller, the 60px width holds. Knowing this, I can use the 46px versions of the buttons and everything lines up nicely. (The larger "custom" size currently used was around 64 pix and that was why my headers weren't lining up with everything else as those small differences pushed everything out of alignment). See the following screens, cropped to 1024 width; thumbnail links to a 75% size.





There is still more work to do such as highlighting "matching" civics, but the layout was the biggest headache and I think that's now under control. I'm currently using the random leader graphic as a placeholder "unknown" graphic since it has a big question mark in it.

The next question is how useful is the category qualifier on the favorite civic in the current implementation? e.g. Isabella shows the Theocracy button and then next to it you see "(Religion)". That still looks kinda weird to me and I don't think it should be shown when you are guessing civics and have multiple possibilities because then things look real sloppy, but perhaps if it really is useful there are ways to preserve it.

It might be useful to have some notation of the fact that multiple listed civics means "the real one is one of these choices"; perhaps changing the header text to "Possible Favorite Civic" when you are running Random Personalities and leaving it as "Favorite Civic" when you aren't will help with that. I'm thinking limiting the choices shown to no more than 5 and using the "unknown" graphic when there are more possibilities than that as seen in the first screen.

I'm also considering putting a column for the overall attitude (you can't have too many attitude notations ;)) and rather than using a "*" for a matching religion/favorite civic maybe pulling out the actual religion or civic-related diplo modifer, assuming that won't mess the layout too badly. e.g.:

Code:
Me        Islam
Isabella  Hindu [COLOR="Red"][-3][/COLOR]
Gandhi    Islam [COLOR="Green"][+2][/COLOR]
 
Wow, that's already a huge improvement! :goodjob:

I'm currently using the random leader graphic as a placeholder "unknown" graphic since it has a big question mark in it.

NikNaks is chomping at the bit to get more graphic requests. Send him a PM and I'm sure he'll whip something up in a day or two.

The next question is how useful is the category qualifier on the favorite civic in the current implementation?

It was always my intention to remove it entirely to make room for something more informative. As it seems you've found several things, I say ditch it. A different possibility would be to create buttons for each category and use them for the headings and in the case where you know their favorite for sure, but that just seems overkill and confusing. If everything is an image, they start to lose their effectiveness.

. . . perhaps changing the header text to "Possible Favorite Civic" when you are running Random Personalities and leaving it as "Favorite Civic" when you aren't.

That sounds like a great idea. The longer header won't matter because you'll be listing so many civics until you figure everyone out (unlikely to happen I would guess).

I'm thinking limiting the choices shown to no more than 5 and using the "unknown" graphic when there are more possibilities than that as seen in the first screen.

In the beginning, you're more apt to know a few civics that can't be their favorites. As soon as you get the positive diplomacy modifier, you can narrow it down to five or less (those that you already eliminated). Given that, I'd say your "five or less" suggestion is the easiest way to handle both cases.

I'm also considering putting a column for the overall attitude (you can't have too many attitude notations ;)) and rather than using a "*" for a matching religion/favorite civic maybe pulling out the actual religion or civic-related diplo modifer, assuming that won't mess the layout too badly. e.g.:

Brilliant! Much better and more informative than the asterisk. As well, it will look cooler and may not require any explanation.

If you're going to show the full attitude total, I suggest adding the attitude icon as well since you'll have room (64 px) to fill. Don't forget about the AttitudeUtils module. This is the place where extracting a single line item should live, or was that added already?

I love that we've added so much stuff that I can't remember everything anymore. :D
 
Back
Top Bottom