Files
dusklight/src/dusk/randomizer/logic/fill.cpp
T
2026-04-11 20:21:59 -07:00

530 lines
25 KiB
C++

#include "fill.hpp"
#include "item_pool.hpp"
#include "search.hpp"
#include "../utility/random.hpp"
#include "../utility/string.hpp"
#include "../utility/time.hpp"
#include <iostream>
#include <algorithm>
namespace randomizer::logic::fill
{
void FillWorlds(world::WorldPool& worlds)
{
// Place each world's restricted items first
for (auto& world : worlds)
{
PlaceRestrictedItems(world, worlds);
}
item_pool::ItemPool itemPool = {};
location::LocationPool locationPool = {};
// Combine all worlds' item pools and location pools
for (const auto& world : worlds)
{
for (const auto& item : world->GetItemPool())
{
itemPool.emplace_back(item);
}
for (const auto& location : world->GetAllLocations())
{
locationPool.emplace_back(location);
}
}
// Place remaining major items in progress locations
auto majorItems =
randomizer::utility::container::FilterAndEraseFromVector(itemPool, [](const auto& item) { return item->IsMajor(); });
auto progressLocations =
randomizer::utility::container::FilterFromVector(locationPool,
[](const auto& location) { return location->IsProgression(); });
AssumedFill(worlds, majorItems, itemPool, progressLocations);
// Place Minor items in progression locations if possible
auto minorItems =
randomizer::utility::container::FilterAndEraseFromVector(itemPool, [](const auto& item) { return item->IsMinor(); });
FastFill(minorItems, progressLocations);
// If there are still minor items left, add them back to the main item pool
for (const auto& minorItem : minorItems)
{
itemPool.push_back(minorItem);
}
// Then place everything else anywhere
FastFill(itemPool, locationPool);
// Verify that all logic is satisfied
auto verifyLogicError = search::VerifyLogic(&worlds);
if (verifyLogicError.has_value())
{
throw std::runtime_error("Not all logic satisfied! Reason:\n" + verifyLogicError.value());
}
}
void AssumedFill(world::WorldPool& worlds,
item_pool::ItemPool& itemsToPlacePool,
const item_pool::ItemPool& itemsNotYetPlaced,
location::LocationPool allowedLocations,
const int& worldToFill /* = -1 */)
{
// Assumed Fill may sometimes place items in such a way that accidentally locks out being able to place specific items
// later on. Allow the algorithm to retry a reasonable amount of times before returning an error.
int retries = 10;
bool unsuccessfulPlacement = true;
while (unsuccessfulPlacement)
{
if (retries <= 0)
{
std::string errorMsg = "Ran out of retries while attempting to place the following items:\n";
int count = itemsToPlacePool.size() > 5 ? 5 : itemsToPlacePool.size();
for (int i = 0; i < count; i++)
{
auto& item = itemsToPlacePool[i];
errorMsg += "- " + item->GetName() + "\n";
}
if (count < itemsToPlacePool.size())
{
errorMsg += "- (" + std::to_string(itemsToPlacePool.size() - count) + " more)";
}
throw std::runtime_error(errorMsg);
}
retries -= 1;
unsuccessfulPlacement = false;
randomizer::utility::random::ShufflePool(itemsToPlacePool);
auto itemsToPlace = itemsToPlacePool;
location::LocationPool rollbacks = {};
while (!itemsToPlace.empty())
{
// Get a random item to place
auto itemToPlace = itemsToPlace.back();
itemsToPlace.pop_back();
randomizer::utility::random::ShufflePool(allowedLocations);
location::Location* spotToFill = nullptr;
// Assume we have all the items which haven't been played yet, except the one we're about to place
auto assumedItems = itemsNotYetPlaced;
assumedItems.insert(assumedItems.end(), itemsToPlace.begin(), itemsToPlace.end());
auto search = search::Search::Accessible(&worlds, assumedItems, worldToFill);
search.SearchWorlds();
// search.DumpWorldGraph();
// return 1;
// Loop through the shuffled locations until we find a valid one.
// If a world is only checking for beatable logic, then we can ignore
// any access checks and just choose a random location if the world is already beatable
auto beatableOnlyLogic = itemToPlace->GetWorld()->Setting("Logic Rules") == "Beatable Only";
bool canChooseAnyLocation =
search._ownedItems.contains(itemToPlace->GetWorld()->GetGameWinningItem()) && beatableOnlyLogic;
for (const auto& location : allowedLocations)
{
// Get all reachable LocationAccess spots for this location
std::list<area::LocationAccess*> locAccList;
for (const auto& locAcc : location->GetAccessList())
{
if (canChooseAnyLocation || search._visitedAreas.contains(locAcc->GetArea()))
{
locAccList.push_back(locAcc);
}
}
// If this location is not empty, or has no potentially reachable LocationAccess spot, or is forbidden from
// having this item, then we can't place the item here
if (!location->IsEmpty() || locAccList.empty() || location->GetForbiddenItems().contains(itemToPlace))
{
continue;
}
// If any of the LocationAccess spots evaluate to complete, then we can place an item here
if (std::any_of(locAccList.begin(),
locAccList.end(),
[&](const auto& la)
{
return canChooseAnyLocation ||
requirement::EvaluateLocationRequirement(&search, la) ==
requirement::EvalSuccess::COMPLETE;
}))
{
spotToFill = location;
break;
}
}
// If we couldn't find a spot to place this item, undo all item placements within this fill attempt and try
// again from the top.
if (spotToFill == nullptr)
{
LOG_TO_DEBUG("No accessible locations to place " + itemToPlace->GetName() + ". Retrying " +
std::to_string(retries) + " more times.");
for (auto& location : rollbacks)
{
itemsToPlace.push_back(location->GetCurrentItem());
location->RemoveCurrentItem();
}
// Also add back the randomly selected item
itemsToPlace.push_back(itemToPlace);
rollbacks.clear();
// Break out of the item placement loop and flag an unsuccessful placement attempt to try again
unsuccessfulPlacement = true;
break;
}
// Place the item at the location
spotToFill->SetCurrentItem(itemToPlace);
rollbacks.push_back(spotToFill);
}
}
}
void FastFill(item_pool::ItemPool& itemsToPlace, location::LocationPool allowedLocations)
{
auto emptyLocations =
randomizer::utility::container::FilterFromVector(allowedLocations,
[](const auto& location) { return location->IsEmpty(); });
if (itemsToPlace.size() > emptyLocations.size())
{
std::cout << "WARNING: More items than locations when placing items with fast fill. Items: " << itemsToPlace.size()
<< " Locations: " << emptyLocations.size() << std::endl;
}
randomizer::utility::random::ShufflePool(emptyLocations);
for (auto& location : emptyLocations)
{
if (itemsToPlace.empty())
{
break;
}
location->SetCurrentItem(randomizer::utility::random::PopRandomElement(itemsToPlace));
}
}
void PlaceRestrictedItems(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
PlaceGoalLocationItems(world, worlds);
PlaceOwnDungeonItems(world, worlds);
PlacePrologueItems(world, worlds);
PlaceAnywhereDungeonRewards(world, worlds);
// Determine required dungeons now so that we can place "any dungeon" items appropriately
world->DetermineRequiredDungeons();
PlaceAnyDungeonItems(world, worlds);
PlaceOverworldItems(world, worlds);
}
void PlacePrologueItems(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
if (world->Setting("Skip Prologue") == "Off")
{
// Filter out the slingshot and progressive swords to place first. The first slingshot and sword have a very limited
// pool of locations and have to be found in the intro. We also include the lantern, shadow crystal, and progressive
// fishing rod because those items can lock prologue locations also.
auto& itemPool = world->GetItemPool();
auto prologueItems = randomizer::utility::container::FilterAndEraseFromVector(
itemPool,
[](const auto& item)
{
return item->GetName() == "Slingshot" || item->GetName() == "Progressive Sword" ||
item->GetName() == "Lantern" || item->GetName() == "Progressive Fishing Rod" ||
item->IsShadowCrystal();
});
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, prologueItems, completeItemPool, world->GetAllLocations());
}
}
void PlaceGoalLocationItems(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
// If dungeon rewards can be anywhere, then return early and place them later
if (world->Setting("Dungeon Rewards Can Be Anywhere") == "On")
{
return;
}
auto allLocations = world->GetAllLocations();
location::LocationPool goalLocations = {};
// Filter out goal locations
goalLocations = randomizer::utility::container::FilterFromVector(
allLocations,
[](const auto& location) { return location->IsGoalLocation() && location->IsEmpty(); });
// Filter out goal items
std::set<std::string> goalItemNames = {"Progressive Mirror Shard", "Progressive Fused Shadow"};
auto goalItems = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item) { return goalItemNames.contains(item->GetName()); });
// Return an error if there aren't enough goal locations
if (goalItems.size() > goalLocations.size())
{
throw std::runtime_error("Not enough goal locations to place dungeon rewards on goal locations.");
}
// Place goal items at goal locations
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, goalItems, completeItemPool, goalLocations);
}
void PlaceOwnDungeonItems(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
for (const auto& [dungeonName, dungeon] : world->GetDungeonTable())
{
// Filter hint signs out of dungeon locations
auto dungeonLocations = dungeon->GetLocations();
randomizer::utility::container::FilterAndEraseFromVector(dungeonLocations,
[](const auto& location)
{ return location->HasCategories("Hint Sign"); });
// Clang doesn't like passing structured binding variables to lambda functions via reference, so we create these
// temporary variables to serve the purpose
auto& dungeon_ = dungeon;
auto& dungeonName_ = dungeonName;
// Small Keys
if (world->Setting("Small Keys") == "Own Dungeon")
{
auto smallKeys = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item)
{
return item == dungeon_->GetSmallKey() ||
(dungeonName_ == "Snowpeak Ruins" &&
(item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese"));
});
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, smallKeys, completeItemPool, dungeonLocations);
}
// Big Keys
if (world->Setting("Big Keys") == "Own Dungeon")
{
auto bigKeys = randomizer::utility::container::FilterAndEraseFromVector(world->GetItemPool(),
[&](const auto& item)
{ return item == dungeon_->GetBigKey(); });
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, bigKeys, completeItemPool, dungeonLocations);
}
// Place maps and compasses last with fast fill since they're junk items
if (world->Setting("Maps and Compasses") == "Own Dungeon")
{
auto mapsCompasses = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item) { return item == dungeon_->GetCompass() || item == dungeon_->GetDungeonMap(); });
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
FastFill(mapsCompasses, dungeonLocations);
}
}
}
void PlaceAnywhereDungeonRewards(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
// If dungeon rewards can't be anywhere, then return early as we placed them earlier
if (world->Setting("Dungeon Rewards Can Be Anywhere") == "Off")
{
return;
}
auto allLocations = world->GetAllLocations();
// Filter out goal items
std::set<std::string> goalItemNames = {"Progressive Mirror Shard", "Progressive Fused Shadow"};
auto goalItems = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item) { return goalItemNames.contains(item->GetName()); });
// Place the items
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, goalItems, completeItemPool, allLocations);
}
void PlaceAnyDungeonItems(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
item_pool::ItemPool anyDungeonItems = {};
location::LocationPool anyDungeonLocations = {};
// Split the placement of any dungeon items into two pools. Dungeon items from dungeons which should be barren
// will only be distributed among barren dungeons, where as items from nonbarren dungeons will be distributed
// among nonbarren dungeons
std::list<dungeon::Dungeon*> nonBarrenDungeons = {};
std::list<dungeon::Dungeon*> barrenDungeons = {};
for (const auto& [dungeonName, dungeon] : world->GetDungeonTable())
{
if (dungeon->ShouldBeBarren())
{
barrenDungeons.push_back(dungeon.get());
}
else
{
nonBarrenDungeons.push_back(dungeon.get());
}
}
// Loop through each pool separately
for (const auto& dungeons : {nonBarrenDungeons, barrenDungeons})
{
anyDungeonItems.clear();
anyDungeonLocations.clear();
// Gather all the appropriate items and locations for the dungeon in this pool
for (const auto& dungeon : dungeons)
{
// Clang doesn't like passing structured binding variables to lambda functions via reference, so we create these
// temporary variables to serve the purpose
auto& dungeon_ = dungeon;
// Add small keys to the pool if small keys are any dungeon
if (world->Setting("Small Keys") == "Any Dungeon")
{
auto smallKeys = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item)
{
return item == dungeon_->GetSmallKey() ||
(dungeon_->GetName() == "Snowpeak Ruins" &&
(item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese"));
});
std::copy(smallKeys.begin(), smallKeys.end(), std::back_inserter(anyDungeonItems));
}
// Add big keys to the pool if big keys are any dungeon
if (world->Setting("Big Keys") == "Any Dungeon")
{
auto bigKeys = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item) { return item == dungeon_->GetBigKey(); });
std::copy(bigKeys.begin(), bigKeys.end(), std::back_inserter(anyDungeonItems));
}
// Add maps and compasses to the pool if maps and compasses are any dungeon
if (world->Setting("Maps and Compasses") == "Any Dungeon")
{
auto mapsCompasses = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item) { return item == dungeon_->GetCompass() || item == dungeon_->GetDungeonMap(); });
std::copy(mapsCompasses.begin(), mapsCompasses.end(), std::back_inserter(anyDungeonItems));
}
// Add this dungeon's locations to the anyDungeonLocations pool. If this is a nonbarren dungeon, only include
// locations which are still progression. If it's a barren dungeon, include all the locations
auto dungeonLocations = dungeon->GetLocations();
std::copy_if(dungeonLocations.begin(),
dungeonLocations.end(),
std::back_inserter(anyDungeonLocations),
[&](const auto& location) { return dungeon->ShouldBeBarren() || location->IsProgression(); });
}
// Place the dungeon items in the appropriate dungeon locations
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, anyDungeonItems, completeItemPool, anyDungeonLocations);
}
}
void PlaceOverworldItems(std::unique_ptr<world::World>& world, world::WorldPool& worlds)
{
item_pool::ItemPool overworldItems = {};
location::LocationPool overworldLocations = world->GetAllLocations();
// Filter out any nonprogress locations
randomizer::utility::container::FilterAndEraseFromVector(overworldLocations,
[](const auto& location) { return !location->IsProgression(); });
for (const auto& [dungeonName, dungeon] : world->GetDungeonTable())
{
// Clang doesn't like passing structured binding variables to lambda functions via reference, so we create these
// temporary variables to serve the purpose
auto& dungeon_ = dungeon;
auto& dungeonName_ = dungeonName;
// Add small keys to the pool if small keys are overworld
if (world->Setting("Small Keys") == "Overworld")
{
auto smallKeys = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item)
{
return item == dungeon_->GetSmallKey() ||
(dungeonName_ == "Snowpeak Ruins" &&
(item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese"));
});
std::copy(smallKeys.begin(), smallKeys.end(), std::back_inserter(overworldItems));
}
// Add big keys to the pool if big keys are overworld
if (world->Setting("Big Keys") == "Overworld")
{
auto bigKeys = randomizer::utility::container::FilterAndEraseFromVector(world->GetItemPool(),
[&](const auto& item)
{ return item == dungeon_->GetBigKey(); });
std::copy(bigKeys.begin(), bigKeys.end(), std::back_inserter(overworldItems));
}
// Add maps and compasses to the pool if maps and compasses are overworld
if (world->Setting("Maps and Compasses") == "Overworld")
{
auto mapsCompasses = randomizer::utility::container::FilterAndEraseFromVector(
world->GetItemPool(),
[&](const auto& item) { return item == dungeon_->GetCompass() || item == dungeon_->GetDungeonMap(); });
std::copy(mapsCompasses.begin(), mapsCompasses.end(), std::back_inserter(overworldItems));
}
// Remove this dungeon's locations from the overworldLocations pool
overworldLocations = randomizer::utility::container::FilterFromVector(
overworldLocations,
[&](const auto& location)
{ return !randomizer::utility::container::ElementInContainer(dungeon_->GetLocations(), location); });
}
// Place the dungeon items in the overworld locations
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, overworldItems, completeItemPool, overworldLocations);
}
void CacheExitTimeForms(world::WorldPool& worlds)
{
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
auto searchWithItems = search::Search::AllLocationsReachable(&worlds, completeItemPool);
searchWithItems.SearchWorlds();
for (auto& world : worlds)
{
LOG_TO_DEBUG("Caching timeforms for world " + std::to_string(world->GetID()));
auto& exitTimeFormCache = world->GetExitTimeFormCache();
exitTimeFormCache.clear();
for (const auto& [areaName, area] : world->GetAreaTable())
{
const auto& areaFormTimes = searchWithItems._areaFormTime[area.get()];
for (const auto& exit : area->GetExits())
{
auto req = exit->GetRequirement();
exitTimeFormCache[exit] = requirement::FormTime::NONE;
for (const auto& formTime : requirement::FormTime::ALL_FORM_TIMES)
{
if (formTime & areaFormTimes &&
requirement::EvaluateRequirementAtFormTime(req,
&searchWithItems,
formTime,
world.get()))
{
exitTimeFormCache[exit] |= formTime;
}
}
}
}
}
}
} // namespace randomizer::logic::fill