Kael: Scenario Functions split apart

Zechnophobe

Strategy Lich
Joined
Sep 1, 2006
Messages
1,867
Location
Goleta, California
In an attempt to make new Scenario's easier to make, I have changed my local files to split apart the code for each, and I'd like you to use this implementation for the live version. The way I have it set up now works thusly:

A new scenario uses the name SCENARIO_NAME.py, and defines each of the functions currently in Scenario Functions. Whereas Scenario Functions now acts simply as a pass through that checks to see what scenario is active. For instance, this would use this function:

Code:
def getScenario(self):
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_AGAINST_THE_WALL):
			return SCENARIO_AGAINST_THE_WALL.SCENARIO_AGAINST_THE_WALL()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_AGAINST_THE_GREY):
			return SCENARIO_AGAINST_THE_GREY.SCENARIO_AGAINST_THE_GREY()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_INTO_THE_DESERT):
			return SCENARIO_INTO_THE_DESERT.SCENARIO_INTO_THE_DESERT()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_FALL_OF_CUANTINE):
			return SCENARIO_FALL_OF_CUANTINE.SCENARIO_FALL_OF_CUANTINE()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_GIFT_OF_KYLORIN):
			return SCENARIO_GIFT_OF_KYLORIN.SCENARIO_GIFT_OF_KYLORIN()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_GRAND_MENAGERIE):
			return SCENARIO_GRAND_MENAGERIE.SCENARIO_GRAND_MENAGERIE()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_THE_BLACK_TOWER):
			return SCENARIO_THE_BLACK_TOWER.SCENARIO_THE_BLACK_TOWER()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_THE_RADIANT_GUARD):
			return SCENARIO_THE_RADIANT_GUARD.SCENARIO_THE_RADIANT_GUARD()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_BARBARIAN_ASSAULT):
			return SCENARIO_BARBARIAN_ASSAULT.SCENARIO_BARBARIAN_ASSAULT()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_BENEATH_THE_HEEL):
			return SCENARIO_BENEATH_THE_HEEL.SCENARIO_BENEATH_THE_HEEL()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_BLOOD_OF_ANGELS):
			return SCENARIO_BLOOD_OF_ANGELS.SCENARIO_BLOOD_OF_ANGELS()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_LORD_OF_THE_BALORS):
			return SCENARIO_LORD_OF_THE_BALORS.SCENARIO_LORD_OF_THE_BALORS()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_MULCARN_REBORN):
			return SCENARIO_MULCARN_REBORN.SCENARIO_MULCARN_REBORN()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_RETURN_OF_WINTER):
			return SCENARIO_RETURN_OF_WINTER.SCENARIO_RETURN_OF_WINTER()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_THE_CULT):
			return SCENARIO_THE_CULT.SCENARIO_THE_CULT()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_THE_MOMUS):
			return SCENARIO_THE_MOMUS.SCENARIO_THE_MOMUS()
		if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_THE_SPLINTERED_COURT):
			return SCENARIO_THE_SPLINTERED_COURT.SCENARIO_THE_SPLINTERED_COURT()

Which would be called like:

Code:
def doTurn(self):
		myScenario = self.getScenario()
		myScenario.doTurn()

Generate a new SCENARIO.PY from a template (Which I have and can provide).

Lastly, I removed all 'extra' function from ScenarioFunctions, and placed them in CustomScenarioFunctions (Such as the getHeroXP) since these break the design paradigm (You should only have functions in the scope of loading or working with scenarios, not grab bag functions):

