Removing, reducing or modifying peaks

civcivv

Warlord
Joined
Dec 1, 2009
Messages
126
Location
London
Been rather annoyed at how peaks are generated and wanted to look into doing some modification.

I was wondering whether it is possible to modify the amount of peaks that a python mapscript generates? Could I set a certain threshold above which hills turn into peaks or turn a percentage of generated peaks into hills?

Also, is there a way to not have peaks on the only tile that links two landmasses?

Any help on any of these points would be most appreciated.
 
I think in most cases it depends on the map script. In my script PerfectWorld, there are variables at the beginning that you can adjust, but it's slightly complicated because one variable is a percentage of land squares, and another is a random chance that goes up as the land tile increases in altitude.
 
Thanks for your reply.
Would it be possible for you to extract the relevant variables for me as I am still learning how to work on mapscripts? I saw some in some default mapscripts too and didn't quite understand which lines were needed and which were not.
 
Thanks for your reply.
Would it be possible for you to extract the relevant variables for me as I am still learning how to work on mapscripts? I saw some in some default mapscripts too and didn't quite understand which lines were needed and which were not.

In my script there are a bunch of tunable variables right at the beginning, and they are all very well commented. Have a look at it and feel free to ask questions.
 
Gone through your code now, would these be all I need to plug in (and where in the script would these need to be added) or am I missing pieces that are crucial to correct generation?

Code:
        #How many land squares will be above peak threshold and thus 'peaks'.
        self.PeakPercent = 0.12

        #How many land squares will be above hill threshold and thus 'hills' unless
        #they are also above peak threshold in which case they will be 'peaks'.
        self.HillPercent = 0.35

        #In addition to the relative peak and hill generation, there is also a
        #process that changes flats to hills or peaks based on altitude. This tends
        #to randomize the high altitude areas somewhat and improve their appearance.
        #These variables control the frequency of hills and peaks at the highest altitude.
        self.HillChanceAtOne = .50
        self.PeakChanceAtOne = .27

    def createPlotMap(self):
        print "creating plot map"
        self.plotMap = array('i')
        #create height difference map to allow for tuning
        diffMap = array('d')
        for i in range(0,mc.height*mc.width):
            diffMap.append(0.0)
        #I tried using a deviation from surrounding average altitude
        #to determine hills and peaks but I didn't like the
        #results. Therefore I an using lowest neighbor
        for y in range(mc.height):
            for x in range(mc.width):
                i = GetIndex(x,y)
                myAlt = self.heightMap[i]
                minAlt = 1.0
                for direction in range(1,9,1):
                    xx,yy = GetXYFromDirection(x,y,direction)
                    ii = GetIndex(xx,yy)
                    if ii == -1:
                        continue
                    if self.heightMap[ii] < minAlt:
                        minAlt = self.heightMap[ii]
                diffMap[i] = myAlt - minAlt

        NormalizeMap(diffMap,mc.width,mc.height)

        #zero out water tiles so percent is percent of land
        for y in range(mc.height):
            for x in range(mc.width):
                i = GetIndex(x,y)
                if self.isBelowSeaLevel(x,y):
                    diffMap[i] = 0.0
                    
        peakHeight = FindValueFromPercent(diffMap,mc.width,mc.height,mc.PeakPercent,0.01,True)
        hillHeight = FindValueFromPercent(diffMap,mc.width,mc.height,mc.HillPercent,0.01,True)

        self.plotMap = array('i')
        #initialize map with 0CEAN
        for i in range(0,mc.height*mc.width):
            self.plotMap.append(mc.OCEAN)
        for y in range(mc.height):
            for x in range(mc.width):
                i = GetIndex(x,y)
                altDiff = diffMap[i]
                if self.heightMap[i] < hm.seaLevel:
                    self.plotMap[i] = mc.OCEAN
                elif altDiff < hillHeight:
                    self.plotMap[i] = mc.LAND
                elif altDiff < peakHeight:
                    self.plotMap[i] = mc.HILLS
                else:
                    self.plotMap[i] = mc.PEAK

        #Randomize high altitude areas
        for y in range(mc.height):
            for x in range(mc.width):
                i = GetIndex(x,y)
                if self.plotMap[i] == mc.LAND:
                    randomNum = PRand.random()
                    if randomNum < mc.PeakChanceAtOne * self.getAltitudeAboveSeaLevel(x,y):
                        self.plotMap[i] = mc.PEAK
                    elif randomNum < mc.HillChanceAtOne * self.getAltitudeAboveSeaLevel(x,y):
                        self.plotMap[i] = mc.HILLS

        return

