#include "requirement.hpp" #include "search.hpp" #include "world.hpp" #include "../utility/container.hpp" #include "../utility/log.hpp" #include "../utility/string.hpp" #include #include #include namespace randomizer::logic::requirement { namespace FormTime { const std::vector ALL_FORM_TIMES = {HUMAN_DAY, HUMAN_NIGHT, WOLF_DAY, WOLF_NIGHT}; const std::vector ALL_FORM_TIMES_AND_TWILIGHT = {HUMAN_DAY, HUMAN_NIGHT, WOLF_DAY, WOLF_NIGHT, TWILIGHT}; const std::vector 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(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(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(this->_args[0]); return item->GetName(); case Type::COUNT: count = std::get(this->_args[0]); item = std::get(this->_args[1]); return "count(" + item->GetName() + ", " + std::to_string(count) + ")"; case Type::EVENT: eventIndex = std::get(this->_args[0]); return "'Event_" + std::to_string(eventIndex) + "'"; case Type::MACRO: macroIndex = std::get(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(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 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(arg), search, formTime, world); }); case Type::AND: return std::all_of( req._args.begin(), req._args.end(), [&](const auto& arg) { return EvaluateRequirementAtFormTime(std::get(arg), search, formTime, world); }); case Type::ITEM: item = std::get(req._args[0]); return search->_ownedItems.contains(item); case Type::COUNT: count = std::get(req._args[0]); item = std::get(req._args[1]); return search->_ownedItems.count(item) >= count; case Type::EVENT: eventIndex = std::get(req._args[0]); return search->_ownedEvents.contains(eventIndex); case Type::MACRO: macroIndex = std::get(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(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