Code:
class CustomScenarioFunctions:

	def addPopupWB(self, szText, sDDS):
		szTitle = CyGameTextMgr().getTimeStr(CyGame().getGameTurn(), false)
		screen = CyGInterfaceScreen( "MainInterface", CvScreenEnums.MAIN_INTERFACE )
		xRes = screen.getXResolution()
		yRes = screen.getYResolution()
		popup = PyPopup.PyPopup(-1)
		popup.addDDS(sDDS, 0, 0, 500, 800)
		popup.addSeparator()
		popup.setHeaderString(szTitle)
		popup.setBodyString(szText)
		popup.setPosition((xRes - 840) / 2,(yRes - 640) / 2)
		popup.setSize(840, 640)
		popup.launch(true, PopupStates.POPUPSTATE_IMMEDIATE)
		
	def getHeroXP(self, iPlayer, iUnit):
		"""returns the amount of XP that the given unit has, owned by a player. If iUnit is not unique
		then it will return the xp of the last unit found. If no units are found, returns -1"""
		apUnitList = PyPlayer(iPlayer).getUnitList()
		iXP = -1
		for pLoopUnit in apUnitList:
			if pLoopUnit.getUnitType() == iUnit:
				if pLoopUnit.getExperience() > iXP:
					iXP = pLoopUnit.getExperience()
		return iXP
	
	def giftHeroXP(self, iPlayer, iUnit, iXP):
		"""Finds all units of type 'iUnit' owned by iPlayer, and gives them 'iXP' experience."""
		apUnitList = PyPlayer(iPlayer).getUnitList()
		for pLoopUnit in apUnitList:
			if pLoopUnit.getUnitType() == iUnit:
				pLoopUnit.changeExperience(iXP, -1, False, False, False)

The benefits which this reap (considerable!) Are that all the effects of a scenario are now in their own area. For Example, the Splintered Court:

