Are you, in fact, a pregnant lady who lives in the apartment next door to Superdeath's parents? - Commodore

Create an account  

 
Balanced Resources

Does anyone know exactly what the "Balanced Resources" option does, and where that specific code can be found? My general understanding is that it places the strategic resources (Copper, Iron, Horse, Coal/Oil?) and maybe Stone/Marble as well? within 7-8 tiles of your starting spot, but I am interested in an exact answer. Anyone know?

Thanks in advance!

"There is no wealth like knowledge. No poverty like ignorance."
Reply

Its map specific (the ability to turn the option on or off) and the actual balancing stuff is in CvMapGeneratorUtil.py. The latest version of which is installed under warlords.

snip ...

Code:
class BonusBalancer:
    def __init__(self):
        self.gc = CyGlobalContext()
        self.map = CyMap()
        
        self.resourcesToBalance = ('BONUS_ALUMINUM', 'BONUS_COAL', 'BONUS_COPPER', 'BONUS_HORSE', 'BONUS_IRON', 'BONUS_OIL', 'BONUS_URANIUM')
        self.resourcesToEliminate = ('BONUS_MARBLE', )
        
    def isSkipBonus(self, iBonusType):
        type_string = self.gc.getBonusInfo(iBonusType).getType()

        return ((type_string in self.resourcesToBalance) or (type_string in self.resourcesToEliminate))

        
    def isBonusValid(self, eBonus, pPlot, bIgnoreUniqueRange, bIgnoreOneArea, bIgnoreAdjacent):
        "Returns true if we can place a bonus here"

        iX, iY = pPlot.getX(), pPlot.getY()

        if (not bIgnoreOneArea) and self.gc.getBonusInfo(eBonus).isOneArea():
            if self.map.getNumBonuses(eBonus) > 0:
                if self.map.getArea(pPlot.getArea()).getNumBonuses(eBonus) == 0:
                    return False
                    
        if not bIgnoreAdjacent:
            for iI in range(DirectionTypes.NUM_DIRECTION_TYPES):
                pLoopPlot = plotDirection(iX, iY, DirectionTypes(iI))
                if not pLoopPlot.isNone():
                    if (pLoopPlot.getBonusType(-1) != -1) and (pLoopPlot.getBonusType(-1) != eBonus):
                        return False

        if not bIgnoreUniqueRange:
            uniqueRange = self.gc.getBonusInfo(eBonus).getUniqueRange()
            for iDX in range(-uniqueRange, uniqueRange+1):
                for iDY in range(-uniqueRange, uniqueRange+1):
                    pLoopPlot = plotXY(iX, iY, iDX, iDY)
                    if not pLoopPlot.isNone() and pLoopPlot.getBonusType(-1) == eBonus:
                        return False
        
        return True

    def normalizeAddExtras(self):
    
        for i in range(self.gc.getMAX_CIV_PLAYERS()):
            if (self.gc.getPlayer(i).isAlive()):
                start_plot = self.gc.getPlayer(i).getStartingPlot() # returns a CyPlot
                startx, starty = start_plot.getX(), start_plot.getY()
                
                plots = [] # build a list of the plots near the starting plot
                for dx in range(-5,6):
                    for dy in range(-5,6):
                        x,y = startx+dx, starty+dy
                        pLoopPlot = self.map.plot(x,y)
                        if not pLoopPlot.isNone():
                            plots.append(pLoopPlot)
                
                resources_placed = []
                for pass_num in range(4):
                    bIgnoreUniqueRange  = pass_num >= 1
                    bIgnoreOneArea         = pass_num >= 2
                    bIgnoreAdjacent     = pass_num >= 3
                    
                    for bonus in range(self.gc.getNumBonusInfos()):
                        type_string = self.gc.getBonusInfo(bonus).getType()
                        if (type_string not in resources_placed) and (type_string in self.resourcesToBalance):
                            for (pLoopPlot) in plots:
                                if (pLoopPlot.canHaveBonus(bonus, True)):
                                    if self.isBonusValid(bonus, pLoopPlot, bIgnoreUniqueRange, bIgnoreOneArea, bIgnoreAdjacent):
                                        pLoopPlot.setBonusType(bonus)
                                        resources_placed.append(type_string)
                                        #print "placed", type_string, "on pass", pass_num
                                        break # go to the next bonus
I have finally decided to put down some cash and register a website. It is www.ruffhi.com. Now I remain free to move the hosting options without having to change the name of the site.

(October 22nd, 2014, 10:52)Caledorn Wrote: And ruff is officially banned from playing in my games as a reward for ruining my big surprise by posting silly and correct theories in the PB18 tech thread.
Reply

It looks like it looks from -5 to +6 from your starting plot. So, in a 12 by 12 grid ... or 144 tiles.
I have finally decided to put down some cash and register a website. It is www.ruffhi.com. Now I remain free to move the hosting options without having to change the name of the site.