Aiming to work on a custom Earth3 that I posted.
 
Gone through your code now, would these be all I need to plug in (and where in the script would these need to be added) or am I missing pieces that are crucial to correct generation?

Aiming to work on a custom Earth3 that I posted.

Hmm, I think I misunderstood your question. Really, when you sit down to write a map script yourself, you can place peaks any old way you wan't pretty much. I have no idea how Earth3 does it. That would likely be very different than the way I do it. I don't remember if there is anything in the XML that would control that, because I ignore that in my scripts.

I would definately not try to just merge my code with someone elses. It would just be a big mess. :crazyeye:

EDIT: check out the comments in the python file CvMapScriptInterface.py. This file gives a good rundown on what is expected in a map script.
 
Well Earth3 really is Earth2 with some placement changes :)

I had a look at the file and what I can find is:
normalizeRemovePeaks() or def normalizeRemovePeaks():

But I'm not sure where exactly to call this. The python file notes to call it after start plot setting.

So do I have to start a new section after player start generation or can I append it afterwards as a new section?

This is my start plot generation code:
Code:
# Starting position generation.
def findStartingPlot(argsList):
    gc = CyGlobalContext()
    map = CyMap()
    map.recalculateAreas()
    dice = gc.getGame().getMapRand()
    iPlayers = gc.getGame().countCivPlayersEverAlive()
    areas = CvMapGeneratorUtil.getAreas()
    areaValue = {}
    [playerID] = argsList

    if iPlayers > CyGlobalContext().getMAX_PLAYERS()-1:
        bSuccessFlag = False
        CyPythonMgr().allowDefaultImpl()
        return

        bestAreaValue = 0
        global bestArea
        bestArea = -1
        for area in areas:
                    if area.isWater(): continue
                    if area.getNumTiles() < 12: continue
        areaValue[area.getID()] = area.calculateTotalBestNatureYield() + area.getNumRiverEdges() + 2 * area.countCoastalLand() + 3 * area.countNumUniqueBonusTypes()
        players = 2*area.getNumStartingPlots()

        #Avoid single players on landmasses:
        value = areaValue[area.getID()] / (1 + 2*players )
        if (value > bestAreaValue):
            bestAreaValue = value;
            bestArea = area.getID()

        def isValid(playerID, x, y):
            global bestArea
            plot = CyMap().plot(x,y)
            if (plot.getArea() != bestArea):
                return false
            if (plot.getLatitude() >= 75):
                return false
            return true
        findstart = CvMapGeneratorUtil.findStartingPlot(playerID,isValid)
        sPlot = map.plotByIndex(findstart)
        player.setStartingPlot(sPlot,true)

    return None

Looking at the rest of the code, maybe as a new section and simply like this after return None?

Code:
def findStartingPlot(argsList):
    <SNIP>
    return None

#Removing peaks
def normalizeRemovePeaks():
    return 1

Or somesuch?

Thanks
 
Actually you don't call those functions in the CvMapScriptInterface file. Those functions are called by Civ, so if you don't want to mess with that feature in your map script, you just don't put it in and the default routine will be called.

A map script is basically a collection of functions that are called by Civ. NormalizeRemovePeaks is called after start positions are decided. If you want to have your own code there, you put a NormalizeRemovePeaks function in your mapscript, if you want the default function, then just don't put that function in the script. The code that you posted would basically disable the removing of peaks around start plots.

From what you have described, I would guess that the function you are interested in would be generatePlotTypes. Every map script has to have one and that's where hills, peaks and such are decided.
 
Hmmm ok :crazyeye:

But what happens when the mapscript for Earth2 or Earth3 both do not specify anything about peaks in that function?

Spoiler :
Code:
def generatePlotTypes():
    NiTextOut("Setting Plot Types (Python Earth2) ...")
    # Call generatePlotsByRegion() function, from TerraMultilayeredFractal subclass.
    global plotgen
    plotgen = EarthMultilayeredFractal()
    return plotgen.generatePlotsByRegion()