Spoiler :
Code:
class SCENARIO_THE_SPLINTERED_COURT:

	def doTurn(self):
		iPlayer = 0
		pPlayer = gc.getPlayer(iPlayer)
		if gc.getGame().getScenarioCounter() == 7:
			gc.getGame().changeScenarioCounter(-7)
		else:
			gc.getGame().changeScenarioCounter(1)

		iCalabim = gc.getInfoTypeForString('CIVILIZATION_CALABIM')
		iDoviello = gc.getInfoTypeForString('CIVILIZATION_DOVIELLO')
		iDuin = gc.getInfoTypeForString('UNIT_DUIN')
		iImmigrants = gc.getInfoTypeForString('EVENTTRIGGER_IMMIGRANTS')
		iLjosalfar = gc.getInfoTypeForString('CIVILIZATION_LJOSALFAR')
		iStrong = gc.getInfoTypeForString('PROMOTION_STRONG')
		iSvartalfar = gc.getInfoTypeForString('CIVILIZATION_SVARTALFAR')
		iVampire = gc.getInfoTypeForString('PROMOTION_VAMPIRE')
		iWerewolf = gc.getInfoTypeForString('UNIT_WEREWOLF')
		for iPlayer in range(gc.getMAX_PLAYERS()):
			pPlayer = gc.getPlayer(iPlayer)
			if pPlayer.isAlive():
				if pPlayer.getCivilizationType() == iCalabim:
					if gc.getGame().getScenarioCounter() == 0 or gc.getGame().getScenarioCounter() == 4:
						bState = False
						if gc.getGame().getScenarioCounter() == 4:
							bState = True
						py = PyPlayer(iPlayer)
						for pLoopUnit in py.getUnitList():
							if pLoopUnit.isHasPromotion(iVampire):
								pLoopUnit.setHasPromotion(iStrong, bState)
				if pPlayer.getCivilizationType() == iDoviello:
					if gc.getGame().getScenarioCounter() == 0:
						py = PyPlayer(iPlayer)
						lList = []
						for pLoopUnit in py.getUnitList():
							if pLoopUnit.getScenarioCounter() != -1:
								lList.append(pLoopUnit)
						for pLoopUnit in lList:
							newUnit = pPlayer.initUnit(pLoopUnit.getScenarioCounter(), pLoopUnit.getX(), pLoopUnit.getY(), UnitAITypes.NO_UNITAI, DirectionTypes.DIRECTION_SOUTH)
							newUnit.convert(pLoopUnit)
							newUnit.setScenarioCounter(-1)
					if gc.getGame().getScenarioCounter() == 4:
						py = PyPlayer(iPlayer)
						lList = []
						for pLoopUnit in py.getUnitList():
							if not pLoopUnit.isOnlyDefensive():
								if pLoopUnit.getUnitType() != iDuin:
									if pLoopUnit.isAlive():
										lList.append(pLoopUnit)
						for pLoopUnit in lList:
							iUnit = pLoopUnit.getUnitType()
							newUnit = pPlayer.initUnit(iWerewolf, pLoopUnit.getX(), pLoopUnit.getY(), UnitAITypes.NO_UNITAI, DirectionTypes.DIRECTION_SOUTH)
							newUnit.convert(pLoopUnit)
							newUnit.setScenarioCounter(iUnit)
				if pPlayer.getCivilizationType() == iLjosalfar or pPlayer.getCivilizationType() == iSvartalfar:
					if gc.getGame().getScenarioCounter() == 0:
						if CyGame().getSorenRandNum(100, "Immigrants") < 10:
							lList = []
							for pyCity in PyPlayer(iPlayer).getCityList():
								pCity = pyCity.GetCy()
								lList.append(pCity)
							if len(lList) > 0:
								pCity = lList[CyGame().getSorenRandNum(len(lList), "Pick City")-1]
								triggerData = pPlayer.initTriggeredData(iImmigrants, true, -1, pCity.getX(), pCity.getY(), iPlayer, pCity.getID(), -1, -1, -1, -1)
	
	def gameStart(self):
		gc.getGame().incrementUnitClassCreatedCount(gc.getInfoTypeForString('UNITCLASS_LUCIAN'))
		gc.getGame().incrementUnitClassCreatedCount(gc.getInfoTypeForString('UNITCLASS_TROJAN_HORSE'))
		gc.getTeam(0).setPermanentWarPeace(1, True)
		gc.getTeam(1).setPermanentWarPeace(0, True)
				
	def getIntro(self):
		introText = "TXT_KEY_WB_THE_SPLINTERED_COURT_INTRO"
		return CyTranslator().getText(introText,())
	
	def getIntroArt(self):
		return 'art/interface/popups/The Splintered Court.dds'
		
	def cannotResearch(self, ePlayer, eTech, bTrade):
		pPlayer = gc.getPlayer(ePlayer)
		return false
		
	def cannotTrain(self, pCity, eUnit, bContinue, bTestVisible, bIgnoreCost, bIgnoreUpgrades):
		ePlayer = pCity.getOwner()
		pPlayer = gc.getPlayer(ePlayer)
		return false
	
	def getGoalTag(self, pPlayer):
		szBuffer = u"<font=2>"
		if pPlayer.getCivilizationType() == gc.getInfoTypeForString('CIVILIZATION_LJOSALFAR'):
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_WB_THE_SPLINTERED_COURT_LJOSALFAR_GOAL", (), gc.getInfoTypeForString("COLOR_RED"))
		if pPlayer.getCivilizationType() == gc.getInfoTypeForString('CIVILIZATION_SVARTALFAR'):
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_WB_THE_SPLINTERED_COURT_SVARTALFAR_GOAL", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 0:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_DAWN", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 1:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_MORNING", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 2:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_NOON", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 3:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_AFTERNOON", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 4:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_DUSK", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 5:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_EARLY_NIGHT", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 6:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_MIDNIGHT", (), gc.getInfoTypeForString("COLOR_RED"))
		if gc.getGame().getScenarioCounter() == 7:
			szBuffer = szBuffer + CyTranslator().getColorText("TXT_KEY_MESSAGE_DAY_CYCLE_LATE_NIGHT", (), gc.getInfoTypeForString("COLOR_RED"))
		return szBuffer
		
	def onCityAcquired(self, iPreviousOwner, iNewOwner, pCity, bConquest, bTrade):
		pPlayer = gc.getPlayer(iNewOwner)
	
	def onCityBuilt(self, pCity):
		iPlayer = pCity.getOwner()
		pPlayer = gc.getPlayer(iPlayer)
		
	def onCityRazed(self, city, iPlayer):
		pass
		
	def onImprovementDestroyed(self, iImprovement, iOwner, iX, iY):
		pass
		
	def atRangeJungleAltar(self, pCaster, pPlot):
		pass
		
	def onMoveJungleAltar(self, pCaster, pPlot):
		pass
	
	def onMoveMirrorOfHeaven(self, pCaster, pPlot):
		pass
		
	def onMovePortal(self, pCaster, pPlot):
		pass
	
	def onMoveWarningPost(self, pCaster, pPlot):
		pass
		
	def onReligionFounded(self, iReligion, iFounder):
		pass
		
	def onTechAcquired(self, iTechType, iTeam, iPlayer, bAnnounce):
		pass
		
	def onUnitCreated(self, pUnit):
		pass
		
	def onUnitKilled(self, pUnit):
		pass
	
	def onVictory(self, iPlayer, iVictory):
		pPlayer = gc.getPlayer(iPlayer)
		gc.getGame().changeTrophyValue("TROPHY_WB_THE_SPLINTERED_COURT", 1)
		if pPlayer.getCivilizationType() == gc.getInfoTypeForString('CIVILIZATION_LJOSALFAR'):
			gc.getGame().setTrophyValue("TROPHY_WB_THE_SPLINTERED_COURT_LJOSALFAR", 1)
			gc.getGame().setTrophyValue("TROPHY_WB_THE_SPLINTERED_COURT_SVARTALFAR", 0)
			csf.addPopupWB(CyTranslator().getText("TXT_KEY_WB_THE_SPLINTERED_COURT_VICTORY_LJOSALFAR",()), 'art/interface/popups/The Splintered Court Ljosalfar Victory.dds')
		if pPlayer.getCivilizationType() == gc.getInfoTypeForString('CIVILIZATION_SVARALFAR'):
			gc.getGame().setTrophyValue("TROPHY_WB_THE_SPLINTERED_COURT_LJOSALFAR", 0)
			gc.getGame().setTrophyValue("TROPHY_WB_THE_SPLINTERED_COURT_SVARTALFAR", 1)
			csf.addPopupWB(CyTranslator().getText("TXT_KEY_WB_THE_SPLINTERED_COURT_VICTORY_SVARTALFAR",()), 'art/interface/popups/The Splintered Court Svartalfar Victory.dds')
			
	def openChest(self, caster, pPlot):
		pass
		
	def playerDefeated(self, pPlayer):
		bLose = False
		if pPlayer.getCivilizationType() == gc.getInfoTypeForString('CIVILIZATION_LJOSALFAR'):
			bLose = True
			iWinningTeam = 2
			if gc.getPlayer(0).isAlive():
				bLose = False
			if gc.getPlayer(1).isAlive():
				bLose = False
			if gc.getPlayer(2).isAlive():
				bLose = False
		if pPlayer.getCivilizationType() == gc.getInfoTypeForString('CIVILIZATION_SVARTALFAR'):
			bLose = True
			iWinningTeam = 1
			if gc.getPlayer(3).isAlive():
				bLose = False
			if gc.getPlayer(4).isAlive():
				bLose = False
			if gc.getPlayer(5).isAlive():
				bLose = False
		iHumanPlayer = -1
		for iLoopPlayer in range(gc.getMAX_PLAYERS()):
			pLoopPlayer = gc.getPlayer(iLoopPlayer)
			if pLoopPlayer.isAlive():
				if pLoopPlayer.isHuman():
					if pLoopPlayer.getCivilizationType() != pPlayer.getCivilizationType():
						iHumanPlayer = iLoopPlayer
		if iHumanPlayer != -1:
			if pPlayer.getLeaderType() == gc.getInfoTypeForString('LEADER_AMELANCHIER'):
				iEvent = gc.getInfoTypeForString('EVENTTRIGGER_WB_SPLINTERED_COURT_DEFEATED_AMELANCHIER')
				triggerData = gc.getPlayer(iHumanPlayer).initTriggeredData(iEvent, true, -1, -1, -1, iHumanPlayer, -1, -1, -1, -1, -1)
			if pPlayer.getLeaderType() == gc.getInfoTypeForString('LEADER_THESSA'):
				iEvent = gc.getInfoTypeForString('EVENTTRIGGER_WB_SPLINTERED_COURT_DEFEATED_THESSA')
				triggerData = gc.getPlayer(iHumanPlayer).initTriggeredData(iEvent, true, -1, -1, -1, iHumanPlayer, -1, -1, -1, -1, -1)
			if pPlayer.getLeaderType() == gc.getInfoTypeForString('LEADER_RIVANNA'):
				iEvent = gc.getInfoTypeForString('EVENTTRIGGER_WB_SPLINTERED_COURT_DEFEATED_RIVANNA')
				triggerData = gc.getPlayer(iHumanPlayer).initTriggeredData(iEvent, true, -1, -1, -1, iHumanPlayer, -1, -1, -1, -1, -1)
			if pPlayer.getLeaderType() == gc.getInfoTypeForString('LEADER_VOLANNA'):
				iEvent = gc.getInfoTypeForString('EVENTTRIGGER_WB_SPLINTERED_COURT_DEFEATED_VOLANNA')
				triggerData = gc.getPlayer(iHumanPlayer).initTriggeredData(iEvent, true, -1, -1, -1, iHumanPlayer, -1, -1, -1, -1, -1)
		if pPlayer.getLeaderType() == gc.getInfoTypeForString('LEADER_ARENDEL'):
			if not bLose:
				cf.addPopup(CyTranslator().getText("TXT_KEY_WB_THE_SPLINTERED_WOOD_DEFEATED_ARENDEL",()),'art/interface/popups/Arendel.dds')
		if pPlayer.getLeaderType() == gc.getInfoTypeForString('LEADER_FAERYL'):
			if not bLose:
				cf.addPopup(CyTranslator().getText("TXT_KEY_WB_THE_SPLINTERED_WOOD_DEFEATED_FAERYL",()),'art/interface/popups/Faeryl.dds')
		if bLose:
			gc.getGame().setWinner(iWinningTeam, 2)


