Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ jobs:
scons -j$(nproc) release=1 server=0 round-trip-hunger-gate-test
python3 test/run-savegame-safety-tests.py --check-preferences build/src/RoundTripHungerGateHarness .

- name: Build and run the inn swap regression
run: |
scons -j$(nproc) release=1 server=0 inn-swap-test
timeout 120s ./build/src/InnSwapHarness

- name: Test savegame loading and atomic autosaves
run: |
scons -j$(nproc) release=1 server=0 savegame-safety-test buffered-file-test
Expand Down
7 changes: 7 additions & 0 deletions src/SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,13 @@ if not env['server']:
regression_test = local.Program('BuildingExpelHarness', regression_sources)
local.Alias('building-expel-test', regression_test)

# Real engine regression for inn swaps between hungry units, built only by its explicit target.
if not env['server']:
regression_sources = [source for source in source_files if source != 'Glob2.cpp']
regression_sources += local.Object('InnSwapHarness.o', '#test/InnSwapHarness.cpp')
regression_test = local.Program('InnSwapHarness', regression_sources)
local.Alias('inn-swap-test', regression_test)

# Real engine regression for the resource hunger gate, built only by its explicit target.
if not env['server']:
regression_sources = [source for source in source_files if source != 'Glob2.cpp']
Expand Down
3 changes: 2 additions & 1 deletion src/building/Building.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ class Building : public BuildingUtils
///It is considered greedy, hiring as many units as it needs in order of its preference
///Returns true if a unit was hired
bool subscribeToBringResourcesStep(void);
//! Whether the unit's type and level qualify it to work for this building.
bool canUnitWorkHere(Unit* unit);
///This function subscribes any flag that needs units.
///It is considered greedy, hiring as many units as it needs in order of its preference
///Returns true if a unit was hired
Expand Down Expand Up @@ -396,7 +398,6 @@ class Building : public BuildingUtils
/// Tells whether a particular unit can work at this building. Takes into account this buildings level,
/// the units type and level, and whether this building is a flag, because flags get a couple of special
/// rules.
bool canUnitWorkHere(Unit* unit);

