#include "search.hpp" #include "world.hpp" #include "../randomizer.hpp" #include "../utility/general.hpp" #include "../utility/platform.hpp" #include #include 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 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& itemLocations) { std::list 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 = ":
"; 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() << ":
" << location->GetCurrentItem()->GetName() << "> shape=\"plain\" fontcolor=\"" << color << "\"];"; worldGraph << "\t\"" << areaName << "\" -> \"" << location->GetName() << "\"[dir=forward color=\"" << color << "\"]"; } } worldGraph << "}"; worldGraph.close(); } std::optional 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 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 tempEmptyLocations = {}; // Keep track of all the locations that appear in the playthrough std::unordered_set 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 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