mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-03 00:16:36 -04:00
719 lines
30 KiB
C++
719 lines
30 KiB
C++
#include "search.hpp"
|
|
|
|
#include "world.hpp"
|
|
#include "../randomizer.hpp"
|
|
#include "../utility/general.hpp"
|
|
#include "../utility/platform.hpp"
|
|
|
|
#include <fstream>
|
|
#include <iostream>
|
|
|
|
namespace randomizer::logic::search
|
|
{
|
|
Search::Search(): _searchMode(SearchMode::NO_SEARCH) {
|
|
|
|
}
|
|
|
|
Search::Search(const SearchMode& searchMode,
|
|
world::WorldPool* worlds,
|
|
const item_pool::ItemPool& items /* = {} */,
|
|
const int& worldToSearch /* = -1 */,
|
|
bool startingInventory /*= true */,
|
|
location::Location* importanceLocation /*= nullptr*/):
|
|
_searchMode(searchMode), _worlds(worlds), _startingInventory(startingInventory),
|
|
_importanceLocation(importanceLocation)
|
|
{
|
|
// Set the items we should already own
|
|
this->_ownedItems.insert(items.begin(), items.end());
|
|
|
|
// Add starting inventory items for each world
|
|
if (this->_startingInventory) {
|
|
for (const auto& world : *(this->_worlds))
|
|
{
|
|
if (worldToSearch == -1 || world->GetID() == worldToSearch)
|
|
{
|
|
const auto& startingInventory = world->GetStartingItemPool();
|
|
this->_ownedItems.insert(startingInventory.begin(), startingInventory.end());
|
|
}
|
|
}
|
|
this->_itemsAtStart = _ownedItems;
|
|
}
|
|
|
|
// Set search starting properties and add each world's root exits to _exitsToTry
|
|
for (const auto& world : *(this->_worlds))
|
|
{
|
|
if (worldToSearch == -1 || world->GetID() == worldToSearch)
|
|
{
|
|
auto root = world->GetRootArea();
|
|
this->_visitedAreas.emplace(root);
|
|
world->SetSearchStartingProperties(this);
|
|
for (const auto& exit : root->GetExits())
|
|
{ // Don't add target exits if we're doing a sphere zero search
|
|
if (!exit->IsDisabled() && (this->_searchMode != SearchMode::SPHERE_ZERO || !exit->IsTarget()))
|
|
{
|
|
this->_exitsToTry.emplace_back(exit);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we're doing an importance search, then we have to set up a few extra things
|
|
if (this->_searchMode == SearchMode::LOCATION_IMPORTANCE && importanceLocation != nullptr) {
|
|
auto importanceItem = importanceLocation->GetCurrentItem();
|
|
|
|
// Generate the logic requirement for what it would "mean" to obtain the item at this importance location.
|
|
// We're going to assume that this logic requirement is always false when searching.
|
|
// This ensures that we only evaluate to true the chain locations that this item absolutely does not help unlock
|
|
auto count = this->_ownedItems.count(importanceItem) + 1;
|
|
if (count == 1) {
|
|
this->_assumedFalseReq = requirement::Requirement{requirement::Type::ITEM, {importanceItem}};
|
|
} else {
|
|
this->_assumedFalseReq = requirement::Requirement{requirement::Type::COUNT, {count, importanceItem}};
|
|
}
|
|
|
|
// If this location is either a heart container or piece of heart, we'll pass along any assumed heart
|
|
// count from this location or any of its path locations. This will properly take into account any
|
|
// heart requirements that were necessary to get here and adjust hint importance accordingly.
|
|
// (I.e. If Accessing Hyrule Castle requires 10 hearts, and this location has a heart piece, but its
|
|
// locked by something in Hyrule Castle, then it'll always be not required.)
|
|
if (importanceItem->IsSameOrSimilarItem(importanceItem->GetWorld()->GetItem("Heart Container"))) {
|
|
this->_assumedHeartCount = this->_importanceLocation->GetComputedRequirement().getHeartCount();
|
|
for (auto location : this->_importanceLocation->GetPathLocations()) {
|
|
this->_assumedHeartCount = std::max(this->_assumedHeartCount, location->GetComputedRequirement().getHeartCount());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::SearchWorlds()
|
|
{
|
|
if (this->_searchMode == SearchMode::NO_SEARCH) {
|
|
return;
|
|
}
|
|
// Get all locations which fit criteria to test on each iteration
|
|
std::list<area::LocationAccess*> itemLocations = {};
|
|
for (const auto& world : *(this->_worlds))
|
|
{
|
|
for (const auto& [areaName, area] : world->GetAreaTable())
|
|
{
|
|
for (const auto& locAccess : area->GetLocations())
|
|
{
|
|
// Only add locations that aren't empty, unless we're searching with one of the modes below
|
|
if (!locAccess->GetLocation()->IsEmpty() ||
|
|
utility::general::IsAnyOf(this->_searchMode,
|
|
SearchMode::ACCESSIBLE_LOCATIONS,
|
|
SearchMode::ALL_LOCATIONS_REACHABLE,
|
|
SearchMode::SPHERE_ZERO,
|
|
SearchMode::TRACKER_SPHERES))
|
|
{
|
|
itemLocations.emplace_back(locAccess);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main Searching Loop
|
|
// Keep iterating while new things are being found, but if the search is beatable and we're either generating the
|
|
// playthrough or checking for beatability, exit early.
|
|
this->_newThingsFound = true;
|
|
while (
|
|
this->_newThingsFound &&
|
|
!(this->_isBeatable &&
|
|
utility::general::IsAnyOf(this->_searchMode, SearchMode::GENERATE_PLAYTHROUGH, SearchMode::GAME_BEATABLE)))
|
|
{
|
|
// Keep track of making logical progress. We want to keep iterating as long as we're finding new things on each
|
|
// iteration
|
|
this->_newThingsFound = false;
|
|
|
|
// Add an empty sphere if we're generating the playthrough or tracker spheres
|
|
if (utility::general::IsAnyOf(this->_searchMode,
|
|
SearchMode::GENERATE_PLAYTHROUGH,
|
|
SearchMode::TRACKER_SPHERES))
|
|
{
|
|
this->_playthroughSpheres.push_back({});
|
|
this->_entranceSpheres.push_back({});
|
|
}
|
|
|
|
// Process Events and Exits at least once. If we're calculating spheres, then keep repeating these until nothing
|
|
// new is found.
|
|
do
|
|
{
|
|
this->_newThingsFound = false;
|
|
this->ProcessEvents();
|
|
this->ProcessExits();
|
|
} while (this->_newThingsFound && utility::general::IsAnyOf(this->_searchMode,
|
|
SearchMode::GENERATE_PLAYTHROUGH,
|
|
SearchMode::TRACKER_SPHERES));
|
|
|
|
this->ProcessLocations(itemLocations);
|
|
this->_sphereNum += 1;
|
|
}
|
|
}
|
|
|
|
void Search::ProcessEvents()
|
|
{
|
|
for (const auto& event : this->_eventsToTry)
|
|
{
|
|
// Ignore the event if we've already found it, or we're not searching its world at the moment
|
|
if (this->_ownedEvents.contains(event->GetEventIndex()) ||
|
|
(this->_worldToSearch != -1 && event->GetArea()->GetWorld()->GetID() != this->_worldToSearch))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (requirement::EvaluateEventRequirement(this, event) ==
|
|
requirement::EvalSuccess::COMPLETE)
|
|
{
|
|
this->_newThingsFound = true;
|
|
this->_ownedEvents.insert(event->GetEventIndex());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::ProcessExits()
|
|
{
|
|
for (const auto& exit : this->_exitsToTry)
|
|
{
|
|
// Ignore the exit if we've already completed it, or we're not searching its world at the moment
|
|
if (this->_successfulExits.contains(exit) ||
|
|
(this->_worldToSearch != -1 && this->_worldToSearch != exit->GetWorld()->GetID()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If the exit is successful
|
|
auto evalSuccess = requirement::EvaluateExitRequirement(this, exit);
|
|
if (utility::general::IsAnyOf(evalSuccess,
|
|
requirement::EvalSuccess::COMPLETE,
|
|
requirement::EvalSuccess::PARTIAL))
|
|
{
|
|
this->AddExitToEntranceSpheres(exit);
|
|
if (evalSuccess == requirement::EvalSuccess::COMPLETE)
|
|
{
|
|
this->_successfulExits.insert(exit);
|
|
}
|
|
this->_newThingsFound = true;
|
|
|
|
// If this exit's connected region hasn't been explored yet, then explore it
|
|
if (!this->_visitedAreas.contains(exit->GetConnectedArea()))
|
|
{
|
|
this->_visitedAreas.insert(exit->GetConnectedArea());
|
|
this->Explore(exit->GetConnectedArea());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::ProcessLocations(std::list<area::LocationAccess*>& itemLocations)
|
|
{
|
|
std::list<location::Location*> accessibleThisIteration = {};
|
|
// Loop through all possible item locations for this search
|
|
for (const auto& locAccess : itemLocations)
|
|
{
|
|
auto location = locAccess->GetLocation();
|
|
auto world = location->GetWorld();
|
|
|
|
// If we've already visited this location, or have *not* visited this area, or aren't searching this world,
|
|
// then ignore the location this time
|
|
if (this->_visitedLocations.contains(location) || !this->_visitedAreas.contains(locAccess->GetArea()) ||
|
|
(this->_worldToSearch != -1 && world->GetID() != this->_worldToSearch))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If the location's requirement is met
|
|
if (requirement::EvaluateLocationRequirement(this, locAccess) ==
|
|
requirement::EvalSuccess::COMPLETE)
|
|
{
|
|
this->_visitedLocations.insert(location);
|
|
this->_newThingsFound = true;
|
|
// If we're calculating spheres, then process this location later for accurate sphere calculation. Otherwise
|
|
// process it now for slightly faster searching
|
|
if (utility::general::IsAnyOf(this->_searchMode,
|
|
SearchMode::GENERATE_PLAYTHROUGH,
|
|
SearchMode::TRACKER_SPHERES))
|
|
{
|
|
accessibleThisIteration.push_back(location);
|
|
}
|
|
else
|
|
{
|
|
this->ProcessLocation(location);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& location : accessibleThisIteration)
|
|
{
|
|
this->ProcessLocation(location);
|
|
if (this->_isBeatable)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::ProcessLocation(location::Location* location)
|
|
{
|
|
// Don't return if we aren't collecting items, or if we're doing an importance search
|
|
// and the item at this location is a similar item to our importance location
|
|
if (!this->_collectItems ||
|
|
(this->_searchMode == SearchMode::LOCATION_IMPORTANCE &&
|
|
this->_importanceLocation->GetCurrentItem()->IsSameOrSimilarItem(location->GetCurrentItem())))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Add the tracked item if we're doing tracker sphere tracking
|
|
if (this->_searchMode == SearchMode::TRACKER_SPHERES)
|
|
{
|
|
this->_ownedItems.insert(location->GetTrackedItem());
|
|
}
|
|
// Otherwise add the current item as usual
|
|
else
|
|
{
|
|
this->_ownedItems.insert(location->GetCurrentItem());
|
|
}
|
|
|
|
// If we just added the shadow crystal, expand timeforms for all areas we've visited so far
|
|
if (location->GetCurrentItem()->IsShadowCrystal())
|
|
{
|
|
for (auto& area : this->_visitedAreas)
|
|
{
|
|
if (area->GetWorld()->GetID() == location->GetWorld()->GetID())
|
|
{
|
|
this->ExpandFormTimes(area);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we're generating spheres and the location has a major item, add the location to the last sphere
|
|
if (this->_searchMode == SearchMode::TRACKER_SPHERES ||
|
|
(this->_searchMode == SearchMode::GENERATE_PLAYTHROUGH && location->GetCurrentItem()->IsMajor()))
|
|
{
|
|
this->_playthroughSpheres.back().push_back(location);
|
|
}
|
|
|
|
// If we're generating the playthrough or just checking for beatability, then we can stop searching early if we've
|
|
// found all world's game winning items
|
|
if (utility::general::IsAnyOf(this->_searchMode, SearchMode::GENERATE_PLAYTHROUGH, SearchMode::GAME_BEATABLE) &&
|
|
location->GetCurrentItem()->IsGameWinningItem())
|
|
{
|
|
if (std::ranges::count_if(this->_ownedItems, [](const auto& item) {
|
|
return item->IsGameWinningItem();
|
|
}) == this->_worlds->size())
|
|
{
|
|
if (this->_searchMode == SearchMode::GENERATE_PLAYTHROUGH)
|
|
{
|
|
auto& lastSphere = this->_playthroughSpheres.back();
|
|
std::erase_if(lastSphere,[](const auto& loc) { return !loc->GetCurrentItem()->IsGameWinningItem(); });
|
|
}
|
|
this->_isBeatable = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::Explore(area::Area* area)
|
|
{
|
|
for (const auto& event : area->GetEvents())
|
|
{
|
|
this->_eventsToTry.push_back(event);
|
|
}
|
|
|
|
for (const auto& exit : area->GetExits())
|
|
{
|
|
auto evalSuccess = requirement::EvaluateExitRequirement(this, exit);
|
|
switch (evalSuccess)
|
|
{
|
|
case requirement::EvalSuccess::COMPLETE:
|
|
this->_successfulExits.insert(exit);
|
|
this->AddExitToEntranceSpheres(exit);
|
|
if (!this->_visitedAreas.contains(exit->GetConnectedArea()))
|
|
{
|
|
this->_visitedAreas.insert(exit->GetConnectedArea());
|
|
this->Explore(exit->GetConnectedArea());
|
|
}
|
|
case requirement::EvalSuccess::PARTIAL:
|
|
this->_exitsToTry.push_back(exit);
|
|
this->AddExitToEntranceSpheres(exit);
|
|
if (!this->_visitedAreas.contains(exit->GetConnectedArea()))
|
|
{
|
|
this->_visitedAreas.insert(exit->GetConnectedArea());
|
|
this->Explore(exit->GetConnectedArea());
|
|
}
|
|
case requirement::EvalSuccess::NONE:
|
|
[[fallthrough]];
|
|
case requirement::EvalSuccess::DISCONNECTED:
|
|
this->_exitsToTry.push_back(exit);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::ExpandFormTimes(area::Area* area)
|
|
{
|
|
using namespace requirement;
|
|
|
|
auto& areaFormTime = this->_areaFormTime[area];
|
|
auto twilightCleared = area->TwilightCleared(this);
|
|
|
|
auto shadowCrystal = area->GetWorld()->GetShadowCrystal();
|
|
// Check if we can add additional form times to the area
|
|
if (area->CanChangeTime() && area->CanTransform() && this->_ownedItems.contains(shadowCrystal) && twilightCleared)
|
|
{
|
|
// LOG_TO_DEBUG("Spread All to " + area->GetName());
|
|
areaFormTime |= FormTime::ALL;
|
|
}
|
|
// This might look backwards at first glance, but spreading formtime by the form spreads both day and night for the form
|
|
else if (area->CanChangeTime() && twilightCleared)
|
|
{
|
|
if (areaFormTime & FormTime::WOLF)
|
|
{
|
|
// LOG_TO_DEBUG("Spread Day/Night to " + area->GetName());
|
|
areaFormTime |= FormTime::WOLF;
|
|
}
|
|
else if (areaFormTime & FormTime::HUMAN)
|
|
{
|
|
// LOG_TO_DEBUG("Spread Day/Night to " + area->GetName());
|
|
areaFormTime |= FormTime::HUMAN;
|
|
}
|
|
}
|
|
// Same as above except with spreading time spreads the form
|
|
else if (area->CanTransform() && this->_ownedItems.contains(shadowCrystal) && twilightCleared)
|
|
{
|
|
if (areaFormTime & FormTime::NIGHT)
|
|
{
|
|
// LOG_TO_DEBUG("Spread Human/Wolf to " + area->GetName());
|
|
areaFormTime |= FormTime::NIGHT;
|
|
}
|
|
|
|
if (areaFormTime & FormTime::DAY)
|
|
{
|
|
// LOG_TO_DEBUG("Spread Human/Wolf to " + area->GetName());
|
|
areaFormTime |= FormTime::DAY;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::AddExitToEntranceSpheres(entrance::Entrance* exit)
|
|
{
|
|
if (utility::general::IsAnyOf(this->_searchMode,
|
|
SearchMode::GENERATE_PLAYTHROUGH,
|
|
SearchMode::TRACKER_SPHERES) &&
|
|
exit->IsShuffled())
|
|
{
|
|
if (!this->_playthroughEntrances.contains(exit))
|
|
{
|
|
this->_entranceSpheres.back().push_back(exit);
|
|
this->_playthroughEntrances.insert(exit);
|
|
if (!exit->IsDecoupled() && exit->GetReplaces()->GetReverse())
|
|
{
|
|
this->_playthroughEntrances.insert(exit->GetReplaces()->GetReverse());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Search::HasAccessibleDisconnectedExit()
|
|
{
|
|
for (const auto& exit : this->_exitsToTry)
|
|
{
|
|
if (exit->GetConnectedArea() == nullptr &&
|
|
requirement::EvaluateDisconnectedExitRequiremrnt(this, exit) != requirement::EvalSuccess::NONE)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Search::RemoveEmptySpheres()
|
|
{
|
|
// Get rid of any empty spheres in both the item playthrough and entrance playthrough
|
|
// based only on if the item playthrough has empty spheres. Both the playthroughs
|
|
// will have the same number of spheres, so we only need to conditionally
|
|
// check one of them.
|
|
auto itemItr = this->_playthroughSpheres.begin();
|
|
auto entranceItr = this->_entranceSpheres.begin();
|
|
while (itemItr != this->_playthroughSpheres.end())
|
|
{
|
|
if (itemItr->empty() && entranceItr->empty())
|
|
{
|
|
itemItr = this->_playthroughSpheres.erase(itemItr);
|
|
entranceItr = this->_entranceSpheres.erase(entranceItr);
|
|
}
|
|
else
|
|
{
|
|
++itemItr; // Only incremement if we don't erase
|
|
++entranceItr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Search::DumpWorldGraph(const int& worldNum /* = 0 */)
|
|
{
|
|
auto& world = this->_worlds->at(worldNum);
|
|
std::cout << "Now dumping search graph for world " << worldNum << std::endl;
|
|
std::ofstream worldGraph;
|
|
std::string filepath = "World.gv";
|
|
worldGraph.open(filepath);
|
|
worldGraph << "digraph {\n\tcenter=true;\n";
|
|
for (const auto& [areaName, area] : world->GetAreaTable())
|
|
{
|
|
auto color = this->_visitedAreas.contains(area.get()) ? "black" : "red";
|
|
std::string formTimeStr = ":<br/>";
|
|
auto& areaFormTime = this->_areaFormTime[area.get()];
|
|
if (areaFormTime & requirement::FormTime::HUMAN)
|
|
{
|
|
formTimeStr += " Human";
|
|
}
|
|
if (areaFormTime & requirement::FormTime::WOLF)
|
|
{
|
|
formTimeStr += " Wolf";
|
|
}
|
|
if (areaFormTime & requirement::FormTime::DAY)
|
|
{
|
|
formTimeStr += " Day";
|
|
}
|
|
if (areaFormTime & requirement::FormTime::NIGHT)
|
|
{
|
|
formTimeStr += " Night";
|
|
}
|
|
if (areaFormTime & requirement::FormTime::TWILIGHT)
|
|
{
|
|
formTimeStr += " Twilight";
|
|
}
|
|
|
|
worldGraph << "\t\"" << areaName << "\"[label=<" << areaName << formTimeStr << "> shape=\"plain\" fontcolor=\""
|
|
<< color << "\"];\n";
|
|
|
|
// Make edge connections defined by events
|
|
for (const auto& event : area->GetEvents())
|
|
{
|
|
color = this->_ownedEvents.contains(event->GetEventIndex()) ? "blue" : "red";
|
|
auto eventName = world->GetEventName(event->GetEventIndex());
|
|
worldGraph << "\t\"" << eventName << "\"[label=<" << eventName << "> shape=\"plain\" fontcolor=\"" << color
|
|
<< "\"];";
|
|
worldGraph << "\t\"" << areaName << "\" -> \"" << eventName << "\"[dir=forward color=\"" << color << "\"]";
|
|
}
|
|
|
|
// Make edge connections defined by exits
|
|
for (const auto& exit : area->GetExits())
|
|
{
|
|
if (exit->GetConnectedArea())
|
|
{
|
|
color = this->_successfulExits.contains(exit) ? "black" : "red";
|
|
worldGraph << "\t\"" << areaName << "\" -> \"" << exit->GetConnectedArea()->GetName()
|
|
<< "\"[dir=forward color=\"" << color << "\"]";
|
|
}
|
|
}
|
|
|
|
// Make edge connections between areas and their locations
|
|
for (const auto& locAccess : area->GetLocations())
|
|
{
|
|
auto location = locAccess->GetLocation();
|
|
color = this->_visitedLocations.contains(location) ? "black" : "red";
|
|
worldGraph << "\t\"" << location->GetName() << "\"[label=<" << location->GetName() << ":<br/>"
|
|
<< location->GetCurrentItem()->GetName() << "> shape=\"plain\" fontcolor=\"" << color << "\"];";
|
|
worldGraph << "\t\"" << areaName << "\" -> \"" << location->GetName() << "\"[dir=forward color=\"" << color
|
|
<< "\"]";
|
|
}
|
|
}
|
|
|
|
worldGraph << "}";
|
|
worldGraph.close();
|
|
}
|
|
|
|
std::optional<std::string> VerifyLogic(world::WorldPool* worlds,
|
|
const item_pool::ItemPool& items /* = {} */)
|
|
{
|
|
// Run an all locations reachable search
|
|
auto search = Search::AllLocationsReachable(worlds, items);
|
|
search.SearchWorlds();
|
|
|
|
for (const auto& world : *worlds)
|
|
{
|
|
// If all locations should be reachable, make sure they're all reachable
|
|
if (world->Setting("Logic Rules") == "All Locations Reachable")
|
|
{
|
|
auto numlocationsReached =
|
|
std::ranges::count_if(search._visitedLocations, [&](const auto& location) {
|
|
return location->GetWorld() == world.get();
|
|
});
|
|
auto allLocations = world->GetAllLocations(/*includeNonItemLocations = */ true);
|
|
|
|
if (numlocationsReached != allLocations.size())
|
|
{
|
|
std::string errorMsg = "Not all locations reachable! Missing locations:\n";
|
|
// Gather all the missing locations
|
|
std::vector<location::Location*> unreachedLocations = {};
|
|
for (const auto& location : allLocations)
|
|
{
|
|
if (!search._visitedLocations.contains(location))
|
|
{
|
|
unreachedLocations.push_back(location);
|
|
}
|
|
}
|
|
// Only print the first 5 so we don't clog the error message
|
|
for (auto i = 0; i < unreachedLocations.size(); i++)
|
|
{
|
|
errorMsg += "- " + unreachedLocations[i]->GetName() + "\n";
|
|
if (i == 4 && i != unreachedLocations.size())
|
|
{
|
|
errorMsg += "(" + std::to_string(unreachedLocations.size() - i) + " more)";
|
|
break;
|
|
}
|
|
}
|
|
return errorMsg;
|
|
}
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
void GeneratePlaythrough(Randomizer* randomizer)
|
|
{
|
|
auto& worlds = randomizer->GetWorlds();
|
|
LOG_TO_DEBUG("Generating Playthrough");
|
|
// Generate Initial Playthrough
|
|
auto playthroughSearch = Search::Playthrough(&worlds);
|
|
playthroughSearch.SearchWorlds();
|
|
|
|
auto& playthroughSpheres = playthroughSearch._playthroughSpheres;
|
|
|
|
// Keep track of all locations we temporaily take items away from so we can give them back after playthrough calculation
|
|
std::unordered_map<location::Location*, item::Item*> tempEmptyLocations = {};
|
|
// Keep track of all the locations that appear in the playthrough
|
|
std::unordered_set<location::Location*> playthroughLocationsSet = {};
|
|
for (const auto& sphere : playthroughSpheres)
|
|
{
|
|
for (const auto& location : sphere)
|
|
{
|
|
playthroughLocationsSet.insert(location);
|
|
}
|
|
}
|
|
|
|
// Remove all items from locations that are not part of the playthrough set
|
|
for (const auto& world : worlds)
|
|
{
|
|
for (const auto& location : world->GetAllLocations())
|
|
{
|
|
if (!playthroughLocationsSet.contains(location))
|
|
{
|
|
tempEmptyLocations[location] = location->GetCurrentItem();
|
|
location->RemoveCurrentItem();
|
|
}
|
|
}
|
|
}
|
|
|
|
utility::platform::Log("Paring down playthrough");
|
|
// Pare down the playthrough in reverse order so we're paring it down from highest to lowest sphere.
|
|
// This way, lower sphere items will be prioritized for the playthrough
|
|
playthroughSpheres.reverse();
|
|
for (const auto& sphere : playthroughSpheres)
|
|
{
|
|
for (auto& location : sphere)
|
|
{
|
|
auto itemAtLocation = location->GetCurrentItem();
|
|
location->RemoveCurrentItem();
|
|
|
|
// If the game is beatable, temporarily take this item away and erase the location from the playthrough
|
|
// locations
|
|
if (GameBeatable(&worlds))
|
|
{
|
|
tempEmptyLocations[location] = itemAtLocation;
|
|
playthroughLocationsSet.erase(location);
|
|
}
|
|
else
|
|
{
|
|
location->SetCurrentItem(itemAtLocation);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate a new playthrough search incase some spheres were flattened by the previous generation having access
|
|
// to extra items
|
|
auto newSearch = Search::Playthrough(&worlds);
|
|
newSearch.SearchWorlds();
|
|
|
|
// Now do the same process for entrances to pare down the entrance playthrough
|
|
auto& entranceSpheres = newSearch._entranceSpheres;
|
|
std::unordered_map<entrance::Entrance*, area::Area*> nonRequiredEntrances = {};
|
|
|
|
for (auto& sphere : entranceSpheres)
|
|
{
|
|
// Make a copy to avoid iterator invalidation
|
|
auto sphereCopy = sphere;
|
|
for (const auto& entrance : sphereCopy)
|
|
{
|
|
auto connectedArea = entrance->Disconnect();
|
|
if (GameBeatable(&worlds))
|
|
{
|
|
// If the game is still beatable then this entrance is not required
|
|
sphere.remove(entrance);
|
|
nonRequiredEntrances[entrance] = connectedArea;
|
|
}
|
|
else
|
|
{
|
|
// If the entrance is required, reconnect it
|
|
entrance->Connect(connectedArea);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reconnect all non-required entrances
|
|
for (auto& [entrance, connectedArea] : nonRequiredEntrances)
|
|
{
|
|
entrance->Connect(connectedArea);
|
|
}
|
|
|
|
// Give items back their locations
|
|
for (auto& [location, item] : tempEmptyLocations)
|
|
{
|
|
location->SetCurrentItem(item);
|
|
}
|
|
|
|
// Erase all locations not in the playthrough locations set
|
|
for (auto& sphere : newSearch._playthroughSpheres)
|
|
{
|
|
sphere.remove_if([&](const auto& location) {
|
|
return !playthroughLocationsSet.contains(location);
|
|
});
|
|
}
|
|
|
|
// Remove any empty spheres
|
|
newSearch.RemoveEmptySpheres();
|
|
|
|
randomizer->GetPlaythroughSpheres() = newSearch._playthroughSpheres;
|
|
randomizer->GetEntranceSpheres() = newSearch._entranceSpheres;
|
|
}
|
|
|
|
bool GameBeatable(world::WorldPool* worlds, const item_pool::ItemPool& items /* = {} */)
|
|
{
|
|
auto search = Search::Beatable(worlds, items);
|
|
search.SearchWorlds();
|
|
return search._isBeatable;
|
|
}
|
|
|
|
location::LocationPool GetPossibleHintSigns(location::Location* location) {
|
|
// Remove the item at the location
|
|
const auto itemAtLocation = location->GetCurrentItem();
|
|
location->RemoveCurrentItem();
|
|
|
|
// Run an accessible search
|
|
auto search = Search::Accessible(&location->GetWorld()->GetRandomizer()->GetWorlds());
|
|
search.SearchWorlds();
|
|
|
|
// Give the item back
|
|
location->SetCurrentItem(itemAtLocation);
|
|
|
|
// Return all the hint signs that the search found
|
|
location::LocationPool hintSigns{};
|
|
for (auto loc : search._visitedLocations) {
|
|
if (loc->HasCategories("Hint Sign")) {
|
|
hintSigns.push_back(loc);
|
|
}
|
|
}
|
|
return hintSigns;
|
|
}
|
|
|
|
} // namespace randomizer::logic::search
|