/// Per-zonable candidate-selection helpers for subscribeForFlagingStep.
/// Each tests one unit against the per-flag-type requirements (activity,
Expand Down
1 change: 1 addition & 0 deletions src/building/Step.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ bool Building::subscribeToBringResourcesStep()
{
unitsWorking.push_back(sel.choosen);
sel.choosen->subscriptionSuccess(this, false);
owner->swapTask(sel.choosen);
hired=true;
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/map/Map.h
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,12 @@ class Map
//! on the AIs' Uint8 maps alike: the goal is the maximum of the element type.
template<typename T>
bool getGlobalGradientDestination(const T *gradient, int x, int y, Sint32 *targetX, Sint32 *targetY) const;

//! Whether (x, y) is a local maximum of gradient: no neighbour holds a strictly higher
//! value. True at any tile getGlobalGradientDestination's ascent could end on, including
//! gradients like a round-trip field whose seeded goal is a finite cost, not the type's max.
template<typename T>
bool isGradientPeak(const T *gradient, int x, int y) const;

Uint16 getGradient(int teamNumber, Uint8 resourceType, int swimClass, int x, int y)
{
return getResourceGradient(teamNumber, resourceType, swimClass)[coordToIndex(x, y)];
Expand Down
20 changes: 20 additions & 0 deletions src/map/MapResources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,5 +252,25 @@ bool Map::getGlobalGradientDestination(const T *gradient, int x, int y, Sint32 *
template bool Map::getGlobalGradientDestination<Uint8>(const Uint8 *gradient, int x, int y, Sint32 *targetX, Sint32 *targetY) const;
template bool Map::getGlobalGradientDestination<Uint16>(const Uint16 *gradient, int x, int y, Sint32 *targetX, Sint32 *targetY) const;

template<typename T>
bool Map::isGradientPeak(const T *gradient, int x, int y) const
{
// A round-trip gradient's goal is seeded at a finite cost, not the type's
// max the way GRADIENT_AT_GOAL is, so getGlobalGradientDestination's own
// "reached exact goal" check does not generalize to it. This is the
// weaker, gradient-agnostic property an ascent target actually needs:
// no neighbour holds a strictly higher value, so an ascent from anywhere
// nearby would still stop here.
size_t index = coordToIndex(x, y);
T here = gradient[index];
for (int d=0; d<8; d++)
if (gradient[coordToIndex(x+deltaOne[d][0], y+deltaOne[d][1])]>here)
return false;
return true;
}

template bool Map::isGradientPeak<Uint8>(const Uint8 *gradient, int x, int y) const;
template bool Map::isGradientPeak<Uint16>(const Uint16 *gradient, int x, int y) const;



9 changes: 9 additions & 0 deletions src/team/Team.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class Unit;

class Game;

//! Tiles a unit can still walk before it starves: what is left of its hunger, then its hp.
Sint32 starvationLimitedTravelDistance(const Unit *unit);

class Team:public BaseTeam
{
public:
Expand Down Expand Up @@ -110,6 +113,12 @@ class Team:public BaseTeam
void removeBuildingNeedingWork(Building* b, Sint32 priority);
//! Update every building in buildingsNeedingUnits, highest priority first.
void updateAllBuildingTasks();
//! Give `unit`'s fetching job to a team mate and take the mate's job, when that
//! shortens the two trips together by more than a few tiles (see TeamStep.cpp).
void swapTask(Unit *unit);
//! Give `unit` a team mate's inn and the mate `unit`'s, when that shortens the
//! two walks together by more than a few tiles; called as `unit` books its place.
void swapInn(Unit *unit);

//! Highest build level any unit of the team has.
int maxBuildLevel(void);
Expand Down
4 changes: 0 additions & 4 deletions src/team/TeamRouting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,11 @@
#include "Team.h"
#include "Unit.h"

namespace {

Sint32 starvationLimitedTravelDistance(const Unit *unit)
{
return std::max(0, unit->hungry) / unit->race->hungriness + unit->hp;
}

} // namespace

Building *Team::findNearestHeal(Unit *unit)
{
if (unit->hungry < 0)
Expand Down
169 changes: 169 additions & 0 deletions src/team/TeamStep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière

#include <algorithm>
#include <cmath>

#include "BuildingType.h"
#include "Game.h"
Expand Down Expand Up @@ -171,6 +172,172 @@ void Team::updateAllBuildingTasks()



namespace
{
// A swap has to save this many tiles over the two trips to be worth the churn.
constexpr int SWAP_MIN_GAIN = 4;
// Fetchers checked for a swap per tick; every unit gets its turn every 256 ticks.
constexpr int SWAP_CHECKS_PER_TICK = Unit::MAX_COUNT / 256;

// A unit on its way to fetch, or to deliver, a resource for its building.
bool isFetching(const Unit *u)
{
return u && u->activity == Unit::ACT_FILLING && u->attachedBuilding && u->ownExchangeBuilding == NULL
&& u->medical == Unit::MED_FREE && u->destinationPurpose >= 0
&& (u->displacement == Unit::DIS_GOING_TO_RESOURCE || u->displacement == Unit::DIS_GOING_TO_BUILDING);
}

// Tiles `u` would walk to do the job (building, resource): deliver what it
// carries, or fetch and carry. Only gradients that already exist are read,
// so a comparison never builds one. False when it cannot take the job.
bool jobCost(Unit *u, Building *b, int resource, int *cost)
{
Map *map = b->owner->map;
int swimClass = u->swimClass();
if (b->globalGradient[swimClass] == NULL)
return false;
if (u->carriedResource >= 0)
return u->carriedResource == resource && map->buildingAvailable(b, swimClass, u->posX, u->posY, cost);
if (map->roundTripDistance(b, resource, swimClass, u->posX, u->posY, cost))
return true;
// No round-trip field for this class yet: the plain distances, as hiring uses them.
int toBuilding, toResource;
if (!map->buildingAvailable(b, swimClass, u->posX, u->posY, &toBuilding)
|| !map->resourceAvailable(b->owner->teamNumber, resource, swimClass, u->posX, u->posY, &toResource))
return false;
*cost = toBuilding + toResource;
return true;
}

// A unit walking to the inn where it booked its meal.
bool isWalkingToInn(const Unit *u)
{
return u && u->activity == Unit::ACT_UPGRADING && u->destinationPurpose == FEED
&& u->attachedBuilding && u->displacement == Unit::DIS_GOING_TO_BUILDING;
}

// Tiles `u` walks to reach `b`: the building's gradient, which choosing an inn
// already built, or the crow-flight distance findNearestFood uses for a flyer.
bool innCost(Unit *u, Building *b, int *cost)
{
Map *map = b->owner->map;
if (u->performance[FLY])
{
*cost = 1 + (Sint32)sqrt(map->warpDistSquare(u->posX, u->posY, b->posX, b->posY));
return true;
}
return map->buildingAvailable(b, u->swimClass(), u->posX, u->posY, cost);
}

// Move `u`'s booking from one inn to the other; both keep their head count.
void rebook(Unit *u, Building *from, Building *to)
{
from->unitsInside.remove(u);
to->unitsInside.push_back(u);
u->attachedBuilding = to;
u->setTargetBuilding(to);
}

void assignTask(Unit *u, Building *b, int resource)
{
u->attachedBuilding->removeUnitFromWorking(u);
u->attachedBuilding = b;
u->destinationPurpose = resource;
b->unitsWorking.push_back(u);
b->updateCallLists();
if (u->carriedResource == resource)
{
u->displacement = Unit::DIS_GOING_TO_BUILDING;
u->setTargetBuilding(b);
}
else
{
u->displacement = Unit::DIS_GOING_TO_RESOURCE;
u->setTargetBuilding(NULL);
b->owner->map->resourceAvailableUpdate(b->owner->teamNumber, resource, u->swimClass(), u->posX, u->posY, &u->targetX, &u->targetY, NULL);
}
u->validTarget = true;
}
}

void Team::swapTask(Unit *unit)
{
if (!isFetching(unit))
return;
Building *a = unit->attachedBuilding;
int r = unit->destinationPurpose;
int own;
if (!jobCost(unit, a, r, &own))
return;
int timeLeft = (unit->hungry - unit->trigHungry) / unit->race->hungriness;
Unit *best = NULL;
int bestGain = SWAP_MIN_GAIN;
for (int i = 0; i < Unit::MAX_COUNT; i++)
{
Unit *mate = myUnits[i];
if (mate == unit || !isFetching(mate))
continue;
Building *b = mate->attachedBuilding;
int s = mate->destinationPurpose;
if ((b == a && s == r) || !b->canUnitWorkHere(unit) || !a->canUnitWorkHere(mate))
continue;
int mateOwn, mine, theirs;
if (!jobCost(mate, b, s, &mateOwn) || !jobCost(unit, b, s, &mine) || !jobCost(mate, a, r, &theirs))
continue;
if (mine >= timeLeft || theirs >= (mate->hungry - mate->trigHungry) / mate->race->hungriness)
continue;
int gain = own + mateOwn - mine - theirs;
if (gain > bestGain)
{
bestGain = gain;
best = mate;
}
}
if (best == NULL)
return;
Building *b = best->attachedBuilding;
int s = best->destinationPurpose;
assignTask(unit, b, s);
assignTask(best, a, r);
}

void Team::swapInn(Unit *unit)
{
if (!isWalkingToInn(unit))
return;
Building *a = unit->attachedBuilding;
int own;
if (a->owner != this || !innCost(unit, a, &own))
return;
Unit *best = NULL;
int bestGain = SWAP_MIN_GAIN;
for (int i = 0; i < Unit::MAX_COUNT; i++)
{
Unit *mate = myUnits[i];
if (mate == unit || !isWalkingToInn(mate) || mate->attachedBuilding == a || mate->attachedBuilding->owner != this)
continue;
Building *b = mate->attachedBuilding;
int mateOwn, mine, theirs;
if (!innCost(mate, b, &mateOwn) || !innCost(unit, b, &mine) || !innCost(mate, a, &theirs))
continue;
if (mine >= starvationLimitedTravelDistance(unit) || theirs >= starvationLimitedTravelDistance(mate))
continue;
int gain = own + mateOwn - mine - theirs;
if (gain > bestGain)
{
bestGain = gain;
best = mate;
}
}
if (best == NULL)
return;
Building *b = best->attachedBuilding;
rebook(unit, a, b);
rebook(best, b, a);
a->updateCallLists();
b->updateCallLists();
}

void Team::syncStep(void)
{
integrity();
Expand Down Expand Up @@ -272,6 +439,8 @@ void Team::syncStep(void)
}

updateAllBuildingTasks();
for (int k = 0; k < SWAP_CHECKS_PER_TICK; k++)
swapTask(myUnits[(game->stepCounter * SWAP_CHECKS_PER_TICK + k) % Unit::MAX_COUNT]);

bool isEnoughFoodInSwarm=false;

Expand Down
1 change: 1 addition & 0 deletions src/unit/UnitActivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ void Unit::handleActivity(void)
if (verbose)
printf("guid=(%d) Subscribed to food at building gbid=(%d)\n", gid, b->gid);
b->subscribeUnitForInside(this);
owner->swapInn(this);
}
else
activity=ACT_RANDOM;
Expand Down
22 changes: 17 additions & 5 deletions src/unit/UnitMovement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -557,15 +557,27 @@ void Unit::handleMovementGoingToResource()
{
Map *map=owner->map;
int teamNumber=owner->teamNumber;
int swim=swimClass();
bool stopWork;
if (map->pathfindResource(teamNumber, destinationPurpose, swimClass(), posX, posY, &dx, &dy, &stopWork, attachedBuilding))
if (map->pathfindResource(teamNumber, destinationPurpose, swim, posX, posY, &dx, &dy, &stopWork, attachedBuilding))
{
directionFromDxDy();
movement=MOV_GOING_DX_DY;
// Routing can follow a rebuilt gradient while the stored target is stale.
// Recompute the target only when it no longer marks a resource goal.
if (map->getGradient(teamNumber, destinationPurpose, swimClass(), targetX, targetY)!=GRADIENT_AT_GOAL)
map->resourceAvailableUpdate(teamNumber, destinationPurpose, swimClass(), posX, posY, &targetX, &targetY, NULL);
// targetX/Y (also the debug path line, hotkey T) were set once, by
// ascending a gradient, when the fetch task started. pathfindResource
// above re-reads whichever gradient actually governs the step fresh
// every action -- the round-trip field when attachedBuilding has one
// and it is valid here, the plain resource gradient otherwise -- and
// either field can be rebuilt, or the preference between them can
// flip, while the unit is still walking. Re-ascend from here whenever
// the stored target has stopped being a peak of that same gradient;
// isGradientPeak is a cheap check to run every action, the ascent
// itself only when it actually goes stale.
const Uint16 *roundTrip = attachedBuilding ? map->roundTripGradient(attachedBuilding, destinationPurpose, swim) : NULL;
const Uint16 *gradient = (roundTrip && roundTrip[map->coordToIndex(posX, posY)]>GRADIENT_UNREACHABLE)
? roundTrip : map->getResourceGradient(teamNumber, destinationPurpose, swim);
if (!map->isGradientPeak(gradient, targetX, targetY))
map->getGlobalGradientDestination(gradient, posX, posY, &targetX, &targetY);
}
else
{
Expand Down
Loading
Loading