update generator with hint stuff

This commit is contained in:
gymnast86
2026-08-02 01:25:06 -07:00
parent 631cb99208
commit 66f6942dbb
35 changed files with 2266 additions and 198 deletions
+926 -14
View File
@@ -1,7 +1,12 @@
#include "hints.hpp"
#include "../utility/text.hpp"
#include "search.hpp"
#include "world.hpp"
#include "../utility/platform.hpp"
#include "../utility/string.hpp"
#include "../utility/text.hpp"
#include "../utility/random.hpp"
#include "../randomizer.hpp"
#include <ranges>
@@ -16,15 +21,854 @@ namespace randomizer::logic::hints {
{"Temple of Time", "<dark green>"},
{"City in the Sky", "<yellow>"},
{"Palace of Twilight", "<purple>"},
// {"Hyrule Castle", "<silver>"}
{"Hyrule Castle", "<silver>"}
};
static const std::map<std::string, std::string> bossColors = {
{"Diababa", "<green>"},
{"Fyrus", "<red>"},
{"Morpheel", "<blue>"},
{"Stallord", "<orange>"},
{"Blizzeta", "<light blue>"},
{"Armogohma", "<dark green>"},
{"Argorok", "<yellow>"},
{"Zant", "<purple>"},
{"Ganondorf", "<silver>"}
};
static location::Location* GetHintableLocation(const location::LocationPool& locations) {
for (const auto location : locations) {
if (!location->IsHinted()) {
return location;
}
}
return nullptr;
}
static void CalculatePossiblePathLocations(world::WorldPool& worlds) {
LOG_TO_DEBUG("Calculating Possible Path Locations");
// First generate the goal location keys and also remove items from non-
// progress locations since they shouldn't be considered when determining
// path locations
std::unordered_map<location::Location*, item::Item*> nonRequiredLocations{};
for (const auto& world : worlds) {
// Defeat Ganondorf is always a goal location
world->AddGoalLocation(world->GetLocation("Defeat Ganondorf"));
// Required dungeons also have goal locations
for (const auto& dungeon : world->GetDungeonTable() | std::views::values) {
if (dungeon->IsRequired()) {
world->AddGoalLocation(dungeon->GetGoalLocation());
}
}
// Collect items at non-progress locations to give back later
for (const auto& location: world->GetAllLocations()) {
if (!location->IsProgression()) {
nonRequiredLocations[location] = location->GetCurrentItem();
location->RemoveCurrentItem();
}
}
}
// Determine path locations for every progression location with a major item by going through
// and seeing if taking away the item at each location can still access the other locations
for (const auto& world : worlds) {
for (const auto& potentialPathLocation : world->GetAllLocations()) {
auto itemAtLocation = potentialPathLocation->GetCurrentItem();
// Skip over locations with removed items
if (itemAtLocation == nullptr) {
continue;
}
// If this is not a progression location with a major item, skip over it
if (!potentialPathLocation->IsProgression() || !itemAtLocation->IsMajor()) {
continue;
}
// Take the item away from the location
potentialPathLocation->RemoveCurrentItem();
// Run a search without the item
auto search = search::Search::Accessible(&worlds);
search.SearchWorlds();
// Check to see if we can reach each progression location. For each progression location that we can't reach,
// add the potential path location as a path location
for (auto& location : world->GetAllLocations()) {
if (!search._visitedLocations.contains(location)) {
location->AddPathLocation(potentialPathLocation);
}
}
// Then give back the location's item
potentialPathLocation->SetCurrentItem(itemAtLocation);
}
}
// Give back non-progress items
for (auto& [location, item] : nonRequiredLocations) {
location->SetCurrentItem(item);
}
}
// Calculates the passed in location's hint importance, as well as recursively calculates the importance of any
// locations that this one may unlock, but haven't had their own importance calculated yet.
// By the time we call this function, we're only trying to determine if a location is
// possibly required or not required.
//
// This function returns any location that blocks the passed in location from being not required
static location::Location* CalculateLocationHintImportance(
location::Location* location,
std::unordered_set<location::Location*>& currentlyChecking)
{
// If we already know this location's importance, we don't need to calculate it
if (location->GetImportance() != location::Importance::UNKNOWN) {
return nullptr;
}
// Get all the progressive chain locations for this location's item.
// Filter out locations that aren't progression, are already in this
// location's path locations, have an item that we can trivially know
// is barren, or have already been deemed as not required.
std::unordered_set<location::Location*> chainLocations{};
std::unordered_set pathLocations(
location->GetPathLocations().begin(),
location->GetPathLocations().end()
);
for (const auto& loc : location->GetCurrentItem()->GetChainLocations()) {
if (loc->IsProgression() &&
loc->GetCurrentItem()->IsMajor() &&
!pathLocations.contains(loc) &&
!loc->GetCurrentItem()->CanBeInBarrenRegion() &&
loc->GetImportance() != location::Importance::NOT_REQUIRED)
{
chainLocations.insert(loc);
}
}
// Remove the current location, as the item at this location obviously can't lead to itself
chainLocations.erase(location);
// If there are no chain locations left, then this item is not required
if (chainLocations.empty()) {
location->SetImportance(location::Importance::NOT_REQUIRED);
return nullptr;
}
// Get all items from this location's logically required path locations
item_pool::ItemPool logicallyRequiredItems{};
for (auto pathLocation : pathLocations) {
logicallyRequiredItems.push_back(pathLocation->GetCurrentItem());
}
// Perform an importance search to see which locations are reachable without using this item in any way
auto worlds = &location->GetWorld()->GetRandomizer()->GetWorlds();
auto importanceSearch = search::Search::LocationImportance(worlds, location, logicallyRequiredItems);
importanceSearch.SearchWorlds();
// Any locations that we found can be subtracted out of our chain locations because the item at this location
// does not help reach them in any way.
std::erase_if(chainLocations, [&](location::Location* loc) {
return importanceSearch._visitedLocations.contains(loc);
});
// If any remaining chain locations are also currently being checked for their hint importance,
// then we subtract them out. This will allow us to only check locations which aren't potentially
// dependent on each other's items. If we go through all the other locations first and don't
// find anything that makes this location possibly required, we'd only be left with locations
// currently being checked that are dependent on one another. If these location's items are only
// potentially dependent on each other, then they're both not required.
std::erase_if(chainLocations, [&](location::Location* loc) {
return currentlyChecking.contains(loc);
});
// Check to see if any remaining chain locations are at least possible required
for (auto chainLocation : chainLocations) {
// If any remaining chain location hasn't had its hint importance calculated, then do so now
if (chainLocation->GetImportance() == location::Importance::UNKNOWN) {
currentlyChecking.insert(chainLocation);
CalculateLocationHintImportance(chainLocation, currentlyChecking);
currentlyChecking.erase(chainLocation);
}
// If any remaining chain location is at least possibly required, then this location is also possibly required
if (chainLocation->GetImportance() > location::Importance::NOT_REQUIRED) {
location->SetImportance(location::Importance::POSSIBLY_REQUIRED);
return chainLocation;
}
}
// If all remaining chain locations were not required, then this location is also not required
location->SetImportance(location::Importance::NOT_REQUIRED);
return nullptr;
}
static void CalculatePossibleBarrenRegions(const world::WorldPool& worlds) {
LOG_TO_DEBUG("Calculating Barren Regions");
for (const auto& world : worlds) {
auto defeatGanondorf = world->GetLocation("Defeat Ganondorf");
std::unordered_set ganondorfPathLocations(
defeatGanondorf->GetPathLocations().begin(),
defeatGanondorf->GetPathLocations().end()
);
for (auto& location: world->GetAllLocations()) {
// If this location is progression, then add its hint regions to
// the set of potentially barren regions
if (location->IsProgression()) {
for (const auto& locAccess : location->GetAccessList()) {
for (const auto& hintRegion : locAccess->GetArea()->GetHintRegions()) {
world->GetBarrenRegions()[hintRegion] = {};
}
}
}
// Set importance for locations that we can easily calculate as required or not
// required right now.
// Locations which contain barren items, or whose chain locations
// all contain barren items are not required
if (!location->GetCurrentItem()->IsMajor() || std::ranges::none_of(
location->GetCurrentItem()->GetChainLocations(), [](location::Location* loc) {
return loc->GetCurrentItem()->IsMajor();
}
)) {
location->SetImportance(location::Importance::NOT_REQUIRED);
// Locations which are on the path to Ganondorf are always required
} else if (ganondorfPathLocations.contains(location) || location == defeatGanondorf) {
location->SetImportance(location::Importance::REQUIRED);
}
}
// Any location which appears in the playthrough, but doesn't have an importance set
// yet, is going to be possibly required
const auto& playthrough = world->GetRandomizer()->GetPlaythroughSpheres();
for (const auto& sphere : playthrough) {
for (auto loc : sphere) {
if (loc->GetImportance() == location::Importance::UNKNOWN) {
loc->SetImportance(location::Importance::POSSIBLY_REQUIRED);
}
}
}
// Any remaining locations which don't have a hint importance set yet are either possibly required or not required.
// These locations' hint importance take much longer to calculate if we have complex hint importance on
for (const auto& location : world->GetAllLocations()) {
if (location->GetImportance() == location::Importance::UNKNOWN) {
std::unordered_set<location::Location*> currentlyChecking{};
auto blockLocation = CalculateLocationHintImportance(location, currentlyChecking);
if (blockLocation) {
LOG_TO_DEBUG(location->GetName() + ": " + location->GetCurrentItem()->GetName() +
" possibly required due to " + blockLocation->GetName() + ": " + blockLocation->GetCurrentItem()->GetName());
} else {
LOG_TO_DEBUG(location->GetName() + ": " + location->GetCurrentItem()->GetName() + " not required.");
}
}
}
// Now loop through all the progression locations again and remove
// any regions from the barren regions which have any possibly required or
// required items (unless they can be in a barren region). Otherwise, add
// the location to the list of locations in the barren region
for (auto location : world->GetAllLocations()) {
if (!location->IsProgression()) {
continue;
}
for (auto& locAccess : location->GetAccessList()) {
for (const auto& hintRegion : locAccess->GetArea()->GetHintRegions()) {
if (world->GetBarrenRegions().contains(hintRegion)) {
if (location->GetImportance() == location::Importance::NOT_REQUIRED ||
location->GetCurrentItem()->CanBeInBarrenRegion())
{
world->GetBarrenRegions()[hintRegion].push_back(location);
} else {
world->GetBarrenRegions().erase(hintRegion);
}
}
}
}
}
LOG_TO_DEBUG("Barren regions for world " + std::to_string(world->GetID()));
for (const auto& region : world->GetBarrenRegions() | std::views::keys) {
LOG_TO_DEBUG("- " + region)
}
}
}
// Chooses the appropriate plurality for the given text
static std::string ProcessHintPlurality(const std::string& text, bool plural) {
auto pluralStartIndex = text.find('|');
auto pluralEndIndex = text.find('|', pluralStartIndex + 1);
auto pluralSubstr = text.substr(pluralStartIndex + 1, pluralEndIndex - pluralStartIndex - 1);
auto pluralSubstrParts = utility::str::Split(pluralSubstr, '/');
std::string& chosenPluralityStr = plural ? pluralSubstrParts[1] : pluralSubstrParts[0];
return text.substr(0, pluralStartIndex) + chosenPluralityStr + text.substr(pluralEndIndex + 1);
}
static Text GeneratePathHintText(location::Location* location, location::Location* goalLocation) {
// Collect all the hint regions this location is in
std::set<std::string> hintRegionNames{};
for (const auto& locAcc : location->GetAccessList()) {
for (const auto& region : locAcc->GetArea()->GetHintRegions()) {
hintRegionNames.insert(region);
}
}
// Get the text object for each region and surround it with the color red
std::vector<Text> hintRegionText{};
std::ranges::transform(hintRegionNames, std::back_inserter(hintRegionText), [&](const std::string& region) {
return addColor(getTextObject(region), Text::RED);
});
// Make the regions into a listing
Text hintRegionListing = makeTextListing(hintRegionText);
// Get the goal name and apply its color
const auto& goalName = goalLocation->GetGoalName();
const auto& goalColor = bossColors.at(goalName);
Text goalNameText = goalColor + getTextObject(goalName) + "<white>";
// Construct the full text
Text fullText = getTextObject("Path Hint");
fullText.Replace("<regions>", hintRegionListing);
fullText.Replace("<goal name>", goalNameText);
// Handle plurality if necessary
for (auto& langText : fullText.mText) {
if (!langText.empty() && utility::str::Contains(langText, '|')) {
langText = ProcessHintPlurality(langText, hintRegionNames.size() > 1);
}
}
return fullText;
}
static Text GenerateBarrenHintText(const std::string& region) {
auto fullText = getTextObject("Barren Hint");
auto regionText = addColor(getTextObject(region), Text::PURPLE);
fullText.Replace("<region>", regionText);
return fullText;
}
static Text GenerateItemHintText(location::Location* location) {
// Collect all the hint regions this location is in
std::set<std::string> hintRegionNames{};
for (const auto& locAcc : location->GetAccessList()) {
for (const auto& region : locAcc->GetArea()->GetHintRegions()) {
hintRegionNames.insert(region);
}
}
// Get the text object for each region and surround it with the color red
std::vector<Text> hintRegionText{};
std::ranges::transform(hintRegionNames, std::back_inserter(hintRegionText), [&](const std::string& region) {
return addColor(getTextObject(region, Text::PRETTY), Text::RED);
});
// Make the regions into a listing
Text hintRegionListing = makeTextListing(hintRegionText);
// Get item text with color added
auto textType = Text::PRETTY;
Text itemText = addColor(getTextObject(location->GetCurrentItem()->GetName(), textType), Text::GREEN);
// TODO: Cryptic Text
Text fullText = getTextObject("Item Hint");
fullText.Replace("<regions>", hintRegionListing);
fullText.Replace("<Item Pretty or Cryptic Name>", itemText);
// Handle plurality if necessary
for (auto& langText : fullText.mText) {
if (!langText.empty() && utility::str::Contains(langText, '|')) {
langText = ProcessHintPlurality(langText, hintRegionNames.size() > 1);
}
}
return fullText;
}
static Text GenerateLocationHintText(location::Location* location) {
// TODO: Cryptic Text
auto textType = Text::PRETTY;
const auto& itemText = addColor(getTextObject(location->GetCurrentItem()->GetName(), textType), Text::GREEN);
Text fullText = getTextObject("Location Hint");
fullText.Replace("<Item Pretty or Cryptic Name>", itemText);
fullText.Replace("<Location Name>", addColor(Text{location->GetName()}, Text::RED));
return fullText;
}
HintGenerator::HintGenerator(world::World* world) : _world(world) {}
void HintGenerator::GeneratePathHints() {
// Shuffle each pool of path locations so that their orders are random
std::vector<location::Location*> goalLocationsList{};
auto defeatGanondorf = this->_world->GetLocation("Defeat Ganondorf");
for (auto& goalLocation : this->_world->GetGoalLocations()) {
utility::random::ShufflePool(goalLocation->GetPathLocations());
// Initially we want to create hints for required dungeon's goal locations
// and then add Ganondorf in once we have at least 1 hint for each required dungeon
if (goalLocation != defeatGanondorf) {
goalLocationsList.push_back(goalLocation);
}
}
bool addedGanondorfPathLocation = false;
auto numPathHints = this->_world->Setting("Number of Path Hints").GetCurrentOptionAsNumber();
for (size_t i = 0; i < numPathHints; ++i) {
// Try to get at least one hint for each required dungeon first
location::Location* goalLocation{};
if (i < goalLocationsList.size()) {
goalLocation = goalLocationsList[i];
} else {
// Once we've pulled from all required dungeons, then add ganondorf
// to the list and choose randomly
if (i == goalLocationsList.size() && !addedGanondorfPathLocation) {
addedGanondorfPathLocation = true;
goalLocationsList.push_back(defeatGanondorf);
}
if (goalLocationsList.empty()) {
LOG_TO_DEBUG("No more possible path hints")
break;
}
goalLocation = utility::random::RandomElement(goalLocationsList);
}
// Collect all valid path locations to hint for this location
std::vector<location::Location*> validPathLocations{};
for (auto location : goalLocation->GetPathLocations()) {
// Don't choose locations which have expected items or that are already hinted
if (location->HasExpectedItem() || location->IsHinted()) {
continue;
}
// This is unlikely, but also don't choose any locations which are logically
// necessary for accessing *every* possible hint sign if we're placing path hints
// on hint signs.
if (this->_world->Setting("Path Hints on Hint Signs") != "Off") {
// Get a list of all hint signs where we can place path hints to use later
auto possiblePathHintSigns = search::GetPossibleHintSigns(location);
// Remove the ones which wouldn't be relevant
std::erase_if(possiblePathHintSigns, [](location::Location* loc) {
const auto& hintSignPlacement = loc->GetWorld()->Setting("Path Hints on Hint Signs");
return (hintSignPlacement == "Overworld" && loc->HasCategories("Dungeon")) ||
(hintSignPlacement == "Dungeon" && loc->HasCategories("Overworld"));
});
// If none are left, then don't choose this location
if (possiblePathHintSigns.empty()) {
continue;
}
}
validPathLocations.push_back(location);
}
auto hintLocation = GetHintableLocation(validPathLocations);
if (hintLocation == nullptr) {
LOG_TO_DEBUG("No more path locations to hint for " + goalLocation->GetName())
utility::container::Erase(goalLocationsList, goalLocation);
--i;
continue;
}
hintLocation->SetHinted(true);
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as path hint for " + goalLocation->GetName());
auto hintText = GeneratePathHintText(hintLocation, goalLocation);
this->_pathHints.emplace_back(hintText, PathHint{goalLocation, hintLocation});
}
}
void HintGenerator::GenerateBarrenHints() {
std::vector<std::string> barrenPool = {};
std::vector<double> barrenDistributions = {};
for (auto& [barrenRegion, barrenLocations] : this->_world->GetBarrenRegions()) {
barrenPool.push_back(barrenRegion);
// The probability of a region being chosen for a barren hint is the square root
// of how many locations are in that region
barrenDistributions.push_back(sqrt(barrenLocations.size()));
}
std::discrete_distribution<size_t> barrenDistribution(barrenDistributions.begin(), barrenDistributions.end());
auto numBarrenHints = this->_world->Setting("Number of Barren Hints").GetCurrentOptionAsNumber();
for (size_t i = 0; i < numBarrenHints; ++i) {
if (barrenPool.empty()) {
LOG_TO_DEBUG("No more barren regions to hint at.")
break;
}
auto regionIndex = barrenDistribution(utility::random::GetGenerator());
auto& barrenRegion = barrenPool[regionIndex];
// Set all locations in the selected barren region as hinted at
// so we don't hint at them again
for (auto location : this->_world->GetBarrenRegions()[barrenRegion]) {
location->SetHinted(true);
}
auto hintText = GenerateBarrenHintText(barrenRegion);
this->_barrenHints.emplace_back(hintText, BarrenHint{barrenRegion});
LOG_TO_DEBUG("Chose " + barrenRegion + " as hinted barren region");
// Erase the hinted region from the pool
barrenPool.erase(barrenPool.begin() + regionIndex);
barrenDistributions.erase(barrenDistributions.begin() + regionIndex);
// Reset the distribution
barrenDistribution = std::discrete_distribution<size_t>(barrenDistributions.begin(), barrenDistributions.end());
}
}
void HintGenerator::GenerateItemHints() {
location::LocationPool possibleItemHintLocations{};
for (auto location : this->_world->GetAllLocations()) {
// If the location is progression...
// and has a major item...
// and does not have an expected item...
// and is not already hinted...
// then it can be hinted as an item hint
if (location->IsProgression() &&
location->GetCurrentItem()->IsMajor() &&
!location->HasExpectedItem() &&
!location->IsHinted())
{
possibleItemHintLocations.push_back(location);
}
}
// Choose randomly until we've selected the appropriate number of item hints
utility::random::ShufflePool(possibleItemHintLocations);
auto numItemHints = this->_world->Setting("Number of Item Hints").GetCurrentOptionAsNumber();
for (auto i = 0; i < numItemHints; ++i) {
if (possibleItemHintLocations.empty()) {
LOG_TO_DEBUG("No more possible item hint locations.")
break;
}
auto hintLocation = possibleItemHintLocations.back();
possibleItemHintLocations.pop_back();
hintLocation->SetHinted(true);
auto hintText = GenerateItemHintText(hintLocation);
this->_itemHints.emplace_back(hintText, ItemHint{hintLocation});
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + ": " + hintLocation->GetCurrentItem()->GetName() + " as item hint location")
}
}
void HintGenerator::GenerateRemoteLocationHints() {
// Return early if we're not prioritizing remote locations for location hints
if (this->_world->Setting("Prioritize Remote Location Hints") == "Off") {
return;
}
// Gather all the remote locations that are progression and haven't already been hinted
std::vector<location::Location*> remoteLocations{};
for (auto location : this->_world->GetAllLocations()) {
if (location->IsProgression() &&
location->HasCategories("Remote Location") &&
!location->IsHinted())
{
remoteLocations.push_back(location);
}
}
// Shuffle the pool
utility::random::ShufflePool(remoteLocations);
// Get as many remote locations as we can
auto numLocationHints = this->_world->Setting("Number of Location Hints").GetCurrentOptionAsNumber();
for (int i = 0; i < numLocationHints; ++i) {
if (i >= remoteLocations.size()) {
LOG_TO_DEBUG("All remote locations hinted");
break;
}
auto hintLocation = remoteLocations[i];
hintLocation->SetHinted(true);
auto hintText = GenerateLocationHintText(hintLocation);
this->_locationHints.emplace_back(hintText, LocationHint{hintLocation});
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as remote location hint location")
}
}
void HintGenerator::GenerateLocationHints() {
// Collect all possible locations to hint
std::vector<location::Location*> possibleLocationHintLocations{};
for (auto location : this->_world->GetAllLocations()) {
if (location->IsProgression() && !location->IsHinted() && !location->HasExpectedItem()) {
possibleLocationHintLocations.push_back(location);
}
}
// Shuffle the pool
utility::random::ShufflePool(possibleLocationHintLocations);
// Get the necessary number of hints we want. If we failed to generate enough hints for any of
// the other hint types, compensate by adding the difference to the number of location hints
auto numLocationHints = this->_world->Setting("Number of Location Hints").GetCurrentOptionAsNumber();
auto expectedNumHints = this->_world->Setting("Number of Path Hints").GetCurrentOptionAsNumber() +
this->_world->Setting("Number of Barren Hints").GetCurrentOptionAsNumber() +
this->_world->Setting("Number of Item Hints").GetCurrentOptionAsNumber();
auto totalNumHints = this->_pathHints.size() + this->_barrenHints.size() +
this->_itemHints.size() + this->_locationHints.size();
numLocationHints += expectedNumHints - totalNumHints;
size_t curNumLocationHints = this->_locationHints.size();
// Choose location hints until we've filled up the amount we need. Since we previously may
// have already chosen some remote location hints, start with the number we already have
for (auto i = curNumLocationHints; i < numLocationHints; ++i) {
if (possibleLocationHintLocations.empty()) {
LOG_TO_DEBUG("No more possible location hint locations.")
break;
}
auto hintLocation = possibleLocationHintLocations.back();
possibleLocationHintLocations.pop_back();
hintLocation->SetHinted(true);
auto hintText = GenerateLocationHintText(hintLocation);
this->_locationHints.emplace_back(hintText, LocationHint{hintLocation});
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as location hint location")
}
}
static void AssignHintSignHints(const location::LocationPool& hintSigns, std::vector<Hint> hints, world::World* world) {
size_t hintsPerSign = std::ceil(static_cast<double>(hints.size()) / static_cast<double>(hintSigns.size()));
auto& worlds = world->GetRandomizer()->GetWorlds();
auto& hintSignHints = world->GetHintSignHints();
auto hintSignHintsOriginal = hintSignHints;
// Keep trying to place hints until all have been logically placed at least once
bool successfullyPlaceHints = false;
int retryCount = 50;
while (!successfullyPlaceHints) {
--retryCount;
if (retryCount < 0) {
throw std::runtime_error("Failed to properly place hint sign hints. "
"Try using a different seed for generation");
}
// Clear any previous attempt at placing hints
successfullyPlaceHints = true;
hintSignHints = hintSignHintsOriginal;
for (auto& hint : hints) {
item::Item* itemAtHintLocation{};
location::Location* hintLocation{};
if (auto pathHint = std::get_if<PathHint>(&hint.data)) {
hintLocation = pathHint->hintedLocation;
} else if (auto itemHint = std::get_if<ItemHint>(&hint.data)) {
hintLocation = itemHint->location;
}
else if (auto locationHint = std::get_if<LocationHint>(&hint.data)) {
hintLocation = locationHint->location;
}
// Remove this item from the world and see which hint signs are available to place hints
if (hintLocation != nullptr) {
itemAtHintLocation = hintLocation->GetCurrentItem();
hintLocation->RemoveCurrentItem();
}
auto search = search::Search::Accessible(&worlds);
search.SearchWorlds();
location::LocationPool availableHintSigns{};
for (const auto& sign : hintSigns) {
if (search._visitedLocations.contains(sign) && hintSignHints[sign].size() < hintsPerSign) {
availableHintSigns.emplace_back(sign);
}
}
if (availableHintSigns.empty()) {
LOG_TO_DEBUG("No available hint signs to place hint " + hint.text.mText[Text::ENGLISH]);
if (hintLocation != nullptr) {
hintLocation->SetCurrentItem(itemAtHintLocation);
}
successfullyPlaceHints = false;
break;
}
// Place the hint at the hint sign
auto hintSign = utility::random::RandomElement(availableHintSigns);
hintSignHints[hintSign].push_back(hint);
if (hintLocation != nullptr) {
hintLocation->SetCurrentItem(itemAtHintLocation);
}
}
}
// Once we've placed every hint at least once, duplicate hints
// and place them randomly until all hint signs have the
// necessary number of hints. Don't check for logic here since
// every hint can already be logically accessed at some sign
auto duplicateHints = hints;
location::LocationPool availableHintSigns{};
for (const auto& sign : hintSigns) {
if (hintSignHints[sign].size() < hintsPerSign) {
availableHintSigns.emplace_back(sign);
}
}
utility::random::ShufflePool(availableHintSigns);
while (!availableHintSigns.empty()) {
// Reset the pool of duplicate hints if we run out.
// This way we'll duplicate every hint at least n times
// before potentially duplicating each hint n + 1 times
if (duplicateHints.empty()) {
duplicateHints = hints;
}
auto hint = duplicateHints.back();
duplicateHints.pop_back();
location::Location* chosenSign{};
for (auto sign : availableHintSigns) {
// Don't give the same hint to a single sign multiple times
if (!utility::container::ElementInContainer(hintSignHints[sign], hint)) {
chosenSign = sign;
break;
}
}
if (chosenSign == nullptr) {
LOG_TO_DEBUG("Could not find any gossip stones to place hint " + hint.text.mText[Text::ENGLISH] + " Trying a different hint.")
continue;
}
hintSignHints[chosenSign].push_back(hint);
if (hintSignHints[chosenSign].size() >= hintsPerSign) {
utility::container::Erase(availableHintSigns, chosenSign);
}
}
}
void HintGenerator::DistributeHints() {
// Pools to distribute hints
std::vector<Hint> overworldSignHints{};
std::vector<Hint> dungeonSignHints{};
std::vector<Hint> anySignHints{};
static constexpr int MIDNA = 0;
static constexpr int HINT_SIGN = 1;
// Helper function for distributing hints to their appropriate places
auto distributeHintTypeToPools = [&](std::vector<Hint>& hints, const std::string& hintType) {
std::vector<int> hintAssignments{};
if (this->_world->Setting(hintType + " on Midna") != "Off") {
hintAssignments.push_back(MIDNA);
}
if (this->_world->Setting(hintType + " on Hint Signs") != "Off") {
hintAssignments.push_back(HINT_SIGN);
}
// If the user didn't choose any place to assign these hints, return early
if (hintAssignments.empty()) {
return;
}
for (size_t i = 0; i < hints.size(); ++i) {
auto& pathHint = hints[i];
auto distributeTo = hintAssignments[i % hintAssignments.size()];
if (distributeTo == MIDNA) {
this->_world->GetMidnaHints().push_back(pathHint);
} else if (distributeTo == HINT_SIGN) {
auto& pathHintSigns = this->_world->Setting(hintType + " on Hint Signs");
if (pathHintSigns == "Overworld") {
overworldSignHints.push_back(pathHint);
} else if (pathHintSigns == "Dungeon") {
dungeonSignHints.push_back(pathHint);
} else if (pathHintSigns == "Any") {
anySignHints.push_back(pathHint);
}
}
}
};
distributeHintTypeToPools(this->_pathHints, "Path Hints");
distributeHintTypeToPools(this->_barrenHints, "Barren Hints");
distributeHintTypeToPools(this->_itemHints, "Item Hints");
distributeHintTypeToPools(this->_locationHints, "Location Hints");
// Get our pools of overworld and dungeon signs
location::LocationPool overworldSignLocations{};
location::LocationPool dungeonSignLocations{};
for (auto hintSign : this->_world->GetHintSignLocations()) {
if (hintSign->HasCategories("Overworld")) {
overworldSignLocations.push_back(hintSign);
}
if (hintSign->HasCategories("Dungeon")) {
dungeonSignLocations.push_back(hintSign);
}
}
utility::random::ShufflePool(anySignHints);
// Distribute our hints which can be on any sign to either overworld or dungeon. Ideally
// we fill up each pool of hints to match the number of signs there are equally. Only do
// this if we're already placing some hints on only dungeon, or only overworld hint signs.
if (!overworldSignHints.empty() || !dungeonSignHints.empty()) {
for (size_t i = 0; i < anySignHints.size(); ++i) {
const auto& anySignHint = anySignHints[i];
size_t hintsPerOverworldSign = overworldSignHints.size() / overworldSignLocations.size();
size_t hintsPerDungeonSign = dungeonSignHints.size() / dungeonSignLocations.size();
if (hintsPerOverworldSign < hintsPerDungeonSign) {
overworldSignHints.push_back(anySignHint);
} else if (hintsPerDungeonSign < hintsPerOverworldSign) {
dungeonSignHints.push_back(anySignHint);
} else {
if (dungeonSignHints.empty() || (dungeonSignHints.size() % dungeonSignLocations.size() != 0 && i % 2 != 0)) {
dungeonSignHints.push_back(anySignHint);
} else {
overworldSignHints.push_back(anySignHint);
}
}
}
anySignHints.clear();
}
// Now distribute hints to our hint signs
// If no hints have to be constrained to either overworld or dungeon signs,
// then distribute all hints over all signs
if (overworldSignHints.empty() && dungeonSignHints.empty()) {
AssignHintSignHints(this->_world->GetHintSignLocations(), anySignHints, this->_world);
}
if (!overworldSignHints.empty()) {
AssignHintSignHints(overworldSignLocations, overworldSignHints, this->_world);
}
if (!dungeonSignHints.empty()) {
AssignHintSignHints(dungeonSignLocations, dungeonSignHints, this->_world);
}
}
void HintGenerator::FinalizeHintSignText() {
for (const auto& [sign, hints] : this->_world->GetHintSignHints()) {
auto key = sign->GetName() + " Text";
auto& signText = this->_world->AddNewText(key);
// Put on all the hints
for (const auto& hint : hints) {
signText += hint.text;
signText.PadToNextBox();
}
// pop off last '\n' so we don't have an extra blank textbox
for (auto& text : signText.mText) {
text.pop_back();
}
}
}
// Tell the player which dungeons are required on the sign in front of Link's House
static void GenerateRequiredDungeonsHint(world::WorldPool& worlds) {
for (const auto& world : worlds) {
auto& requiredDungeonText = world->AddNewText("Links House Sign");
// Use dungeonColors to loop through in base game dungeon order
for (const auto& [dungeonName, color] : dungeonColors) {
// Hyrule Castle is implicitly required
if (dungeonName == "Hyrule Castle") {
continue;
}
auto dungeon = world->GetDungeon(dungeonName);
if (dungeon->IsRequired()) {
requiredDungeonText += color + getTextObject(dungeonName) + "\n";
@@ -42,7 +886,7 @@ namespace randomizer::logic::hints {
const std::list<std::string>& textNames,
Text::Color color) {
auto itemName = world->GetLocation(locationName)->GetCurrentItem()->GetName();
auto itemStandardName = addColor(getTextObject(itemName), color, 1, true);
auto itemStandardName = addColor(getTextObject(itemName), color);
auto itemPrettyName = addColor(getTextObject(itemName, Text::PRETTY), color);
for (const auto& textName : textNames) {
auto& text = world->AddNewText(textName);
@@ -103,34 +947,102 @@ namespace randomizer::logic::hints {
if (numRequiredDungeons > 0) {
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro At Least One Dungeon");
midnaHintText.Replace("<required dungeon count>", std::to_string(numRequiredDungeons));
// Add newlines to begin listing dungeons on the next textbox
midnaHintText += "\n\n\n\n";
midnaHintText.PadToNextBox();
// Then loop through again to add the dungeon names.
// Use dungeonColors to loop through in base game dungeon order
int displayedRequiredDungeons = 0;
for (const auto& [dungeonName, color] : dungeonColors) {
// Hyrule Castle is implicitly required
if (dungeonName == "Hyrule Castle") {
continue;
}
auto dungeon = world->GetDungeon(dungeonName);
if (dungeon->IsRequired()) {
++displayedRequiredDungeons;
midnaHintText += color + getTextObject(dungeonName) + "<white>";
// Add newline after every dungeon except the last one
if (displayedRequiredDungeons < numRequiredDungeons) {
midnaHintText += "\n";
}
midnaHintText += color + getTextObject(dungeonName) + "<white>\n";
}
}
} else {
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro Zero Dungeons");
}
midnaHintText.PadToNextBox();
midnaHintText.BreakLines();
// Separate Midna hints based on type
std::vector<Hint> midnaPathHints{};
std::vector<Hint> midnaBarrenHints{};
std::vector<Hint> midnaItemHints{};
std::vector<Hint> midnaLocationHints{};
for (auto& hint : world->GetMidnaHints()) {
if (std::holds_alternative<PathHint>(hint.data)) {
midnaPathHints.push_back(hint);
} else if (std::holds_alternative<BarrenHint>(hint.data)) {
midnaBarrenHints.push_back(hint);
} else if (std::holds_alternative<ItemHint>(hint.data)) {
midnaItemHints.push_back(hint);
} else if (std::holds_alternative<LocationHint>(hint.data)) {
midnaLocationHints.push_back(hint);
}
}
// Helper function for adding hint text based on type
auto addHintsToMidnaText = [&](const std::vector<Hint>& hints, const std::string& type) {
if (!hints.empty()) {
midnaHintText += getTextObject("Midna Hints " + type + " Intro");
midnaHintText.PadToNextBox();
for (auto& [text, data] : hints) {
midnaHintText += text;
midnaHintText.PadToNextBox();
}
}
};
addHintsToMidnaText(midnaPathHints, "Path Hints");
// Barren hints are presented slightly differently
if (!midnaBarrenHints.empty()) {
midnaHintText += getTextObject("Midna Hints Barren Hints Intro");
midnaHintText.PadToNextBox();
for (auto& [text, data] : midnaBarrenHints) {
midnaHintText += addColor(getTextObject(std::get<BarrenHint>(data).region), Text::PURPLE) + "\n";
}
midnaHintText.PadToNextBox();
}
addHintsToMidnaText(midnaItemHints, "Item Hints");
addHintsToMidnaText(midnaLocationHints, "Location Hints");
// pop off the last new line so we don't get an extra blank textbox
for (auto& text : midnaHintText.mText) {
text.pop_back();
}
}
}
void GenerateAllHints(world::WorldPool& worlds) {
utility::platform::Log("Generating Hints...");
CalculatePossiblePathLocations(worlds);
CalculatePossibleBarrenRegions(worlds);
for (const auto& world : worlds) {
auto hintGenerator = HintGenerator{world.get()};
// Select barren hints first, as we don't want hinted barren regions to conflict with
// location hints
hintGenerator.GenerateBarrenHints();
// Select remote location hints next as these are the most specific ones.
hintGenerator.GenerateRemoteLocationHints();
// Then come path and item hints which hint at specific locations that have good items
hintGenerator.GeneratePathHints();
hintGenerator.GenerateItemHints();
// And finally regular location hints can just be any location
hintGenerator.GenerateLocationHints();
hintGenerator.DistributeHints();
hintGenerator.FinalizeHintSignText();
}
GenerateRequiredDungeonsHint(worlds);
GenerateItemTextReplacements(worlds);
GenerateMidnaHintsText(worlds);
}
}
}