Assuming you agree this is a good idea, how would you like me to get my files to you? (You don't want to have to duplicate what I've done). Note that this is up through patch 'd', so you'd have to merge it with 'e' before working on 'f'. I have not created a SCENARIO file for Wages of Sin since it didn't look anywhere near completion.

Do you have a version-ing system I could just check these files into or something? Let me know.
 
I appreciate the thought, but I'd rather have it like it is. I did consider having each scenario run out of a seperate python file in the begining but I opted for having them share a file because it was more convienant for me and there will be functions they share.
 
Well, the functions they share should be placed into a separate document. Like I said put the shared functions into a CustomScenarioFunctions document for easy editing and perusal.

Not only does my version cut down on the amount of code you have to write to make changes, but it makes it easier to debug (One additional layer of stack) and read (variables in a scope are always in reference to your scenario, just for starters).

I mean, how many times is:

Code:
if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_AGAINST_THE_GREY)

Written in that code database?
 
I mean, how many times is:

Code:
if gc.getGame().isOption(GameOptionTypes.GAMEOPTION_WB_AGAINST_THE_GREY)

Written in that code database?

Twice. And Id opt for those 2 lines rather than having to deal with multiple files. Its just personal preference though. I dont have any problem with your system, the current way is just the way I prefer. And neither option provides any in game advantage.