(October 22nd, 2014, 10:52)Caledorn Wrote: And ruff is officially banned from playing in my games as a reward for ruining my big surprise by posting silly and correct theories in the PB18 tech thread.
Reply

I seem to recall Sirian saying that the Balanced setting placed all the strategic resources within three "plots" of the start, a plot being a 4x4 grid of tiles. So yes, within 12 tiles would seem about right.

Does Balanced setting actually remove marble from the immediate starting location? (Am I reading that code right?) I never knew that....
Follow Sullla: Website | YouTube | Livestream | Twitter | Discord
Reply

Look carefully - the placement of resources is deterministic. A resource is always placed in the first place it is allowed to go, and the tiles are always scanned in the same order.
Reply

VoiceOfUnreason Wrote:Look carefully - the placement of resources is deterministic. A resource is always placed in the first place it is allowed to go, and the tiles are always scanned in the same order.
Could you explain how this works, for those of us who do not read Python (ie me smile ). Thanks in advance.

Thanks to everyone for the response, as well. While I was waiting for a response, I ran about 50 world builder tests and was able to confirm that the strategic resources will all appear within than 5 tiles away from your starting spot. And I did know about the removal of Marble, though do not know if it is a "bug" or a "feature."

"There is no wealth like knowledge. No poverty like ignorance."
Reply

I'll type something up that steps through the code tomorrow morning on the train. Also, this balancing class was added in Warlords, it is not in the vanilla Civ4 code.
I have finally decided to put down some cash and register a website. It is www.ruffhi.com. Now I remain free to move the hosting options without having to change the name of the site.

(October 22nd, 2014, 10:52)Caledorn Wrote: And ruff is officially banned from playing in my games as a reward for ruining my big surprise by posting silly and correct theories in the PB18 tech thread.
Reply

Firstly ...

YMMV Wrote:The following is based on my reading of the code and my understanding of python / civ / the code. I do not warrant that the below is accurate in any way. Rely on the following at your own risk. Further, even though, logically, an observation of a purple dog supports the logical statement that 'Only cows are black' as well as the statement that 'Only cows are white', I still don't understand why it makes any sort of sense.

Code:
class BonusBalancer:
    def __init__(self):
        self.gc = CyGlobalContext()
        self.map = CyMap()
        
        self.resourcesToBalance = ('BONUS_ALUMINUM', 'BONUS_COAL', 'BONUS_COPPER', 'BONUS_HORSE', 'BONUS_IRON', 'BONUS_OIL', 'BONUS_URANIUM')
        self.resourcesToEliminate = ('BONUS_MARBLE', )

This part of the code is executed when the BonusBalancer class is initialized. It sets up some key variables that hold various aspects. This class is 'called' by various map scripts. For example, from Highlands.py ...

Code:
from CvMapGeneratorUtil import BonusBalancer

balancer = BonusBalancer()

<snip>

def normalizeAddExtras():
    if (CyMap().getCustomMapOption(4) == 1):
        balancer.normalizeAddExtras()
    CyPythonMgr().allowDefaultImpl()    # do the rest of the usual normalizeStartingPlots stuff, don't overrride

def addBonusType(argsList):
    [iBonusType] = argsList
    gc = CyGlobalContext()
    type_string = gc.getBonusInfo(iBonusType).getType()

    if (CyMap().getCustomMapOption(4) == 1):
        if (type_string in balancer.resourcesToBalance) or (type_string in balancer.resourcesToEliminate):
            return None # don't place any of this bonus randomly
        
    CyPythonMgr().allowDefaultImpl() # pretend we didn't implement this method, and let C handle this bonus in the default way

The addBonusType part checks if the 'balanced resources' option is checked and, if it is, does not place any resources contained in balancer.resourcesToBalance (copper, iron, horses, etc) or balancer.resourcesToEliminate (Marble). If the resource is not in either of these lists (ie corn), then it places the resource as per normal.

The normalizeAddExtras portion executes the balancing resources portion based on the custom map option that the user selects when starting a custom game (ie 'balanced resources' option is checked).

