[PYTHON] Merging GreatDoctor mod with Bug

Doeventplague...
Code:
		plagueTurns = 10 + self.getRandomNumber(6)
		szCityName = city.getName()	
		self.setPlague(szCityName, plagueTurns, iCoords)

+

Code:
	def setPlague(self, pCityName, pTurns, pCoords):
		table = BugData.getTable("Plague")
		[COLOR="Red"]table[pCityName] = { pTurns, pCoords }[/COLOR]
		#table["counter"] = pTurns
		#table["cities"] = pCoords

= No Interface :(

Code:
Traceback (most recent call last):
  File "<string>", line 1, in ?
  File "<string>", line 52, in load_module
  File "CvEventInterface", line 17, in ?
  File "<string>", line 52, in load_module
  File "BugEventManager", line 102, in ?
  File "<string>", line 52, in load_module
  File "CvEventManager", line 12, in ?
  File "<string>", line 52, in load_module
  File "CvScreensInterface", line 3, in ?
  File "<string>", line 52, in load_module
  File "CvMainInterface", line 13, in ?
  File "<string>", line 35, in load_module
  File "<string>", line 13, in _get_code
  File "
GreatDoctorEventManager
", line 
312

    
table[pCityName] = { pTurns, pCoords }
 
The {} indicates that its a dictionary, and a dictionary contains pairs of keys and values. Like:
Code:
{key:value}
Several key-value pairs are separated by commas:
Code:
{'first value':1, 'second value':2, 'a list':[1, 2]}
I wouldn't know how BugData operates, I'm just saying... :p (Its in the book, remember? :D)

edit: I took another look: If the variable table is pointing to a dictionary you would indeed assign a value to the entry called pCityName (what's with the "p" on everything?) with table[pCityName] =, but then you get mixed up. Are you trying to add two values to the key pCityName in the dictionary called table or are you trying to pass it another dictionary? If you wanna add two separate values you probably wanna put then in a list [] or a tuple () first. In short: lose the {} and use another data structure.

But then again, BugData might work in some other, mysterious, way... :rolleyes:
 
The {} indicates that its a dictionary, and a dictionary contains pairs of keys and values. Like:
Code:
{key:value}
Several key-value pairs are separated by commas:
Code:
{'first value':1, 'second value':2, 'a list':[1, 2]}
I wouldn't know how BugData operates, I'm just saying... :p (Its in the book, remember? :D)

edit: I took another look: If the variable table is pointing to a dictionary you would indeed assign a value to the entry called pCityName (what's with the "p" on everything?) with table[pCityName] =, but then you get mixed up. Are you trying to add two values to the key pCityName in the dictionary called table or are you trying to pass it another dictionary? If you wanna add two separate values you probably wanna put then in a list [] or a tuple () first. In short: lose the {} and use another data structure.

But then again, BugData might work in some other, mysterious, way... :rolleyes:

Yes, I'm attempting to use a dictionary on purpose. Bugdata says it can use dictionaries. It seems best because I want to keep a group of things together, in this case I'm hoping to keep:

the city name that it originated in (origin)
the counter
the coordinates

if it spreads to another city I want to have
origin stay the same
the counter the same
coordinates different

Once the counter expires, then I'll go through and all with same "origin city name" entries go away and are restored to full health.

If I'm doing it wrong let me know.
bush_doing_it_wrong.jpg
 
:lol:

I'm not sure that I'm following, but if you wanna assign values to key names in a dictionary, then you should use keys and values (obviously):
Code:
table[pCityName] = {'counter':pCounter, 'coords':pCoords}
But you would get away with using a tuple also:
Code:
table[pCityName] = (pCounter, pCoords)
But then you'd have to remember which is which in order. They would be indexed as zero and one, by the way, and you'd fetch them like:
Code:
tuple = table.get(pCityName, None)
pCounter = tuple[0]
pCoords = tuple[1]
(Unless there is a handy BugData method for fetching these values.)

But I'm still not sure why you use the "p" prefix for everything. Its usually saved for class instances (also known as objects or instances of some class). These variables would be an integer value and a tuple respectively, right? Then the proper prefixes would be "i" for integer and "t" for tuple. (I actually have no idea what the "p" stands for, but I'm hoping some programmer type will illuminate this subject for us. :D)
 
Code:
	def setPlague(self, pCityName, pTurns, pCoords):
		table = BugData.getTable("Plague")
		table[pCityName] = {pTurns:1, pCoords:2}
		#table["counter"] = pTurns
		#table["cities"] = pCoords

		CvUtil.pyPrint('plague table built: %s' %(table))

	def isCityInfected(self, pCityName):
		# the "cities" table contains a single set object "coords"
		# that contains (x,y) points
		table = BugData.findTable("Plague")
		#table = BugData.findTable("Plague")
		
		CvUtil.pyPrint('plague table rezlts: %s' %(table[pCityName]))
		if table:
#			if not i in 
			self.msg("Found it coach!")
		else:
			self.msg("No Table")
		return False

Bugdata gettable and findtable not liking what I'm doing for some reason.

Error from debuglog
Code:
17:59:45 DEBUG: BugData - opening root.Plague: {}
17:59:45 DEBUG: BugData - root.Plague.Thebes -> {(15, 16): 2, 13: 1}
PY:plague table built: root.Plague
17:59:45 DEBUG: BugTable - Plague not found in root
17:59:45 TRACE: Error in ModNetMessage event handler <bound method GreatDoctorEventManager.onModNetMessage of <GreatDoctorEventManager.GreatDoctorEventManager instance at 0x18C28760>>
17:59:45[COLOR="Red"] TRACE: unsubscriptable object[/COLOR]
17:59:46 DEBUG: BugEventManager - event UnInit
 
Yeah, so it does sound like you need a counter for each city--not one counter for the entire game. What you can do is store a counter as the value for the (x,y) coordinates of each infected city. So you have a dictionary where the keys are coordinate pairs and the values are the counters for the city at those coordinates.

Code:
	def infectCity(self, pCity):
		table = BugData.getTable("Plague", "cities")
		point = (pCity.getX(), pCity.getY())
		if "coords" not in table:
			table["coords"] = set()
		table["coords"][point] = gc.getGame().getGameTurn()

	def removeCleanCities(self):
		# remove cities that have been infected for the full time
		table = BugData.getTable("Plague", "cities")
		cleanTurn = gc.getGame().getGameTurn() + MAX_INFECTION_TURNS:
		if "coords" in table:
			cities = table["coords"]
			for point, turn in cities.items():
				if now >= cleanTurn:
					del cities[point]

The code above merely handles infecting cities and removing the infection after MAX_INFECTION_TURNS turns. It doesn't handle doing anything to the infected cities. What you want to do determines how you would modify the code. If the city merely experiences a +8:yuck: rate, then it can be added in infectCity() and removed in removeCleanCities(). If there is an effect that happens every turn (+1:yuck: per turn), that would go in removeCleanCities() and you'd probably want to rename the function to something like processInfectedCities().
 
Bah, didn't go to next page. Whenever you post an error message, please also post the full stack trace from PythonErr.log and highlight the line with the error in your posted code. I cannot tell which object is unscriptable because I have no idea on which line that error occurred.

BugData is an API for storing tables (dictionaries). That's it. It isn't any more exciting than that, though it does handle the saving and loading for you, and only saves when the data has changed. Beyond that it lets you store whatever you want to store in any slot, and you can nest tables to any depth.

I recommend that instead of storing the name of the originating city you store its coordinates. First, it will take less space. Second, it is resistant to renaming cities. Third, it's just better. ;)

Next I recommend writing down exactly what you need to have happen. That there is an originating city at all is totally new. Every time you change your requirements you need to rethink how you store your data. If you don't you will end up forcing BTS to do more work, and it may be harder to do what you want. That doesn't mean "don't change anyhing", just make sure you update what you've already done when you change it. It would be really helpful if you could have one post where you constantly update the "business requirements" of this feature.

Something like this:

  1. Any number of cities can be infected.
  2. A city will become clean after X turns of infection (random, or set, or whatever).
  3. Each infection retains the source city, and when that city becomes clean all cities infected by it become clean.
  4. Every turn any city with a trade route (source or destination or both?) with an infected city has X% chance of becoming infected.
  5. Any city infected by outbreak X cannot become infected by outbreak Y until it becomes clean.
The key is to write out a software contract, much like a legal contract, that says exactly what happens and when. Only then can you hope to code it up. If you just try to write out code willy nilly you'll be chasing your tail.
 
Bah, didn't go to next page. Whenever you post an error message, please also post the full stack trace from PythonErr.log and highlight the line with the error in your posted code. I cannot tell which object is unscriptable because I have no idea on which line that error occurred.

I'm sorry I thought it was somewhat clear that the error was in the bottom function when it tries to find the table that was just created with the top function. Error line:
Code:
table = BugData.findTable("Plague")

I decided to store the "city name" AND coordinates. The city name is the plague name, so that if it spreads to 5 or 6 cities, it's the "Washington" plague (so it's a unique key that I can erase when the counter runs out all "washington" plagues) the coordinates are there for the reasons you listed.

I'll update the first post with business requirements then as per your request.
 
:lol: Way to go smeagolheart! You've left the merging stage and started to mod stuff on your own. :goodjob:

Just make sure you read up on the subjects that you are relying on in your work. It will actually save you time, so don't fool yourself into thinking that you don't have time for it. (I really can't recommend the textbook enough. Read it, understand what it is saying - and you can call yourself a amateur programmer. Just like that. And we wouldn't have to figure out what kind of "merger" you really are. :D)
 
I'm sorry I thought it was somewhat clear that the error was in the bottom function when it tries to find the table that was just created with the top function.

You still need to post the stack trace, the one that lists the line numbers in the Python modules. It will be in PythonErr.log. The line you posted

Code:
table = BugData.findTable("Plague")

Is not doing any subscripting itself. If the error is happening inside findTable() I need to see the line numbers so I can find it. More than likely some other code is placing something naughty inside the CyGame's script data, and when BugData tries to use it you get this error. Find all places in your mod that call setScriptData() and replace them with BugData calls.

I decided to store the "city name" AND coordinates. The city name is the plague name, so that if it spreads to 5 or 6 cities, it's the "Washington" plague (so it's a unique key that I can erase when the counter runs out all "washington" plagues).