class Earth2TerrainGenerator(CvMapGeneratorUtil.TerrainGenerator):
        def __init__(self, iDesertPercent=40, iPlainsPercent=26,
	             fSnowLatitude=0.82, fTundraLatitude=0.75,
	             fGrassLatitude=0.1, fDesertBottomLatitude=0.1,
	             fDesertTopLatitude=0.3, fracXExp=-1,
	             fracYExp=-1, grain_amount=3):
                self.gc = CyGlobalContext()
		self.map = CyMap()

		grain_amount += self.gc.getWorldInfo(self.map.getWorldSize()).getTerrainGrainChange()
		
		self.grain_amount = grain_amount

		self.iWidth = self.map.getGridWidth()
		self.iHeight = self.map.getGridHeight()

		self.mapRand = self.gc.getGame().getMapRand()
		
		self.iFlags = 0  # Disallow FRAC_POLAR flag, to prevent "zero row" problems.
		if self.map.isWrapX(): self.iFlags += CyFractal.FracVals.FRAC_WRAP_X
		if self.map.isWrapY(): self.iFlags += CyFractal.FracVals.FRAC_WRAP_Y

		self.deserts=CyFractal()
		self.plains=CyFractal()
		self.variation=CyFractal()

		iDesertPercent += self.gc.getClimateInfo(self.map.getClimate()).getDesertPercentChange()
		iDesertPercent = min(iDesertPercent, 100)
		iDesertPercent = max(iDesertPercent, 0)

		self.iDesertPercent = iDesertPercent
		self.iPlainsPercent = iPlainsPercent

		self.iDesertTopPercent = 100
		self.iDesertBottomPercent = max(0,int(100-iDesertPercent))
		self.iPlainsTopPercent = 100
		self.iPlainsBottomPercent = max(0,int(100-iDesertPercent-iPlainsPercent))
		self.iMountainTopPercent = 75
		self.iMountainBottomPercent = 60

		fSnowLatitude += self.gc.getClimateInfo(self.map.getClimate()).getSnowLatitudeChange()
		fSnowLatitude = min(fSnowLatitude, 1.0)
		fSnowLatitude = max(fSnowLatitude, 0.0)
		self.fSnowLatitude = fSnowLatitude

		fTundraLatitude += self.gc.getClimateInfo(self.map.getClimate()).getTundraLatitudeChange()
		fTundraLatitude = min(fTundraLatitude, 1.0)
		fTundraLatitude = max(fTundraLatitude, 0.0)
		self.fTundraLatitude = fTundraLatitude

		fGrassLatitude += self.gc.getClimateInfo(self.map.getClimate()).getGrassLatitudeChange()
		fGrassLatitude = min(fGrassLatitude, 1.0)
		fGrassLatitude = max(fGrassLatitude, 0.0)
		self.fGrassLatitude = fGrassLatitude

		fDesertBottomLatitude += self.gc.getClimateInfo(self.map.getClimate()).getDesertBottomLatitudeChange()
		fDesertBottomLatitude = min(fDesertBottomLatitude, 1.0)
		fDesertBottomLatitude = max(fDesertBottomLatitude, 0.0)
		self.fDesertBottomLatitude = fDesertBottomLatitude

		fDesertTopLatitude += self.gc.getClimateInfo(self.map.getClimate()).getDesertTopLatitudeChange()
		fDesertTopLatitude = min(fDesertTopLatitude, 1.0)
		fDesertTopLatitude = max(fDesertTopLatitude, 0.0)
		self.fDesertTopLatitude = fDesertTopLatitude
		
		self.fracXExp = fracXExp
		self.fracYExp = fracYExp

		self.initFractals()
		
	def initFractals(self):
		self.deserts.fracInit(self.iWidth, self.iHeight, self.grain_amount, self.mapRand, self.iFlags, self.fracXExp, self.fracYExp)
		self.iDesertTop = self.deserts.getHeightFromPercent(self.iDesertTopPercent)
		self.iDesertBottom = self.deserts.getHeightFromPercent(self.iDesertBottomPercent)

		self.plains.fracInit(self.iWidth, self.iHeight, self.grain_amount+1, self.mapRand, self.iFlags, self.fracXExp, self.fracYExp)
		self.iPlainsTop = self.plains.getHeightFromPercent(self.iPlainsTopPercent)
		self.iPlainsBottom = self.plains.getHeightFromPercent(self.iPlainsBottomPercent)

		self.variation.fracInit(self.iWidth, self.iHeight, self.grain_amount, self.mapRand, self.iFlags, self.fracXExp, self.fracYExp)

		self.terrainDesert = self.gc.getInfoTypeForString("TERRAIN_DESERT")
		self.terrainPlains = self.gc.getInfoTypeForString("TERRAIN_PLAINS")
		self.terrainIce = self.gc.getInfoTypeForString("TERRAIN_SNOW")
		self.terrainTundra = self.gc.getInfoTypeForString("TERRAIN_TUNDRA")
		self.terrainGrass = self.gc.getInfoTypeForString("TERRAIN_GRASS")

	def getLatitudeAtPlot(self, iX, iY):
		"""given a point (iX,iY) such that (0,0) is in the NW,
		returns a value between 0.0 (tropical) and 1.0 (polar).
		This function can be overridden to change the latitudes; for example,
		to make an entire map have temperate terrain, or to make terrain change from east to west
		instead of from north to south"""
		lat = abs((self.iHeight / 2) - iY)/float(self.iHeight/2) # 0.0 = equator, 1.0 = pole

		# Adjust latitude using self.variation fractal, to mix things up:
		lat += (128 - self.variation.getHeight(iX, iY))/(255.0 * 5.0)

		# Limit to the range [0, 1]:
		if lat < 0:
			lat = 0.0
		if lat > 1:
			lat = 1.0

		return lat

	def generateTerrain(self):		
		terrainData = [0]*(self.iWidth*self.iHeight)
		for x in range(self.iWidth):
			for y in range(self.iHeight):
				iI = y*self.iWidth + x
				terrain = self.generateTerrainAtPlot(x, y)
				terrainData[iI] = terrain
		return terrainData

	def generateTerrainAtPlot(self,iX,iY):
		lat = self.getLatitudeAtPlot(iX,iY)

		if (self.map.plot(iX, iY).isWater()):
			return self.map.plot(iX, iY).getTerrainType()

		terrainVal = self.terrainGrass

		if lat >= self.fSnowLatitude:
			terrainVal = self.terrainIce
		elif lat >= self.fTundraLatitude:
			terrainVal = self.terrainTundra
		elif lat < self.fGrassLatitude:
			terrainVal = self.terrainGrass
		else:
			desertVal = self.deserts.getHeight(iX, iY)
			plainsVal = self.plains.getHeight(iX, iY)
			if ((desertVal >= self.iDesertBottom) and (desertVal <= self.iDesertTop) and (lat >= self.fDesertBottomLatitude) and (lat < self.fDesertTopLatitude)):
				terrainVal = self.terrainDesert
			elif ((plainsVal >= self.iPlainsBottom) and (plainsVal <= self.iPlainsTop)):
				terrainVal = self.terrainPlains

		if (terrainVal == TerrainTypes.NO_TERRAIN):
			return self.map.plot(iX, iY).getTerrainType()

		return terrainVal