Code:
def normalizeAddExtras(self):
    
    for i in range(self.gc.getMAX_CIV_PLAYERS()):
        if (self.gc.getPlayer(i).isAlive()):
            start_plot = self.gc.getPlayer(i).getStartingPlot() # returns a CyPlot
            startx, starty = start_plot.getX(), start_plot.getY()
                
            plots = [] # build a list of the plots near the starting plot
            for dx in range(-5,6):
                for dy in range(-5,6):
                    x,y = startx+dx, starty+dy
                    pLoopPlot = self.map.plot(x,y)
                    if not pLoopPlot.isNone():
                        plots.append(pLoopPlot)
                
            resources_placed = []
            for pass_num in range(4):
                bIgnoreUniqueRange  = pass_num >= 1
                bIgnoreOneArea         = pass_num >= 2
                bIgnoreAdjacent     = pass_num >= 3
                    
                for bonus in range(self.gc.getNumBonusInfos()):
                    type_string = self.gc.getBonusInfo(bonus).getType()
                    if (type_string not in resources_placed) and (type_string in self.resourcesToBalance):
                        for (pLoopPlot) in plots:
                            if (pLoopPlot.canHaveBonus(bonus, True)):
                                if self.isBonusValid(bonus, pLoopPlot, bIgnoreUniqueRange, bIgnoreOneArea, bIgnoreAdjacent):
                                    pLoopPlot.setBonusType(bonus)
                                    resources_placed.append(type_string)
                                    #print "placed", type_string, "on pass", pass_num

Lets step through this bit by bit ...

Code:
for i in range(self.gc.getMAX_CIV_PLAYERS()):
    if (self.gc.getPlayer(i).isAlive()):
        start_plot = self.gc.getPlayer(i).getStartingPlot() # returns a CyPlot
        startx, starty = start_plot.getX(), start_plot.getY()

The balancing code loops over all players that are currently alive. It also stores the starting plot of the player.

Code:
plots = [] # build a list of the plots near the starting plot
for dx in range(-5,6):
    for dy in range(-5,6):
        x,y = startx+dx, starty+dy
        pLoopPlot = self.map.plot(x,y)
        if not pLoopPlot.isNone():
            plots.append(pLoopPlot)

The first line initializes an array called 'plots' (ie empty). It then loops over a grid of 144 plots (12 x 12) where the starting plot is at the middle of the grid. If the plot is valid (ie not off the edge of the map), then the plot is added to the 'plots' array.

Code:
resources_placed = []
for pass_num in range(4):
    bIgnoreUniqueRange = pass_num >= 1
    bIgnoreOneArea     = pass_num >= 2
    bIgnoreAdjacent    = pass_num >= 3

This part initializes an array called 'resources_placed' (ie empty). It loops over the included code 4 times, counting each time as a pass in 'pass_num'. It also sets up some boolean (true / false) variables:

bIgnoreUniqueRange - False in the first pass, True in the others
bIgnoreOneArea - False in the first and second pass, True in the others
bIgnoreAdjacent - False in the first, second and third pass, True in the others

Code:
for bonus in range(self.gc.getNumBonusInfos()):
    type_string = self.gc.getBonusInfo(bonus).getType()
    if (type_string not in resources_placed) and (type_string in self.resourcesToBalance):
        for (pLoopPlot) in plots:
            if (pLoopPlot.canHaveBonus(bonus, True)):
                if self.isBonusValid(bonus, pLoopPlot, bIgnoreUniqueRange, bIgnoreOneArea, bIgnoreAdjacent):
                    pLoopPlot.setBonusType(bonus)
                    resources_placed.append(type_string)
                    #print "placed", type_string, "on pass", pass_num

Here, finally is the guts of the code. This does the following:

loop over all resources
if the resource has not been placed AND it is in the resources to place
then loop over all of the plots you stored above
if the plot can have the bonus (ie not copper on peak)
and the bonus is valid (see below) then
- place the resource

Well, that is pretty straight forward. And, according to my reading of the code, marble will NOT be placed when balanced resources is selected. Wow - I didn't know that.

Oh - one final thing ... 'isBonusValid' ...

Code:
def isBonusValid(self, eBonus, pPlot, bIgnoreUniqueRange, bIgnoreOneArea, bIgnoreAdjacent):
    "Returns true if we can place a bonus here"

    iX, iY = pPlot.getX(), pPlot.getY()

    if (not bIgnoreOneArea) and self.gc.getBonusInfo(eBonus).isOneArea():
        if self.map.getNumBonuses(eBonus) > 0:
            if self.map.getArea(pPlot.getArea()).getNumBonuses(eBonus) == 0:
                return False

The bonus is NOT valid if
- it is the 1st or 2nd pass and the bonus has a 'OneArea' restriction (I have no idea - read from XML)
- if the map already has a bonus of this type
- if the map area (no idea what this is about) already has a bonus of this type

Code:
    if not bIgnoreAdjacent:
        for iI in range(DirectionTypes.NUM_DIRECTION_TYPES):
            pLoopPlot = plotDirection(iX, iY, DirectionTypes(iI))
            if not pLoopPlot.isNone():
                if (pLoopPlot.getBonusType(-1) != -1) and (pLoopPlot.getBonusType(-1) != eBonus):
                    return False

The bonus is NOT valid if
- it is the 1st, 2nd or 3rd pass
- if any plot in a certain range (I think this is the 8 surrounding plots) already contains any resource

