2nd was looking closer at
Host.bsf/Host_Disperse_Slaves(), which was doing some complicated figuring based on
DS_SLAVE_COEFF_TOSTRUCT and such.
It finally dawned on me that the point of the exercise was to move Slave POPs from regions with too many, to regions with too few.
Not wanting to try solving the full
k-partition problem (see:
https://en.wikipedia.org/wiki/Partition_problem), I implemented a simple (weighted) load-balancing approach.
(Yes, it's
O(3n) to the number of regions, but
n is typically <<
REGION_MAX, which isn't all that large.)
First, the data structure:
Code: Select all
struct RegionCountsStruct
{
int id; // region id
int nPOPs; // total number of POPs
int nSlaves; // number of nPOPs that are slaves
int nSlots; // number of available structure slots, relative to then-current nPOPs
}
RegionCountsStruct localRegionCounts[REGION_MAX]; // worst-case: all regions owned by same faction
#define DS_RECEIVERS 0
#define DS_SENDERS 1
// treating parameter "delta" as the threshold between receiver # and sender # needed to effect transfer
FUNCTION Host_Disperse_Slaves(factionID, delta)
{
int regCount;
int rgn;
int regionID;
int nPOPs; // total number of POPs for faction
int nSlaves; // total number of Slave POPs for faction
int nSlots; // total number of available structure slots for faction
int avgSlaves; // simple average of nSlaves / regCount
int hwm; // high water mark: largest # nSlaves seen in any region
int iter; // loop counter
int diff; // difference between region's nSlaves and faction's avgSlaves
int destID; // regionID of region receiving moved POP
int popID; // ID of POP selected for move
int xfer; // number of transfers to make
Then (after some pre-condition checks), the 1st pass:
Code: Select all
// Pass 1: gather
// also gather total number of POPs, slaves across all regions for this faction
nPOPs = 0;
nSlaves = 0;
nSlots = 0;
hwm = 0;
for (rgn = 0; rgn < regCount; rgn++)
{
regionID = GetOwnedRegionID(factionID, rgn);
localRegionCounts[rgn].id = regionID
localRegionCounts[rgn].nPOPs = Region_Population_Count(regionID);
localRegionCounts[rgn].nSlaves = Region_Population_CountEx(factionID, regionID, POP_SOCIAL_SLAVE);
localRegionCounts[rgn].nSlots = Region_Structures_NumFreeSlot(regionID);
// nSlots == 0 when # structures >= nPOPs
// update running totals:
nPOPs += localRegionCounts[rgn].nPOPs;
nSlaves += localRegionCounts[rgn].nSlaves;
nSlots += localRegionCounts[rgn].nSlots;
hwm = Max(hwm,localRegionCounts[rgn].nSlaves);
}
The 2nd pass decides sending and receiving regions, using simple load balancing, moving POPs from regions with "above average" (surplus) to regions "below average" (deficit).
Over successive turns, this has the effect of smoothing out the population to be ≈ the avg # in all regions.
Note that the
nSlots field is currently unused, so the original "not disturb planification" logic is not present in this version.
Code: Select all
avgSlaves = nSlaves / regCount; // n.b.: integer math
avgSlaves = Max(avgSlaves, 1); // make avg at least 1, so regions w/none are eligible as receivers
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "faction %S: avg slaves# %d, max %d, total %d", gString, avgSlaves, hwm, nSlaves);
// rely on bag's property to weight selections (see: Tools.bsf/PickBag_* and https://en.wikipedia.org/wiki/Set_(abstract_data_type)#Multiset)
PickBag_ResetAndLock(DS_RECEIVERS); // Bag 0 for receivers
PickBag_ResetAndLock(DS_SENDERS); // Bag 1 for senders
// loop to classify regions as either receivers or senders, based on their # slaves relative to avgSlaves:
for (rgn = 0; rgn < regCount; rgn++)
{
regionID = localRegionCounts[rgn].id;
diff = localRegionCounts[rgn].nSlaves - avgSlaves;
Region_Name2(regionID); // see Region.bsf: sets gString2 (display via %S)
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "region %S: slaves# %d (diff %d)", gString2, localRegionCounts[rgn].nSlaves, diff);
if (diff < 0) // Regions with deficit (i.e. # slaves below average) are candidates to receive<-
{
diff = -diff; // make positive, for bag count
PickBag_Add(DS_RECEIVERS, regionID, diff); // larger deficit more likely to receive
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "region %S: Adding as receiving candidate, slave deficit is %d", gString2, diff);
}
else if (diff > 0) // Regions with excess (i.e. # slaves above average) can send->
{
PickBag_Add(DS_SENDERS, regionID, diff); // larger surplus more likely to supply
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "region %S: Adding as sending candidate, slave surplus is %d", gString2, diff);
}
// otherwise, already has avgSlaves - skip
}
This design relies on the properties of the "bag"/multiset (
https://en.wikipedia.org/wiki/Set_(abst ... )#Multiset) data structure to use the amount by which a region is over / under the average to make it more likely (than other regions) to send / receive transfers, respectively.
The final pass effects the actual transfer(s), if any:
Code: Select all
// Relies on bag property that higher-count entries are more likely to be picked
xfer = Max(delta, hwm - avgSlaves); // excess to distribute, but at least the given delta
xfer = Min(xfer, PickBag_Count(DS_SENDERS)); // but no more than available
xfer = Min(xfer, PickBag_Count(DS_RECEIVERS)); // and no more than expected
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "# of slaves to move: %d, %d sender(s), %d receiver(s)", xfer, PickBag_Count(DS_SENDERS), PickBag_Count(DS_RECEIVERS));
// process while receiver(s)/sender(s) found (PickBag removes item, decreasing count)
while (((PickBag_Count(DS_RECEIVERS) > 0) && (PickBag_Count(DS_SENDERS) > 0)) && (xfer > 0))
{
regionID = PickBag_PickEx(DS_SENDERS, TRUE, FALSE); // pick a sender region, reducing its weight by one
destID = PickBag_PickEx(DS_RECEIVERS, TRUE, FALSE); // pick a receiver region, reducing its weight by one
if ((regionID > 0) && (destID > 0) && (destID != regionID))
{
popID = Region_Population_PickOne(regionID, -1, POP_SOCIAL_SLAVE, FALSE);
Region_Name2(regionID); // see Region.bsf: sets gString2 (display via %S)
Region_Name3(destID); // see Region.bsf: sets gString3 (display via %S)
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "%d: move slave ID %d from %S -> %S", xfer, popID, gString2, gString3);
if (popID > 0)
{
xfer--; // only count against total if the transfer happens
Population_SetRegion(popID, destID);
Message_Region_SlaveMove(factionID, regionID, destID)
}
}
TraceLogEx(gVerbosity.decisions, "", "decisions", -1, "# of slaves to move: %d, %d sender(s), %d receiver(s)", xfer, PickBag_Count(DS_SENDERS), PickBag_Count(DS_RECEIVERS));
}
The net result is as expected. Here's an excerpt from the
decisions.log for one faction:
Code: Select all
Host_Disperse_Slaves(faction Bosporus, delta 2)
faction Bosporus: 13 region(s)
faction Bosporus: avg slaves# 2, max 3, total 29
region Phanagoria: slaves# 3 (diff 1)
region Phanagoria: Adding as sending candidate, slave surplus is 1
region Ampsalis: slaves# 0 (diff -2)
region Ampsalis: Adding as receiving candidate, slave deficit is 2
region Henochia: slaves# 3 (diff 1)
region Henochia: Adding as sending candidate, slave surplus is 1
region Azabitis: slaves# 3 (diff 1)
region Azabitis: Adding as sending candidate, slave surplus is 1
region Paniardis: slaves# 3 (diff 1)
region Paniardis: Adding as sending candidate, slave surplus is 1
region Corasus: slaves# 2 (diff 0)
region Tyrambae: slaves# 2 (diff 0)
region Vardanes: slaves# 2 (diff 0)
region Siracia: slaves# 2 (diff 0)
region Suruba: slaves# 0 (diff -2)
region Suruba: Adding as receiving candidate, slave deficit is 2
region Scymitae: slaves# 3 (diff 1)
region Scymitae: Adding as sending candidate, slave surplus is 1
region Abasgia: slaves# 3 (diff 1)
region Abasgia: Adding as sending candidate, slave surplus is 1
region Ebriapa: slaves# 3 (diff 1)
region Ebriapa: Adding as sending candidate, slave surplus is 1
# of slaves to move: 2, 7 sender(s), 4 receiver(s)
2: move slave ID 411304000 from Abasgia -> Ampsalis
# of slaves to move: 1, 6 sender(s), 3 receiver(s)
1: move slave ID 407634064 from Azabitis -> Suruba
# of slaves to move: 0, 5 sender(s), 2 receiver(s)
Checking the in-game output led to the discovery of another bug (see next post).