The closest I can gather is MountainTopPercent but that doesn't sound like peaks to me I think. But maybe I'm wrong?


This is the function from your script:
Code:
def generatePlotTypes():
    gc = CyGlobalContext()
    mmap = gc.getMap()
    mc.width = mmap.getGridWidth()
    mc.height = mmap.getGridHeight()
    mc.minimumMeteorSize = (1 + int(round(float(mc.hmWidth)/float(mc.width)))) * 3
    PRand.seed()
    hm.performTectonics()
    hm.generateHeightMap()
    hm.combineMaps()
    hm.calculateSeaLevel()
    hm.fillInLakes()
    pb.breakPangaeas()
##    hm.Erode()
##    hm.printHeightMap()
    hm.addWaterBands()
##    hm.printHeightMap()
    cm.createClimateMaps()
    sm.initialize()
    rm.generateRiverMap()
    plotTypes = [PlotTypes.PLOT_OCEAN] * (mc.width*mc.height)

    for i in range(mc.width*mc.height):
        mapLoc = sm.plotMap[i]
        if mapLoc == mc.PEAK:
            plotTypes[i] = PlotTypes.PLOT_PEAK
        elif mapLoc == mc.HILLS:
            plotTypes[i] = PlotTypes.PLOT_HILLS
        elif mapLoc == mc.LAND:
            plotTypes[i] = PlotTypes.PLOT_LAND
        else:
            plotTypes[i] = PlotTypes.PLOT_OCEAN
    print "Finished generating plot types."         
    return plotTypes

So maybe I should do something like?
Code:
        if mapLoc == mc.PEAK:
            plotTypes[i] = PlotTypes.PLOT_HILLS

Thanks for your help!
 
Ok, forget about the generatePlotTypes in my script. I do everything differently to what is normal and will only confuse.

In your Earth3 script, the return value comes from plotgen.generatePlotsByRegion() so all the work is happening inside that function. The plotgen variable is given by EarthMultilayeredFractal() which might actually be in a different file, possibly the terra mapscript. Somewhere there is a function called generatePlotsByRegion. Find that, and that will get you one step closer to where actual decisions are made.

EDIT: Are you trying to get rid of peaks entirely? Why is that?
 
Thanks, its indeed in the Terra script and called under:
Code:
class TerraMultilayeredFractal(CvMapGeneratorUtil.MultilayeredFractal):
	# Subclass. Only the controlling function overridden in this case.
	def generatePlotsByRegion(self):

However, in both Earth2 / Earth3, this function is also called right after starting point generation:
Code:
class EarthMultilayeredFractal(CvMapGeneratorUtil.MultilayeredFractal):
    # Subclass. Only the controlling function overridden in this case.
    def generatePlotsByRegion(self):

So I find it odd that towards the end it is then calling on Terra which basically has almost the same function all over again, just with regions slightly differently?
What I'm trying to say is that Earth 2 / Earth 3 both first call Earth's multilayerfractal and then at the end call terramultilayered fractal.

I'm really confused now.


In terms of my aim, I really just want to reduce the amount of peaks spawned or at least cause peaks to cluster rather than spread like a grid. If that is too complicated, I suppose I'd be happy to settle for turning peaks into hills or somesuch, given that my understanding on mapscripts is still rather limited :)

Thanks
 
Thanks, its indeed in the Terra script and called under:
Code:
class TerraMultilayeredFractal(CvMapGeneratorUtil.MultilayeredFractal):
	# Subclass. Only the controlling function overridden in this case.
	def generatePlotsByRegion(self):

However, in both Earth2 / Earth3, this function is also called right after starting point generation:
Code:
class EarthMultilayeredFractal(CvMapGeneratorUtil.MultilayeredFractal):
    # Subclass. Only the controlling function overridden in this case.
    def generatePlotsByRegion(self):

So I find it odd that towards the end it is then calling on Terra which basically has almost the same function all over again, just with regions slightly differently?
What I'm trying to say is that Earth 2 / Earth 3 both first call Earth's multilayerfractal and then at the end call terramultilayered fractal.

Thanks

I would not think that function would be called twice. It seems like you would either call one or the other and not both. What are the actual function calls?
 
Hmmm the forum has a character limit for posts...so I can't seem to post them here.

If you look at Earth2 in your BtS public maps folder though you can see what I mean. Essentially Earth2 generates the starting positions, then plotsbyregion and then goes on to generateplottypes which in turn refers to terra's plotsbyregion
 
I should say for completeness that, just because the functions appear in a certain order in the map script, does not mean that they are called in that order. The order is set in the CvMapScriptInterface file, read the comments there to see what is called and when. In the map script itself, it only matters that the function exists or not, not the order of appearance. For example, if you put an arbitrary function in the script, it will never be called unless you call it. It will sit there in memory unused.
 
Hmm in neither Terra nor Earth2 mapscripts is the word peak mentioned anywhere. Am I missing something or are peaks generated without a function call?

Thanks
 
Ok, I did some digging and it turns out this is all ultimately controlled from the XML.

I will point out that the EarthMultilayeredFractal class inherits(You'll want to research what that means, if you're interested in programming.) from the CvMapGeneratorUtil.MultilayeredFractal class from the CvMapGeneratorUtil.py file. Ultimately the work is done in a function in that class called generatePlotsInRegion, and that function looks up the iPeakPercent value from the CIV4ClimateInfo.xml file.

If you don't want to change the XML, you can override(This is related to inheritance) the generatePlotsInRegion function in your EarthMultilayeredFractal class and tweak it. Basically you copy the whole function from the other file, and change this value...
Code:
iPeakThreshold = regionPeaksFrac.getHeightFromPercent(self.gc.getClimateInfo([B][COLOR="Blue"]self.map.getClimate()).getPeakPercent()[/COLOR][/B])
On most climate settings, this value defaults to 25.
 
Ah, so it becomes a subclass of multilayeredfractal class?

Thanks so much for your help tracking this down and helping me understand the map scripts better!
 
Top Bottom