I dont think this system contributes to or helps prevent any reliability issues as you mentioned. Thats just a factor of being the first week of release. If they were in the same file or different the errors would still be there (because the buggy programmer is the same either way).
 
Twice. And Id opt for those 2 lines rather than having to deal with multiple files. Its just personal preference though. I dont have any problem with your system, the current way is just the way I prefer. And neither option provides any in game advantage.

I dont think this system contributes to or helps prevent any reliability issues as you mentioned. Thats just a factor of being the first week of release. If they were in the same file or different the errors would still be there (because the buggy programmer is the same either way).

I didn't mean specifically for Against the Grey :p. Anyhow, I don't want to beat a dead horse, but the thing my version fixes IS the buggy programmer. Readable, maintainable code goes along way to make a programmer look like a master :lol:. (Note that 'GAMEOPTION_WB_' appears 106 times).

Just sad now that I did that work for potentially naught. Maybe after all the scenarios are out, so that fans can add new ones fairly easily?
 
I would suggest that the current system is fine for what FFH is trying to accomplish. However, these changes may be valuable in Fall Further...
 
I chime in and would like to say that the current system is extremely painful in terms of merging changes. When you issue a patch, if someone wants to modify the ScenarioFunctions file for a scenario, it's a pain.
You don't even have to split the files (although I think it would be cleaner), but make each scenario a subclass. The doTurn are already different functions.
It would
1) Make the design of a scenario way easier for external people with regards to patching and merging.
2) Remove 109 'if' tests from the file to leave only 12 (one per scenario).
3) By consequence, make the game slightly faster by not checking 12 if's each doTurn and making such checks other functions (although admittedly this is nitpicking)
 
I would suggest that the current system is fine for what FFH is trying to accomplish. However, these changes may be valuable in Fall Further...

I had similar thoughts when I first read it - and we do plan to have some scenarios sooner or later...
 
Top Bottom