That will be okay as long as you can trust players not to rename their cities to create a duplicate name. And that would only cause a problem if someone then spawns a plague in two cities with the same name. I'd still use the coordinates of that city as the unique key and then store the name as well, but that's because I'm paranoid. ;)
 
You still need to post the stack trace, the one that lists the line numbers in the Python modules.
Debugging is something smeagolheart also needs to get into himself, so now would be a good time to start. Because those exceptions can be read. In fact, he should be reading them himself, even if they probably won't mean much at this point. But its important none-the-less.

That will be okay as long as you can trust players not to rename their cities to create a duplicate name. And that would only cause a problem if someone then spawns a plague in two cities with the same name. I'd still use the coordinates of that city as the unique key and then store the name as well, but that's because I'm paranoid. ;)
Its called "fool-proofing". :D I second the motion to define cities by plot coordinates, by the way.

As a sidenote: Wouldn't it be easier to use plot ID instead of a tuple containing plot coordinates? I use tuples myself, but it just occurred to me that it would be very convenient to use CyMap.plotByIndex() to get the CyPlot instance... And if you need to get the X and Y values you just make a function that returns the tuple, right?
Spoiler :
Code:
pMap = CyMap()
...
def getCoords(iID):
    return ( pMap.plotX(iID), pMap.plotY(iID) )
 
I wanted the stack trace in case it was happening inside BugData. But as I said, I suspect some other code is writing to the script data.

Yes, plot index is a good way to go.
 
That will be okay as long as you can trust players not to rename their cities to create a duplicate name.

Hmm.. Good point about the renaming thing. It is possible if you were really determined to beat the effect to do some renaming and overcome the effect early if you were hit twice or something. It's a lot of effort but you are right.

I do like having it be the 'Washington' Plague or whatever in my immediate messages to the player to differentiate it from the 'Istanbul' plague being spread and it's notifications. But you are right about using coordinates as the key, not the name.

meh. That means I should need 4 variables now?

* plague name (origin city name)
* origin city coords
* remaining turns counter
* current city infected coords

And I do read the pythonerr.log I have been posting the errors from it all along if you look at the earlier posts. I'll look into Baldur's suggested plot variable.
 
Code:
	def infectCity(self, pCity):
		table = BugData.getTable("Plague", "cities")
		point = (pCity.getX(), pCity.getY())
		if "coords" not in table:
			table["coords"] = set()
		[COLOR="Red"]table["coords"][point] = gc.getGame().getGameTurn()[/COLOR]

	def removeCleanCities(self):
		# remove cities that have been infected for the full time
		MAX_INFECTION_TURNS = 2		
		table = BugData.getTable("Plague", "cities")
		#cleanTurn = gc.getGame().getGameTurn():
		cleanTurn = 0
		self.msg(cleanTurn)
		if "coords" in table:
			cities = table["coords"]
			for point, turn in cities.items():
				if now >= cleanTurn:
					del cities[point]
					city = gc.getMap( ).plot( point ).getPlotCity()
					self.doPlagueRemoveEffect(city)
					szCityName = city.getName()
					plot = city.plot()
					szTitle = localText.getText("TXT_KEY_GODS_PLAGUE_DECAY", ( szCityName, ))
					CyInterface().addMessage(plot.getOwner(), True, gc.getEVENT_MESSAGE_TIME(), szTitle, "AS2D_DISCOVERBONUS", InterfaceMessageTypes.MESSAGE_TYPE_INFO, gc.getEffectInfo(iPlagueEffect).getButton(), gc.getInfoTypeForString("COLOR_RED"), plot.getX(), plot.getY(), True, True)