Code:
    if not bIgnoreUniqueRange:
        uniqueRange = self.gc.getBonusInfo(eBonus).getUniqueRange()
        for iDX in range(-uniqueRange, uniqueRange+1):
            for iDY in range(-uniqueRange, uniqueRange+1):
                pLoopPlot = plotXY(iX, iY, iDX, iDY)
                if not pLoopPlot.isNone() and pLoopPlot.getBonusType(-1) == eBonus:
                    return False

The bonus is NOT valid if
- it is the 1st pass
- if any plot in a certain range already contains this resource

Code:
    return True

Otherwise, the bonus is valid.
I have finally decided to put down some cash and register a website. It is www.ruffhi.com. Now I remain free to move the hosting options without having to change the name of the site.

(October 22nd, 2014, 10:52)Caledorn Wrote: And ruff is officially banned from playing in my games as a reward for ruining my big surprise by posting silly and correct theories in the PB18 tech thread.
Reply

Speaker Wrote:Could you explain how this works, for those of us who do not read Python (ie me smile ). Thanks in advance.

Speaking very roughly (and I don't promise accurately - I'm going from memory here)

Code:
plots = [] # build a list of the plots near the starting plot
for dx in range(-5,6):
    for dy in range(-5,6):
        x,y = startx+dx, starty+dy
        pLoopPlot = self.map.plot(x,y)
        if not pLoopPlot.isNone():
                plots.append(pLoopPlot)

This code here is building a square around the starting plot (the range function includes the lower boundary but not the upper one, so the coordinates dx and dy each run from -5 to +5 (so 11x11, not 12x12). The first plot in the list is bottom left, and we move all the way up the left most column, then scan the next column from the bottom, and so on until we scan the right most column and end with the upper right corner.

The key point - the same order is always used.

Code:
for (pLoopPlot) in plots:
    if (pLoopPlot.canHaveBonus(bonus, True)):
        if self.isBonusValid(bonus, pLoopPlot, bIgnoreUniqueRange, bIgnoreOneArea, bIgnoreAdjacent):
            pLoopPlot.setBonusType(bonus)
            resources_placed.append(type_string)
            #print "placed", type_string, "on pass", pass_num
            break # go to the next bonus

This code translates to "check each plot in turn - if the bonus (resource) can go there, add it to the plot, and stop checking".

CvPlot.canHaveBonus looks like
Code:
bool CvPlot::canHaveBonus(BonusTypes eBonus, bool bIgnoreLatitude) const
{
    FAssertMsg(getTerrainType() != NO_TERRAIN, "TerrainType is not assigned a valid value");

    if (eBonus == NO_BONUS)
    {
        return true;
    }

    if (getBonusType() != NO_BONUS)
    {
        return false;
    }

    if (isPeak())
    {
        return false;
    }

    if (getFeatureType() != NO_FEATURE)
    {
        if (!(GC.getBonusInfo(eBonus).isFeature(getFeatureType())))
        {
            return false;
        }

        if (!(GC.getBonusInfo(eBonus).isFeatureTerrain(getTerrainType())))
        {
            return false;
        }
    }
    else
    {
        if (!(GC.getBonusInfo(eBonus).isTerrain(getTerrainType())))
        {
            return false;
        }
    }

    if (isHills())
    {
        if (!(GC.getBonusInfo(eBonus).isHills()))
        {
            return false;
        }
    }
    else if (isFlatlands())
    {
        if (!(GC.getBonusInfo(eBonus).isFlatlands()))
        {
            return false;
        }
    }

    if (GC.getBonusInfo(eBonus).isNoRiverSide())
    {
        if (isRiverSide())
        {
            return false;
        }
    }

    if (GC.getBonusInfo(eBonus).getMinAreaSize() != -1)
    {
        if (area()->getNumTiles() < GC.getBonusInfo(eBonus).getMinAreaSize())
        {
            return false;
        }
    }

    if (!bIgnoreLatitude)
    {
        if (getLatitude() > GC.getBonusInfo(eBonus).getMaxLatitude())
        {
            return false;
        }

        if (getLatitude() < GC.getBonusInfo(eBonus).getMinLatitude())
        {
            return false;
        }
    }

    if (!isPotentialCityWork())
    {
        return false;
    }

    return true;
}

Which translates roughly to "if the plot doesn't already have a bonus on it, and the plot meets the conditions described in CIV4BonusInfos.xml, go ahead".

So knowing the terrain for a bonus (coal needs grass hills or plains hills, aluminum needs plains hills, tundra hills, or desert hills, etc), the order that the resources are added (defined by resourcesToBalance), the order that the plots are checked, and the restrictions of the four different passes, you can look at your start position and pick off where each of the resources is hidden.
Reply



Forum Jump: