Skip to content

Commit

Permalink
Modify QM distance calculation configurably.
Browse files Browse the repository at this point in the history
* Added a new config value for transport to use linear connected
  distance (with a cross-level penalty for going through a shaft)
* Wrote a new a* search linear connected distance function.
* modified the evaluation of the order to compute the distance using
  either the old or new function based on the config.
* store the distance on the transport order for payment later rather
  than recomputing it a second time.
  • Loading branch information
jt-traub committed Jun 19, 2024
1 parent 11f1ab2 commit 4c18595
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 28 deletions.
81 changes: 79 additions & 2 deletions aregion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
#include <random>
#include <ctime>
#include <cassert>
#include <unordered_set>
#include <queue>

using namespace std;

#ifndef M_PI
#define M_PI 3.14159265358979323846
Expand Down Expand Up @@ -2345,8 +2349,81 @@ ARegion *ARegionList::FindNearestStartingCity(ARegion *start, int *dir)
return 0;
}

int ARegionList::GetPlanarDistance(ARegion *one, ARegion *two,
int penalty, int maxdist)
// Some structures for the get_connected_distance function
// structures to allow us to do an efficient search
struct RegionVisited {
int x, y, z;
bool operator==(const RegionVisited &v) const { return x == v.x && y == v.y && z == v.z; }
};
class RegionVisitHash {
public:
size_t operator()(const RegionVisited v) const {
return std::hash<uint32_t>()(v.x) ^ std::hash<uint32_t>()(v.y) ^ std::hash<uint32_t>()(v.z);
}
};
struct QEntry { ARegion *r; int dist; };
class QEntryCompare {
public:
// We want to sort by min distance
bool operator()(const QEntry &below, const QEntry &above) const { return above.dist < below.dist; }
};

// This doesn't really need to be on the ARegionList but, it's okay for now.
int ARegionList::get_connected_distance(ARegion *start, ARegion *target, int penalty, int maxdist) {
unordered_set<RegionVisited, RegionVisitHash> visited_regions;
// We want to search the closest regions first so that as soon as we find one that is too far we know *all* the
// rest will be too far as well.
priority_queue<QEntry, vector<QEntry>, QEntryCompare> q;

if (start == 0 || target == 0) {
// We were given some unusual (nonexistant) regions
return 10000000;
}
ARegion *cur = start;
int cur_dist = 0;

while (maxdist == -1 || cur_dist <= maxdist) {
// If we have hit our target, we are done
if (cur == target) {
// found our target within range
return cur_dist;
}

// Add my current region to the visited set to make sure we don't loop
visited_regions.insert({cur->xloc, cur->yloc, cur->zloc});

// Add all neighbors to the queue as long as we haven't visited them yet
for (int i = 0; i < NDIRS; i++) {
ARegion *n = cur->neighbors[i];
if (n == nullptr) continue; // edge of map has missing neighbors
// cur and n *should* have the same zloc, but ... let's just future-proof in case that changes sometime
int cost = (cur->zloc == n->zloc ? 1 : penalty);
if (n && visited_regions.insert({n->xloc, n->yloc, n->zloc}).second) {
q.push({n, cur_dist + cost });
}
}
// Add any inner regions to the queue as long as we haven't visited them yet
forlist(&cur->objects) {
Object *o = (Object *) elem;
if (o->inner != -1) {
ARegion *inner = GetRegion(o->inner);
int cost = (cur->zloc == inner->zloc ? 1 : penalty);
if (visited_regions.insert({inner->xloc, inner->yloc, inner->zloc}).second) {
q.push({inner, cur_dist + cost});
}
}
}

cur = q.top().r;
cur_dist = q.top().dist;
q.pop();
}

// Should never happen
return 10000000;
}

int ARegionList::GetPlanarDistance(ARegion *one, ARegion *two, int penalty, int maxdist)
{
// make sure you cannot teleport into or from the nexus
if (Globals->NEXUS_EXISTS && (one->zloc == ARegionArray::LEVEL_NEXUS || two->zloc == ARegionArray::LEVEL_NEXUS))
Expand Down
3 changes: 2 additions & 1 deletion aregion.h
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,8 @@ class ARegionList : public AList
int maxY);

ARegion *FindGate(int);
int GetPlanarDistance(ARegion *, ARegion *, int penalty, int maxdist = -1);
int GetPlanarDistance(ARegion *one, ARegion *two, int penalty, int maxdist = -1);
int get_connected_distance(ARegion *start, ARegion *target, int penalty, int maxdist = -1);
int GetWeather(ARegion *pReg, int month);

ARegionArray *GetRegionArray(int level);
Expand Down
1 change: 1 addition & 0 deletions gamedefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ class GameDefs {
QM_AFFECT_COST = 0x02, // QM level affect shipping cost?
// actual distance will be NONLOCAL_TRANSPORT + ((level + 1)/3)
QM_AFFECT_DIST = 0x04, // QM level affect longrange dist?
USE_CONNECTED_DISTANCES = 0x08, // Use connected distance instead of planar distance
};
int TRANSPORT;

Expand Down
1 change: 1 addition & 0 deletions neworigins/extra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,7 @@ void Game::ModifyTablesPerRuleset(void)
ModifyRangeFlags("rng_farsight", RangeType::RNG_CROSS_LEVELS);
ModifyRangeFlags("rng_clearsky", RangeType::RNG_CROSS_LEVELS);
ModifyRangeFlags("rng_weather", RangeType::RNG_CROSS_LEVELS);
ModifyRangeLevelPenalty("rng_transport", 4);
}

if (Globals->TRANSPORT & GameDefs::ALLOW_TRANSPORT) {
Expand Down
3 changes: 2 additions & 1 deletion neworigins/rules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ static GameDefs g = {
0, // GATES_NOT_PERENNIAL
0, // START_GATES_OPEN
0, // SHOW_CLOSED_GATES
GameDefs::ALLOW_TRANSPORT | GameDefs::QM_AFFECT_COST | GameDefs::QM_AFFECT_DIST, // TRANSPORT
GameDefs::ALLOW_TRANSPORT | GameDefs::QM_AFFECT_COST |
GameDefs::QM_AFFECT_DIST | GameDefs::USE_CONNECTED_DISTANCES, // TRANSPORT
2, // LOCAL_TRANSPORT
3, // NONLOCAL_TRANSPORT
5, // SHIPPING_COST
Expand Down
1 change: 1 addition & 0 deletions orders.h
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ class TransportOrder : public Order {
// any other amount is also checked at transport time
int amount;
int except;
int distance;

enum TransportPhase {
SHIP_TO_QM,
Expand Down
18 changes: 10 additions & 8 deletions runorders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3125,16 +3125,21 @@ void Game::CheckTransportOrders()
int penalty = 10000000;
RangeType *rt = FindRange("rng_transport");
if (rt) penalty = rt->crossLevelPenalty;
o->distance = Globals->LOCAL_TRANSPORT; // default to local max distance
if (maxdist > 0) {
// 0 maxdist represents unlimited range
dist = regions.GetPlanarDistance(r, tar->region, penalty, maxdist);
// 0 maxdist represents unlimited range for QM->QM transport
if (Globals->TRANSPORT & GameDefs::USE_CONNECTED_DISTANCES) {
dist = regions.get_connected_distance(r, tar->region, penalty, maxdist);
} else {
dist = regions.GetPlanarDistance(r, tar->region, penalty, maxdist);
}
if (dist > maxdist) {
u->error("TRANSPORT: Recipient " + string(tar->unit->name->const_str()) + " is too far away.");
o->type = NORDERS;
continue;
}
} else {
dist = regions.GetPlanarDistance(r, tar->region, penalty, Globals->LOCAL_TRANSPORT);
// Store off the distance for later use so we don't need to recompute it.
o->distance = dist;
}

// We will check the amount at transport time so that if you receive items in you can tranport them
Expand Down Expand Up @@ -3258,10 +3263,7 @@ void Game::RunTransportPhase(TransportOrder::TransportPhase phase) {
}

// now see if the unit can pay for shipping
int penalty = 10000000;
RangeType *rt = FindRange("rng_transport");
if (rt) penalty = rt->crossLevelPenalty;
int dist = regions.GetPlanarDistance(r, tar->region, penalty, Globals->LOCAL_TRANSPORT);
int dist = t->distance;
int weight = ItemDefs[t->item].weight * amt;
if (weight == 0 && Globals->FRACTIONAL_WEIGHT > 0)
weight = (amt/Globals->FRACTIONAL_WEIGHT) + 1;
Expand Down
12 changes: 6 additions & 6 deletions unittest/json_report_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,15 @@ ut::suite<"JSON Report"> json_report_suite = []
expect(for_sale == 4_ul);

auto expected_sale = json{
{"tag", "IVOR"}, {"name", "ivory"}, {"plural", "ivory"}, {"amount", 13 }, { "price", 81 }
{"tag", "PEAR"}, {"name", "pearls"}, {"plural", "pearls"}, {"amount", 9 }, { "price", 148 }
};
auto first_sale = json_report["markets"]["for_sale"][0];
expect(first_sale == expected_sale);

auto wanted = json_report["markets"]["wanted"].size();
expect(wanted == 9_ul);
auto expected_wanted = json{
{"tag", "GRAI"}, {"name", "grain"}, {"plural", "grain"}, {"amount", 51 }, { "price", 16 }
{"tag", "GRAI"}, {"name", "grain"}, {"plural", "grain"}, {"amount", 87 }, { "price", 20 }
};
auto first_wanted = json_report["markets"]["wanted"][0];
expect(first_wanted == expected_wanted);
Expand Down Expand Up @@ -249,16 +249,16 @@ ut::suite<"JSON Report"> json_report_suite = []
region->build_json_report(json_report_2, faction2, helper.get_month(), regions);

// Verify that owner sees additional data
auto capacity = json_report_1["structures"][0]["capacity"];
auto capacity = json_report_1["structures"][1]["capacity"];
expect(capacity == 8400_ul);

// Verify that non-owner does not see additional data
auto capacity2 = json_report_2["structures"][0]["capacity"];
auto capacity2 = json_report_2["structures"][1]["capacity"];
expect(capacity2 == nullptr);

// Verify they both see the same ships
auto ships = json_report_1["structures"][0]["ships"];
auto ships2 = json_report_2["structures"][0]["ships"];
auto ships = json_report_1["structures"][1]["ships"];
auto ships2 = json_report_2["structures"][1]["ships"];
expect(ships == ships2);
};

Expand Down
43 changes: 39 additions & 4 deletions unittest/map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ void ARegionList::CreateSurfaceLevel(int level, int xSize, int ySize, char const

void ARegionList::CreateIslandLevel(int level, int nPlayers, char const *name) { }

void ARegionList::CreateUnderworldLevel(int level, int xSize, int ySize, char const *name) { }
void ARegionList::CreateUnderworldLevel(int level, int xSize, int ySize, char const *name) {
MakeRegions(level, xSize, ySize);
pRegionArrays[level]->SetName(name);
pRegionArrays[level]->levelType = ARegionArray::LEVEL_UNDERWORLD;
AssignTypes(pRegionArrays[level]);
FinalSetup(pRegionArrays[level]);
}

void ARegionList::CreateUnderdeepLevel(int level, int xSize, int ySize, char const *name) { }

Expand Down Expand Up @@ -130,14 +136,17 @@ void ARegionList::MakeUWMaze(ARegionArray *pArr) { }
void ARegionList::AssignTypes(ARegionArray *pArr) {
// we have a fixed world, so just assign the types.
int terrains[] = { R_PLAIN, R_FOREST, R_MOUNTAIN, R_DESERT };
int uwterrains[] = { R_CAVERN };
int loc = 0;

int *t_array = (pArr->levelType == ARegionArray::LEVEL_UNDERWORLD) ? uwterrains : terrains;

for (auto x = 0; x < pArr->x; x++) {
for (auto y = 0; y < pArr->y; y++) {
ARegion *reg = pArr->GetRegion(x, y);
if (!reg) continue;

reg->type = terrains[loc++];
reg->type = t_array[loc++];
reg->race = TerrainDefs[reg->type].races[0];
}
}
Expand All @@ -160,9 +169,35 @@ void ARegionList::FinalSetup(ARegionArray *pArr) {
}
}

void ARegionList::MakeShaft(ARegion *reg, ARegionArray *pFrom, ARegionArray *pTo) { }
void ARegionList::MakeShaft(ARegion *reg, ARegionArray *pFrom, ARegionArray *pTo) {
ARegion *toReg = pTo->GetRegion(0, 0);
if (!toReg) return;

Object *o = new Object(reg);
o->num = reg->buildingseq++;
o->name = new AString(AString("Shaft [") + o->num + "]");
o->type = O_SHAFT;
o->incomplete = 0;
o->inner = toReg->num;
reg->objects.Add(o);

o = new Object(toReg);
o->num = toReg->buildingseq++;
o->name = new AString(AString("Shaft [") + o->num + "]");
o->type = O_SHAFT;
o->incomplete = 0;
o->inner = reg->num;
toReg->objects.Add(o);
}

void ARegionList::MakeShaftLinks(int levelFrom, int levelTo, int odds) {
ARegionArray *pFrom = pRegionArrays[levelFrom];
ARegionArray *pTo = pRegionArrays[levelTo];

void ARegionList::MakeShaftLinks(int levelFrom, int levelTo, int odds) { }
if (!pFrom || !pTo) return;
// we are ignoring the odds and always creating the shaft
MakeShaft(pFrom->GetRegion(0, 0), pFrom, pTo);
}

void ARegionList::SetACNeighbors(int levelSrc, int levelTo, int maxX, int maxY) { }

Expand Down
25 changes: 24 additions & 1 deletion unittest/quartermaster_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ ut::suite<"Quartermaster"> quartermaster_suite = []

"Quartermasters cannot chain with other quartermasters"_test = []
{
UnitTestHelper helper;
UnitTestHelper helper;
helper.initialize_game();
helper.setup_turn();

Expand Down Expand Up @@ -151,4 +151,27 @@ ut::suite<"Quartermaster"> quartermaster_suite = []
expect(faction->errors[1].message == "TRANSPORT: Unable to transport. Have 0 stone [STON].");
expect(faction->errors[1].unit == qm3);
};

"Connected distance function computes correctly"_test = []
{
UnitTestHelper helper;
helper.initialize_game();
helper.setup_turn();
ARegion *region1 = helper.get_region(0, 0, 0);
ARegion *region2 = helper.get_region(0, 0, 1);
ARegion *region3 = helper.get_region(1, 1, 0);

// going cross level from location to an inner location is just the penalty cost
int d = helper.connected_distance(region1, region2, 4, 10);
expect(d == 4_i);
// adjacent locations are 1
int d2 = helper.connected_distance(region1, region3, 4, 10);
expect(d2 == 1_i);
// inner location of an adjacent hex is 1 + penalty
int d3 = helper.connected_distance(region2, region3, 8, 10);
expect(d3 == 9_i);
// locations that are too far away are a huge number signifying not connected within range
int d4 = helper.connected_distance(region2, region3, 4, 0);
expect(d4 == 10000000_i);
};
};
2 changes: 1 addition & 1 deletion unittest/rules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ static GameDefs g = {
0, // CITY_RENAME_COST
0, // MULTI_HEX_NEXUS
0, // ICOSAHEDRAL_WORLD
0, // UNDERWORLD_LEVELS
1, // UNDERWORLD_LEVELS
0, // UNDERDEEP_LEVELS
0, // ABYSS_LEVEL
0, // TOWN_PROBABILITY
Expand Down
4 changes: 4 additions & 0 deletions unittest/testhelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ ARegion *UnitTestHelper::get_region(int x, int y, int z) {
return level->GetRegion(x, y);
}

int UnitTestHelper::connected_distance(ARegion *reg1, ARegion *reg2, int penalty, int max) {
return game.regions.get_connected_distance(reg1, reg2, penalty, max);
}

string UnitTestHelper::cout_data() {
return cout_buffer.str();
}
Expand Down
2 changes: 2 additions & 0 deletions unittest/testhelper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class UnitTestHelper {
void transport_phase(TransportOrder::TransportPhase phase);
// Collect the transported goods from the quartermasters
void collect_transported_goods();
// connected distance
int connected_distance(ARegion *reg1, ARegion *reg2, int penalty, int max);

// dummy
int get_seed() { return getrandom(10000); };
Expand Down
8 changes: 4 additions & 4 deletions unittest/unit_test_helper_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ut::suite<"UnitTestHelper"> unit_test_helper_suite = []
expect(helper.get_region_count() == 0_i);
// This will call Game::NewGame() which will set up the dummy game with 4 surface regions.
helper.initialize_game();
expect(helper.get_region_count() == 4_i);
expect(helper.get_region_count() == 5_i);
};

"UnitTestHelper captures cout correctly"_test = []
Expand All @@ -32,7 +32,7 @@ ut::suite<"UnitTestHelper"> unit_test_helper_suite = []
helper.initialize_game();
// The output will have the fact that it created the world and 1 '.' for each region.
string current = helper.cout_data();
string expected = "Creating world\n....";
string expected = "Creating world\n.....";
expect(current == expected);
};

Expand All @@ -45,7 +45,7 @@ ut::suite<"UnitTestHelper"> unit_test_helper_suite = []
// initialize the game which will generate a small bit of output
helper.initialize_game();
auto seed = helper.get_seed();
// As long as we keep the isaac rng (for now) this will always be the same.
expect(seed == 299_i);
// As long as we keep the isaac rng (for now) and do not alter world set up this will always be the same.
expect(seed == 8652_i);
};
};
4 changes: 4 additions & 0 deletions unittest/world.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ void Game::CreateWorld() {
regions.CreateLevels(1);
// because of the way regions are numbered, if you want 4 hexes you need a height of 4 and a width of 2.
regions.CreateSurfaceLevel(0, 2, 4, nullptr);
// Make an underworld level
regions.CreateUnderworldLevel(1, 1, 2, "underworld");
// Make a shaft
regions.MakeShaftLinks(0, 1, 100);

ARegion *reg = regions.GetRegion(0,0,0);
reg->MakeStartingCity();
Expand Down

0 comments on commit 4c18595

Please sign in to comment.