Code:
20:41:23 DEBUG: BugData - root.Plague.cities.coords -> set([])
20:41:23 DEBUG: BugData - root.Plague.cities.coords = set([])
20:41:23 TRACE: Error in ModNetMessage event handler <bound method GreatDoctorEventManager.onModNetMessage of <GreatDoctorEventManager.GreatDoctorEventManager instance at 0x18C30AA8>>
20:41:23 TRACE: object does not support item assignment
20:41:25 DEBUG: BugEventManager - event UnInit


Code:
Traceback (most recent call last):
  File "BugEventManager", line 361, in _handleDefaultEvent
  File "GreatDoctorEventManager", line 62, in onModNetMessage
  File "GreatDoctorEventManager", line 223, in doEventPlague
  File "GreatDoctorEventManager", line 325, in infectCity
TypeError: object does not support item assignment
 
This calls for creating a Plague class that tracks the infected cities, but it depends on what's in control: the plague or the turn counter. When does a plague end? When do cities become infected and clean? Does the entire plague end when the origin city's counter reaches zero? If so, you only need one counter per plague. If not, you need a counter per city.

Code:
class Plague:
	def __init__(self, pOrigin):
		self.name = pOrigin.getName()
		self.origin = CyMap().plotNum(pOrigin.getX(), pOrigin.getY())
		self.startTurn = CyGame().getGameTurn()
		self.cities = {self.origin: self.startTurn}

You can place more methods on the Plague class to handle spreading and its effects.

You could now store the Plague objects in a list and store that list as the single pickled variable in a table. Or you could store each Plague in a separate BugData table. Since you need to check every plague each turn, it probably makes more sense to store them all in one list.

As for the stack traces, I am talking about this post where you show the error message but not the stack trace.

Idea: a city's chance of becoming clean once infected should be higher each turn based on its excess health. Thus a naturally healthy city should suffer infection for a shorter duration than a city at it's :health: cap.
 
Change this line

Code:
table["coords"] = set()

to this

Code:
table["coords"] = dict()

Sets only support add/remove operations. They do not map keys to values like dictionaries do.
 
This calls for creating a Plague class that tracks the infected cities, but it depends on what's in control: the plague or the turn counter. When does a plague end? When do cities become infected and clean? Does the entire plague end when the origin city's counter reaches zero? If so, you only need one counter per plague. If not, you need a counter per city.

Code:
class Plague:
	def __init__(self, pOrigin):
		self.name = pOrigin.getName()
		self.origin = CyMap().plotNum(pOrigin.getX(), pOrigin.getY())
		self.startTurn = CyGame().getGameTurn()
		self.cities = {self.origin: self.startTurn}

You can place more methods on the Plague class to handle spreading and its effects.

You could now store the Plague objects in a list and store that list as the single pickled variable in a table. Or you could store each Plague in a separate BugData table. Since you need to check every plague each turn, it probably makes more sense to store them all in one list.

Idea: a city's chance of becoming clean once infected should be higher each turn based on its excess health. Thus a naturally healthy city should suffer infection for a shorter duration than a city at it's :health: cap.

I like the idea of city's healthiness affecting the duration. The counter is in control, the entire plague ends when the origin city's counter reaches zero. This represents your doctors (medicine men) or whatever "finding the cure" I guess. I don't want to make this plague thing overwhelming to the basic core game, just a add-on neat feature. Other people will be able to strengthen/weaken it as they see fit.
 
Ok I think I may be getting near the end of the beginning. It's slow going here. No errors this time, just not working as hoped for yet.

Code:
	def infectCity(self, pCity, pEndTurn):
		table = BugData.getTable("Plague")
		plague = Plague(pCity, pEndTurn)
		
		table = BugData.getTable("Plague")
		point = (pCity.getX(), pCity.getY())
		if "name" not in table:
			table["name"] = dict()
		if "origin" not in table:
			table["origin"] = dict()
		if "endTurn" not in table:
			table["endTurn"] = dict()
		if "cities" not in table:
			table["cities"] = dict()		
			
		table["name"][point] = plague.name
		table["origin"][point] = plague.origin
		table["endTurn"][point] = plague.endTurn
		table["cities"][point] = plague.cities
		
		#CvUtil.pyPrint('new plague table built: %s' %Plague(pCity, pEndTurn))

	def isCityInfected(self, pCity, pEndTurn):
		table = BugData.findTable("Plague")
		if table:
			self.msg("Found it coach!")
		else:
			self.msg("No Table")
		return False
....

class Plague:
	def __init__(self, pOrigin, pEndturn):
		self.name = pOrigin.getName()
		self.origin = CyMap().plotNum(pOrigin.getX(), pOrigin.getY())
		self.endTurn = pEndturn
		self.cities = {self.origin: self.endTurn}

Here's what I got at the moment. Is bug saving my object based on what I've got in infectCity?

After running infect city then isCityInfected immediately after and passing the same variables, the game returns no there is no table plague, see spoiler here:
Spoiler :

Code:
02:23:39 DEBUG: BugData - returning open table root.Plague
02:23:39 DEBUG: BugData - returning open table root.Plague
02:23:39 DEBUG: BugData - name in root.Plague: False
02:23:39 DEBUG: BugData - root.Plague.name -> {}
02:23:39 DEBUG: BugData - origin in root.Plague: False
02:23:39 DEBUG: BugData - root.Plague.origin -> {}
02:23:39 DEBUG: BugData - endTurn in root.Plague: False
02:23:39 DEBUG: BugData - root.Plague.endTurn -> {}
02:23:39 DEBUG: BugData - cities in root.Plague: False
02:23:39 DEBUG: BugData - root.Plague.cities -> {}
02:23:39 DEBUG: BugData - root.Plague.name = {}
02:23:39 DEBUG: BugData - root.Plague.origin = {}
02:23:39 DEBUG: BugData - root.Plague.endTurn = {}
02:23:39 DEBUG: BugData - root.Plague.cities = {}
02:23:39 DEBUG: BugTable - Plague not found in root
 
Have you looked for any other uses of setScriptData() on the CyGame object?

What's the rest of your code look like? When are you calling infectCity() and isCityInfected()? For one thing, you should only create a new Plague when a new plague is started. After that you will add cities to an existing plague to make it spread.

You can store the Plague objects directly in the table. Here's another stab at fleshing out the behavior. I've expanded the Plague class to perform the data storage itself as they are started and cured.

Code:
	def startPlague(self, pCity, iEndTurn):
		"""Starts a new plague in pCity."""
		plague = Plague(pCity, iEndTurn)

	def spread(self, pFromCity, pToCity):
		"""Spreads the plague that infected pFromCity to pToCity."""
		plague = self.findPlague(pFromCity)
		if plague:
			plague.spread(pToCity)
		else:
			BugUtil.error("spread - from city is not infected")
	
	def findPlague(self, pCity):
		"""Returns the plague that infected the given city."""
		plagues = BugData.getTable("Plague")
		for plague in plagues.itervalues():
			if plague.isInfecting(pCity):
				return plague
		return None
	
	def checkPlagues(self, iGameTurn):
		"""Checks all of the plagues to see if any should be cured."""
		plagues = BugData.getTable("Plague")
		for plague in plagues.values():
			if iGameTurn >= plague.endTurn:
				plague.cure()

class Plague:
	def __init__(self, pOrigin, iEndTurn):
		self.name = pOrigin.getName()
		self.origin = getPlotIndex(pOrigin)
		self.endTurn = iEndTurn
		self.cities = set()
		self.cities.add(self.origin)
		plagues = BugData.getTable("Plague")
		plagues[self.origin] = self
		
	def getPlotIndex(self, pCity):
		return CyMap().plotNum(pOrigin.getX(), pOrigin.getY())
	
	def isInfecting(self, pCity):
		return self.getPlotIndex(pCity) in self.cities
		
	def spread(self, pToCity):
		self.cities.add(self.getPlotIndex(pCity))
	
	def cure(self):
		for pCity in self.cities:
			self.cureCity(pCity)
		plagues = BugData.getTable("Plague")
		del plagues[self.origin]
	
	def infectCity(self, pCity):
		pCity.changeHealth(-10)
	
	def cureCity(self, pCity):
		pCity.changeHealth(10)
 
Back
Top Bottom