Files
dusklight/src/dusk/randomizer/logic/requirement.cpp
T
2026-04-10 17:20:34 -07:00

634 lines
24 KiB
C++

#include "requirement.hpp"
#include "search.hpp"
#include "world.hpp"
#include "../utility/container.hpp"
#include "../utility/log.hpp"
#include "../utility/string.hpp"
#include <algorithm>
#include <ranges>
#include <iostream>
namespace randomizer::logic::requirement
{
namespace FormTime
{
const std::vector<int> ALL_FORM_TIMES = {HUMAN_DAY, HUMAN_NIGHT, WOLF_DAY, WOLF_NIGHT};
const std::vector<int> ALL_FORM_TIMES_AND_TWILIGHT = {HUMAN_DAY, HUMAN_NIGHT, WOLF_DAY, WOLF_NIGHT, TWILIGHT};
const std::vector<int> ALL_FORM_AND_DAY_TIMES = {HUMAN_DAY, HUMAN_NIGHT, WOLF_DAY, WOLF_NIGHT, DAY, NIGHT};
std::string to_string(const int& formTime)
{
std::string formTimeStr = "";
if (formTime & HUMAN_DAY)
formTimeStr += " Human_Day";
if (formTime & HUMAN_NIGHT)
formTimeStr += " Human_Night";
if (formTime & WOLF_DAY)
formTimeStr += " Wolf_Day";
if (formTime & WOLF_NIGHT)
formTimeStr += " Wolf_Night";
if (formTime & TWILIGHT)
formTimeStr += " Twilight";
return formTimeStr;
}
} // namespace FormTime
const extern Requirement NO_REQUIREMENT = Requirement{Type::NOTHING, {}};
const extern Requirement IMPOSSIBLE_REQUIREMENT = Requirement{Type::IMPOSSIBLE, {}};
std::string Requirement::to_string() const
{
std::string reqStr = "";
randomizer::logic::item::Item* item;
Requirement nestedReq;
int count;
int eventIndex;
int macroIndex;
switch (this->_type)
{
case Type::NOTHING:
return "Nothing";
case Type::IMPOSSIBLE:
return "Impossible (Please discover an entrance first)";
case Type::OR:
for (const auto& arg : this->_args)
{
nestedReq = std::get<Requirement>(arg);
if (nestedReq._type == Type::AND || nestedReq._type == Type::OR)
{
reqStr += "(";
reqStr += nestedReq.to_string();
reqStr += ")";
}
else
{
reqStr += nestedReq.to_string();
}
reqStr += " or ";
}
// pop off the last " or "
for (auto i = 0; i < 4; i++)
{
reqStr.pop_back();
}
return reqStr;
case Type::AND:
for (const auto& arg : this->_args)
{
nestedReq = std::get<Requirement>(arg);
if (nestedReq._type == Type::AND || nestedReq._type == Type::OR)
{
reqStr += "(";
reqStr += nestedReq.to_string();
reqStr += ")";
}
else
{
reqStr += nestedReq.to_string();
}
reqStr += " and ";
}
// pop off the last " and "
for (auto i = 0; i < 5; i++)
{
reqStr.pop_back();
}
return reqStr;
case Type::ITEM:
item = std::get<randomizer::logic::item::Item*>(this->_args[0]);
return item->GetName();
case Type::COUNT:
count = std::get<int>(this->_args[0]);
item = std::get<randomizer::logic::item::Item*>(this->_args[1]);
return "count(" + item->GetName() + ", " + std::to_string(count) + ")";
case Type::EVENT:
eventIndex = std::get<int>(this->_args[0]);
return "'Event_" + std::to_string(eventIndex) + "'";
case Type::MACRO:
macroIndex = std::get<int>(this->_args[0]);
return "'Macro_" + std::to_string(macroIndex) + "'";
case Type::DAY:
return "Day";
case Type::NIGHT:
return "Night";
case Type::HUMAN_LINK:
return "Human Link";
case Type::WOLF_LINK:
return "Wolf Link";
case Type::TWILIGHT:
return "Twilight";
case Type::GOLDEN_BUGS:
count = std::get<int>(this->_args[0]);
return "golden_bugs(" + std::to_string(count) + ")";
default:
return reqStr;
}
return reqStr;
}
Requirement ParseRequirementString(const std::string& reqStr,
randomizer::logic::world::World* world,
const bool& forceLogic /* = false */)
{
Requirement req;
std::string logicStr(reqStr);
// First, we make sure that the expression has no missing or extra parenthesis
// and that the nesting level at the beginning is the same at the end.
//
// Logic expressions are split up via spaces, but we only want to evaluate the parts of
// the expression at the highest nesting level for the string that was passed in.
// (We'll recursively call the function later to evaluate deeper levels.) So we replace
// all the spaces on the highest nesting level with an arbitrarily chosen delimeter that shouldn't appear anywhere
// in a logic statement (in req case: '+').
int nestingLevel = 1;
constexpr char delimeter = '+';
for (auto& ch : logicStr)
{
if (ch == '(')
{
nestingLevel++;
}
else if (ch == ')')
{
nestingLevel--;
}
if (nestingLevel == 1 && ch == ' ')
{
ch = delimeter;
}
}
// If the nesting level isn't the same as what we started with, then the logic
// expression is invalid.
if (nestingLevel != 1)
{
throw std::runtime_error("Extra or missing parenthesis within expression: \"" + reqStr + "\"");
}
// Next we split up the expression by the delimeter in the previous step
size_t pos = 0;
std::vector<std::string> splitLogicStr = {};
while ((pos = logicStr.find(delimeter)) != std::string::npos)
{
// When parsing setting checks, take the entire expression
// and the three components individually
auto& chBefore = logicStr[pos - 1];
auto& chAfter = logicStr[pos + 1];
if (chBefore != '!' && chAfter != '!' && chBefore != '=' && chAfter != '=' &&
chBefore != '>' && chAfter != '>' && chBefore != '<' && chAfter != '<')
{
splitLogicStr.push_back(logicStr.substr(0, pos));
logicStr.erase(0, pos + 1);
}
else
{
logicStr.erase(logicStr.begin() + pos);
}
}
splitLogicStr.push_back(logicStr);
// Once we have the different parts of our expression, we can use the number
// of parts we have to determine what kind of expression it is.
// If we only have one part...
if (splitLogicStr.size() == 1)
{
std::string argStr = splitLogicStr[0];
std::ranges::replace(argStr, '_', ' ');
// First, see if we have nothing
if (argStr == "Nothing")
{
req._type = randomizer::logic::requirement::Type::NOTHING;
return req;
}
// Then Human Link...
if (argStr == "Human Link")
{
req._type = randomizer::logic::requirement::Type::HUMAN_LINK;
return req;
}
// Then Wolf Link...
if (argStr == "Wolf Link")
{
req._type = randomizer::logic::requirement::Type::WOLF_LINK;
return req;
}
// Then Twilight...
if (argStr == "Twilight")
{
req._type = randomizer::logic::requirement::Type::TWILIGHT;
return req;
}
// Then an event...
if (argStr[0] == '\'')
{
req._type = randomizer::logic::requirement::Type::EVENT;
std::string eventName(argStr.begin() + 1, argStr.end() - 1); // Remove quotes
int eventId = world->GetEventIndex(eventName);
req._args.emplace_back(eventId);
return req;
}
// NOTE: Checking macros *MUST* come before checking items. Some macros use the exact same name as an item
// and we want the macro to be used in req case instead of just the item
// Then a macro...
if (world->GetMacroIndex(argStr) != -1)
{
req._type = randomizer::logic::requirement::Type::MACRO;
req._args.emplace_back(world->GetMacroIndex(argStr));
return req;
}
// Then an item...
if (world->GetItem(argStr, true) != nullptr)
{
auto item = world->GetItem(argStr);
req._type = randomizer::logic::requirement::Type::ITEM;
req._args.emplace_back(item);
return req;
}
// Then a setting...
else if (randomizer::utility::str::Contains(argStr, "!=", "==", ">=", "<="))
{
bool equalComparison = randomizer::utility::str::Contains(argStr, "==");
bool notEqualComparison = randomizer::utility::str::Contains(argStr, "!=");
bool gteComparison = randomizer::utility::str::Contains(argStr, ">=");
bool lteComparison = randomizer::utility::str::Contains(argStr, "<=");
// Split up the comparison using the second comparison character (which will always be '=')
auto compPos = argStr.rfind('=');
std::string optionName(argStr.begin() + (compPos + 1), argStr.end());
std::string settingName(argStr.begin(), argStr.begin() + (compPos - 1));
// Check using the appropriate comparison function
bool result = false;
if (equalComparison)
{
result = world->Setting(settingName) == optionName.c_str();
}
else if (notEqualComparison)
{
result = world->Setting(settingName) != optionName.c_str();
}
else if (gteComparison)
{
result = world->Setting(settingName) >= optionName.c_str();
}
else if (lteComparison)
{
result = world->Setting(settingName) <= optionName.c_str();
}
if (result == true)
{
req._type = randomizer::logic::requirement::Type::NOTHING;
}
else
{
req._type = randomizer::logic::requirement::Type::IMPOSSIBLE;
}
return req;
}
// Then a count...
else if (argStr.find("count") != std::string::npos)
{
req._type = randomizer::logic::requirement::Type::COUNT;
// Since a count has two arguments (a number and an item), we have
// to split up the string in the parenthesis into those arguments.
// Get rid of parenthesis
std::string countArgs(argStr.begin() + argStr.find('(') + 1, argStr.end() - 1);
// Erase any spaces
// countArgs.erase(std::remove(countArgs.begin(), countArgs.end(), ' '), countArgs.end());
// Split up the arguments
pos = 0;
splitLogicStr = {};
while ((pos = countArgs.find(", ")) != std::string::npos)
{
splitLogicStr.push_back(countArgs.substr(0, pos));
countArgs.erase(0, pos + 2);
}
splitLogicStr.push_back(countArgs);
// Get the arguments
auto& itemName = splitLogicStr[0];
int count = std::stoi(splitLogicStr[1]);
auto item = world->GetItem(itemName);
req._args.emplace_back(count);
req._args.emplace_back(item);
return req;
}
// Then Day...
if (argStr == "Day")
{
req._type = randomizer::logic::requirement::Type::DAY;
return req;
}
// Then Night...
if (argStr == "Night")
{
req._type = randomizer::logic::requirement::Type::NIGHT;
return req;
}
// And finally a health check
// else if (argStr.find("health") != std::string::npos)
// {
// req._type = randomizer::logic::requirement::Type::HEALTH;
// std::string numHeartsStr(argStr.begin() + argStr.find('(') + 1, argStr.end() - 1);
// int numHearts = std::stoi(numHeartsStr);
// req._args.emplace_back(numHearts);
// return req;
// }
// Check Impossible down here since it's very unlikely
else if (argStr == "Impossible")
{
req._type = randomizer::logic::requirement::Type::IMPOSSIBLE;
return req;
}
// Check golden bugs last since it's least likely
else if (argStr.find("golden bugs") != std::string::npos)
{
req._type = randomizer::logic::requirement::Type::GOLDEN_BUGS;
// Get rid of parenthesis
std::string countArg(argStr.begin() + argStr.find('(') + 1, argStr.end() - 1);
int count = std::stoi(countArg);
req._args.emplace_back(count);
return req;
}
throw std::runtime_error("Unrecognized logic symbol: \"" + reqStr + "\"");
}
// If our expression has two parts, then we don't know what that is
if (splitLogicStr.size() == 2)
{
throw std::runtime_error("Unrecognized 2 part expression: " + reqStr);
}
// If we have more than two parts to our expression, then we have either "and"
// or "or".
bool andType = randomizer::utility::container::ElementInContainer(splitLogicStr, "and");
bool orType = randomizer::utility::container::ElementInContainer(splitLogicStr, "or");
// If we have both of them, there's a problem with the logic expression
if (andType && orType)
{
throw std::runtime_error("\"and\" & \"or\" in same nesting level when parsing \"" + reqStr + "\"");
}
if (andType || orType)
{
// Set the appropriate type
if (andType)
{
req._type = randomizer::logic::requirement::Type::AND;
}
else
{
req._type = randomizer::logic::requirement::Type::OR;
}
// Once we know the type, we can erase the "and"s or "or"s and are left with just the deeper
// expressions to be logically operated on.
randomizer::utility::container::FilterAndEraseFromVector(splitLogicStr,
[](const std::string& arg)
{ return arg == "and" || arg == "or"; });
// For each deeper expression, parse it and add it as an argument to the
// Requirement
for (auto& reqStr : splitLogicStr)
{
// Get rid of parenthesis surrounding each deeper expression
if (reqStr[0] == '(')
{
reqStr = reqStr.substr(1, reqStr.length() - 2);
}
req._args.push_back(ParseRequirementString(reqStr, world, forceLogic));
}
}
if (req._type != randomizer::logic::requirement::Type::INVALID)
{
return req;
}
// If we've reached req point, we weren't able to determine a logical operator within the expression
throw std::runtime_error("Could not determine logical operator type from expression: \"" + reqStr + "\"");
return req;
}
bool EvaluateRequirementAtFormTime(const randomizer::logic::requirement::Requirement& req,
randomizer::logic::search::Search* search,
const int& formTime,
randomizer::logic::world::World* world)
{
randomizer::logic::item::Item* item;
int count;
int eventIndex;
int macroIndex;
switch (req._type)
{
case Type::NOTHING:
return true;
case Type::IMPOSSIBLE:
return false;
case Type::OR:
return std::any_of(
req._args.begin(),
req._args.end(),
[&](const auto& arg)
{ return EvaluateRequirementAtFormTime(std::get<Requirement>(arg), search, formTime, world); });
case Type::AND:
return std::all_of(
req._args.begin(),
req._args.end(),
[&](const auto& arg)
{ return EvaluateRequirementAtFormTime(std::get<Requirement>(arg), search, formTime, world); });
case Type::ITEM:
item = std::get<randomizer::logic::item::Item*>(req._args[0]);
return search->_ownedItems.contains(item);
case Type::COUNT:
count = std::get<int>(req._args[0]);
item = std::get<randomizer::logic::item::Item*>(req._args[1]);
return search->_ownedItems.count(item) >= count;
case Type::EVENT:
eventIndex = std::get<int>(req._args[0]);
return search->_ownedEvents.contains(eventIndex);
case Type::MACRO:
macroIndex = std::get<int>(req._args[0]);
return EvaluateRequirementAtFormTime(world->GetMacro(macroIndex), search, formTime, world);
case Type::DAY:
return formTime & FormTime::DAY;
case Type::NIGHT:
return formTime & FormTime::NIGHT;
case Type::HUMAN_LINK:
return formTime & FormTime::HUMAN;
case Type::WOLF_LINK:
return formTime & FormTime::WOLF;
case Type::TWILIGHT:
return formTime & FormTime::TWILIGHT;
case Type::GOLDEN_BUGS:
count = std::get<int>(req._args[0]);
return std::count_if(search->_ownedItems.begin(),
search->_ownedItems.end(),
[](const auto& item) { return item->IsGoldenBug(); }) >= count;
default:
return false;
}
return false;
}
EvalSuccess EvaluateEventRequirement(randomizer::logic::search::Search* search, randomizer::logic::area::EventAccess* event)
{
auto& formTime = search->_areaFormTime[event->GetArea()];
if (EvaluateRequirementAtFormTime(event->GetRequirement(), search, formTime, event->GetArea()->GetWorld()))
{
return EvalSuccess::COMPLETE;
}
return EvalSuccess::NONE;
}
EvalSuccess EvaluateExitRequirement(randomizer::logic::search::Search* search, randomizer::logic::entrance::Entrance* exit)
{
// Some exits in the middle of entrance shuffling will not have a connected area. Ignore these
if (exit->GetConnectedArea() == nullptr)
{
return EvalSuccess::UNNECESSARY;
}
// If the exit is currently disabled, don't try it
if (exit->IsDisabled())
{
return EvalSuccess::NONE;
}
auto& exitFormTimeCache = exit->GetWorld()->GetExitTimeFormCache();
auto parentArea = exit->GetParentArea();
auto connectedArea = exit->GetConnectedArea();
auto parentAreaFormTime = search->_areaFormTime[parentArea];
auto& connectedAreaFormTime = search->_areaFormTime[connectedArea];
auto potentialExitFormTimes = (exitFormTimeCache.contains(exit) ? exitFormTimeCache[exit] : FormTime::ALL);
// LOG_TO_DEBUG("Trying " + connectedArea->GetName());
auto connectedAreaTwilightCleared = connectedArea->TwilightCleared(search);
if (!connectedAreaTwilightCleared)
{
// LOG_TO_DEBUG("Added Twilight");
parentAreaFormTime |= FormTime::TWILIGHT;
potentialExitFormTimes |= FormTime::TWILIGHT;
}
// Calculate the potential form times that we could spread to the connected area. These are the form times
// which the connected area does not have that the parent area has, and that the exit can potentially pass on
// to the connected area
auto potentialFormTimeSpread = ~connectedAreaFormTime & (parentAreaFormTime & potentialExitFormTimes);
// LOG_TO_DEBUG("Potential spreads: " + FormTime::to_string(potentialFormTimeSpread));
// If there's no potential to spread FormTime, then return early
if (potentialFormTimeSpread == FormTime::NONE)
{
// LOG_TO_DEBUG("No potential formtime spread");
return EvalSuccess::NONE;
}
// Check each form time individually and spread the ones which succeed. If any of them pass, set the evaluation success
// to partial.
auto evalSuccess = EvalSuccess::NONE;
const auto& formTimes = connectedAreaTwilightCleared ? FormTime::ALL_FORM_TIMES : FormTime::ALL_FORM_TIMES_AND_TWILIGHT;
for (const auto& formTime : formTimes)
{
if (formTime & potentialFormTimeSpread)
{
if (EvaluateRequirementAtFormTime(exit->GetRequirement(), search, formTime, exit->GetWorld()))
{
if (!connectedAreaTwilightCleared)
{
if (~connectedAreaFormTime & FormTime::TWILIGHT)
{
// LOG_TO_DEBUG("Spread Twilight to " + connectedArea->GetName());
connectedAreaFormTime |= FormTime::TWILIGHT;
evalSuccess = EvalSuccess::PARTIAL;
}
}
else if (formTime != FormTime::TWILIGHT)
{
// LOG_TO_DEBUG("Spread" + FormTime::to_string(formTime) + " to " + connectedArea->GetName());
connectedAreaFormTime |= formTime;
evalSuccess = EvalSuccess::PARTIAL;
}
}
}
else
{
// LOG_TO_DEBUG(FormTime::to_string(formTime) + " is not a potential timespread.");
}
}
if (evalSuccess != EvalSuccess::NONE)
{
search->ExpandFormTimes(connectedArea);
// If the connected area now has complete access, then we mark a complete success instead of just a partial one
}
if (connectedAreaTwilightCleared && ((connectedAreaFormTime & potentialExitFormTimes) == potentialExitFormTimes))
{
evalSuccess = EvalSuccess::COMPLETE;
}
return evalSuccess;
}
EvalSuccess EvaluateLocationRequirement(randomizer::logic::search::Search* search, randomizer::logic::area::LocationAccess* locAccess)
{
auto& formTime = search->_areaFormTime[locAccess->GetArea()];
if (EvaluateRequirementAtFormTime(locAccess->GetRequirement(), search, formTime, locAccess->GetArea()->GetWorld()))
{
return EvalSuccess::COMPLETE;
}
return EvalSuccess::NONE;
}
} // namespace randomizer::logic::requirement