diff --git a/src/logic/LogicHelper.ts b/src/logic/LogicHelper.ts index a30bac5a..1e669d47 100644 --- a/src/logic/LogicHelper.ts +++ b/src/logic/LogicHelper.ts @@ -1,233 +1,21 @@ import _ from 'lodash'; -import BooleanExpression, { Op, ReducerArg } from './BooleanExpression'; +import BooleanExpression, { Op } from './BooleanExpression'; import prettytemNames from '../data/prettyItemNames.json'; -import { - OptionValue, - OptionsCommand, - Settings, -} from '../permalink/SettingsTypes'; -import { Requirements, parseItemCountRequirement } from './Requirements'; +import { parseItemCountRequirement } from './Requirements'; type NestedArray = (T | NestedArray)[]; -interface EvaluatedBooleanExpression { - items: EvaluatedRequirement[]; - type: Op; - value: boolean; -} - -type EvaluatedRequirement = - | EvaluatedBooleanExpression - | { - item: string; - value: boolean; - }; - export interface ReadableRequirement { item: string; name: string; } -function expandRequirement( - requirement: string, - requirements: Requirements, - settings: Settings, - visitedRequirements: Set, -) { - const requirementValue = requirements[requirement]; - if (requirementValue) { - if (visitedRequirements.has(requirement)) { - return 'Impossible'; - } - return booleanExpressionForRequirements( - requirementValue, - requirements, - settings, - new Set(visitedRequirements).add(requirement), - ); - } - - const trickMatch = requirement.match(/^(.+) Trick$/); - let expandedRequirement; - - if (trickMatch) { - const trickName = trickMatch[1]; - // Hack: make up an "enabled tricks" setting - expandedRequirement = `Option "enabled-tricks" Contains "${trickName}"`; - } else { - expandedRequirement = requirement; - } - - const optionEnabledRequirementValue = checkOptionEnabledRequirement( - expandedRequirement, - settings, - ); - if (!_.isNil(optionEnabledRequirementValue)) { - return optionEnabledRequirementValue ? 'Nothing' : 'Impossible'; - } - return expandedRequirement; -} - -function booleanExpressionForTokens( - expressionTokens: string[], - requirements: Requirements, - settings: Settings, - visitedRequirements: Set, -): BooleanExpression { - const itemsForExpression = []; - let expressionTypeToken; - while (!_.isEmpty(expressionTokens)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const currentToken = expressionTokens.shift()!; - if (currentToken === '&' || currentToken === '|') { - expressionTypeToken = currentToken; - } else if (currentToken === '(') { - const childExpression = booleanExpressionForTokens( - expressionTokens, - requirements, - settings, - visitedRequirements, - ); - itemsForExpression.push(childExpression); - } else if (currentToken === ')') { - break; - } else { - itemsForExpression.push( - expandRequirement( - currentToken, - requirements, - settings, - visitedRequirements, - ), - ); - } - } - if (expressionTypeToken === '|') { - return BooleanExpression.or(...itemsForExpression); - } - return BooleanExpression.and(...itemsForExpression); -} - -function splitExpression(expression: string) { - // console.log(expression); - return _.compact(_.map(expression.split(/\s*([(&|)])\s*/g), _.trim)); -} - -export function requirementImplies(firstRequirement: string, secondRequirement: string) { - if (firstRequirement === secondRequirement) { - return true; - } - if (firstRequirement === 'Impossible') { - return true; - } - - if (secondRequirement === 'Nothing') { - return true; - } - const firstItemCountRequirement = parseItemCountRequirement(firstRequirement); - const secondItemCountRequirement = parseItemCountRequirement(secondRequirement); - - if (!_.isNil(firstItemCountRequirement) && !_.isNil(secondItemCountRequirement) && - firstItemCountRequirement.itemName === secondItemCountRequirement.itemName) { - return firstItemCountRequirement.countRequired > secondItemCountRequirement.countRequired; - } - return false; -} - -export function booleanExpressionForRequirements( - requirement: string, - requirements: Requirements, - settings: Settings, - visitedRequirements = new Set(), -) { - const expressionTokens = splitExpression(requirement); - return booleanExpressionForTokens( - expressionTokens, - requirements, - settings, - visitedRequirements, - ); -} - -function checkOptionEnabledRequirement( - requirement: string, - settings: Settings, -) { - const matchers: { - regex: RegExp; - value: (optionValue: OptionValue, expectedValue: string) => boolean; - }[] = [ - { - regex: /^Option "([^"]+)" Enabled$/, - value: (optionValue) => Boolean(optionValue), - }, - { - regex: /^Option "([^"]+)" Disabled$/, - value: (optionValue) => !optionValue, - }, - { - regex: /^Option "([^"]+)" Is "([^"]+)"$/, - value: (optionValue, expectedValue) => - optionValue === expectedValue, - }, - // special case for integers after 'Is' - { - regex: /^Option "([^"]+)" Is ([^"]+)$/, - value: (optionValue, expectedValue) => - optionValue === parseInt(expectedValue, 10), - }, - { - regex: /^Option "([^"]+)" Is Not "([^"]+)"$/, - value: (optionValue, expectedValue) => - optionValue !== expectedValue, - }, - { - regex: /^Option "([^"]+)" Contains "([^"]+)"$/, - value: (optionValue, expectedValue) => - (Array.isArray(optionValue) || - typeof optionValue === 'string') && - optionValue.includes(expectedValue), - }, - { - regex: /^Option "([^"]+)" Does Not Contain "([^"]+)"$/, - value: (optionValue, expectedValue) => - (Array.isArray(optionValue) || - typeof optionValue === 'string') && - !optionValue.includes(expectedValue), - }, - ]; - - let optionEnabledRequirementValue; - - _.forEach(matchers, (matcher) => { - const requirementMatch = requirement.match(matcher.regex); - if (requirementMatch) { - const option = requirementMatch[1] as OptionsCommand; - - let optionValue: OptionValue | undefined; - if ((option as string) === 'enabled-tricks') { - // Hack: if this is our made up 'enabled-tricks' setting, retrieve - // the right setting - optionValue = settings['enabled-tricks-bitless'].length - ? settings['enabled-tricks-bitless'] - : settings['enabled-tricks-glitched']; - } else { - optionValue = settings[option]; - } - const expectedValue = requirementMatch[2]; - optionEnabledRequirementValue = - optionValue !== undefined && - matcher.value(optionValue, expectedValue); - - return false; // break loop - } - return true; // continue - }); - - return optionEnabledRequirementValue; -} - -export function createReadableRequirements(requirements: EvaluatedBooleanExpression) { +/** + * Turn a boolean expression into a readable tooltip requirements list. + * The top-level list is essentially a list of bullet points, while + * the nested lists are the brackets, ` and `s, ` or `s, and actual items. + */ +export function createReadableRequirements(requirements: BooleanExpression) { switch (requirements.type) { case Op.And: return _.map(requirements.items, (item) => @@ -241,20 +29,20 @@ export function createReadableRequirements(requirements: EvaluatedBooleanExpress } function createReadableRequirementsHelper( - requirements: EvaluatedRequirement, + requirements: BooleanExpression | string, ): NestedArray { - if ('item' in requirements) { - const prettyItemName = prettyNameForItemRequirement(requirements.item); + if (!BooleanExpression.isExpression(requirements)) { + const prettyItemName = prettyNameForItemRequirement(requirements); return [ { - item: requirements.item, + item: requirements, name: prettyItemName, }, ]; } return _.map(requirements.items, (item, index) => { const currentResult: NestedArray = []; - if ('items' in item) { + if (BooleanExpression.isExpression(item)) { // expression currentResult.push([ { @@ -288,59 +76,6 @@ function createReadableRequirementsHelper( }); } -export function evaluateRequirements(requirements: BooleanExpression) { - const generateReducerFunction = - (getAccumulatorValue: (acc: boolean, value: boolean) => boolean) => - ({ - accumulator, - item, - isReduced, - }: ReducerArg) => { - if (isReduced) { - return { - items: _.concat(accumulator.items, item), - type: accumulator.type, - value: getAccumulatorValue(accumulator.value, item.value), - }; - } - - const wrappedItem = { - item, - value: false, - }; - - return { - items: _.concat(accumulator.items, wrappedItem), - type: accumulator.type, - value: getAccumulatorValue( - accumulator.value, - wrappedItem.value, - ), - }; - }; - - return requirements.reduce({ - andInitialValue: { - items: [], - type: Op.And, - value: true, - }, - andReducer: (reducerArgs) => - generateReducerFunction( - (accumulatorValue, itemValue) => accumulatorValue && itemValue, - )(reducerArgs), - orInitialValue: { - items: [], - type: Op.Or, - value: false, - }, - orReducer: (reducerArgs) => - generateReducerFunction( - (accumulatorValue, itemValue) => accumulatorValue || itemValue, - )(reducerArgs), - }); -} - function prettyNameForItemRequirement(itemRequirement: string) { const itemCountRequirement = parseItemCountRequirement(itemRequirement); if (!_.isNil(itemCountRequirement)) { diff --git a/src/logic/LogicParser.ts b/src/logic/LogicParser.ts new file mode 100644 index 00000000..b0debd5e --- /dev/null +++ b/src/logic/LogicParser.ts @@ -0,0 +1,192 @@ +import _ from 'lodash'; +import { + Settings, + OptionValue, + OptionsCommand, +} from '../permalink/SettingsTypes'; +import BooleanExpression from './BooleanExpression'; +import { Requirements } from './Logic'; + +/** + * Given a requirement name, a list of macros, and the current settings, + * builds a boolean expression that only depends on tracker items + * (settings are pre-evaluated). + */ +export function booleanExpressionForRequirements( + requirement: string, + requirements: Requirements, + settings: Settings, + visitedRequirements = new Set(), +) { + const expressionTokens = splitExpression(requirement); + return booleanExpressionForTokens( + expressionTokens, + requirements, + settings, + visitedRequirements, + ); +} + + +function expandRequirement( + requirement: string, + requirements: Requirements, + settings: Settings, + visitedRequirements: Set, +) { + const requirementValue = requirements[requirement]; + if (requirementValue) { + if (visitedRequirements.has(requirement)) { + return 'Impossible'; + } + return booleanExpressionForRequirements( + requirementValue, + requirements, + settings, + new Set(visitedRequirements).add(requirement), + ); + } + + const trickMatch = requirement.match(/^(.+) Trick$/); + let expandedRequirement; + + if (trickMatch) { + const trickName = trickMatch[1]; + // Hack: make up an "enabled tricks" setting + expandedRequirement = `Option "enabled-tricks" Contains "${trickName}"`; + } else { + expandedRequirement = requirement; + } + + const optionEnabledRequirementValue = checkOptionEnabledRequirement( + expandedRequirement, + settings, + ); + if (!_.isNil(optionEnabledRequirementValue)) { + return optionEnabledRequirementValue ? 'Nothing' : 'Impossible'; + } + return expandedRequirement; +} + +function booleanExpressionForTokens( + expressionTokens: string[], + requirements: Requirements, + settings: Settings, + visitedRequirements: Set, +): BooleanExpression { + const itemsForExpression = []; + let expressionTypeToken; + while (!_.isEmpty(expressionTokens)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const currentToken = expressionTokens.shift()!; + if (currentToken === '&' || currentToken === '|') { + expressionTypeToken = currentToken; + } else if (currentToken === '(') { + const childExpression = booleanExpressionForTokens( + expressionTokens, + requirements, + settings, + visitedRequirements, + ); + itemsForExpression.push(childExpression); + } else if (currentToken === ')') { + break; + } else { + itemsForExpression.push( + expandRequirement( + currentToken, + requirements, + settings, + visitedRequirements, + ), + ); + } + } + if (expressionTypeToken === '|') { + return BooleanExpression.or(...itemsForExpression); + } + return BooleanExpression.and(...itemsForExpression); +} + +function splitExpression(expression: string) { + // console.log(expression); + return _.compact(_.map(expression.split(/\s*([(&|)])\s*/g), _.trim)); +} + +function checkOptionEnabledRequirement( + requirement: string, + settings: Settings, +) { + const matchers: { + regex: RegExp; + value: (optionValue: OptionValue, expectedValue: string) => boolean; + }[] = [ + { + regex: /^Option "([^"]+)" Enabled$/, + value: (optionValue) => Boolean(optionValue), + }, + { + regex: /^Option "([^"]+)" Disabled$/, + value: (optionValue) => !optionValue, + }, + { + regex: /^Option "([^"]+)" Is "([^"]+)"$/, + value: (optionValue, expectedValue) => + optionValue === expectedValue, + }, + // special case for integers after 'Is' + { + regex: /^Option "([^"]+)" Is ([^"]+)$/, + value: (optionValue, expectedValue) => + optionValue === parseInt(expectedValue, 10), + }, + { + regex: /^Option "([^"]+)" Is Not "([^"]+)"$/, + value: (optionValue, expectedValue) => + optionValue !== expectedValue, + }, + { + regex: /^Option "([^"]+)" Contains "([^"]+)"$/, + value: (optionValue, expectedValue) => + (Array.isArray(optionValue) || + typeof optionValue === 'string') && + optionValue.includes(expectedValue), + }, + { + regex: /^Option "([^"]+)" Does Not Contain "([^"]+)"$/, + value: (optionValue, expectedValue) => + (Array.isArray(optionValue) || + typeof optionValue === 'string') && + !optionValue.includes(expectedValue), + }, + ]; + + let optionEnabledRequirementValue; + + _.forEach(matchers, (matcher) => { + const requirementMatch = requirement.match(matcher.regex); + if (requirementMatch) { + const option = requirementMatch[1] as OptionsCommand; + + let optionValue: OptionValue | undefined; + if ((option as string) === 'enabled-tricks') { + // Hack: if this is our made up 'enabled-tricks' setting, retrieve + // the right setting + optionValue = settings['enabled-tricks-bitless'].length + ? settings['enabled-tricks-bitless'] + : settings['enabled-tricks-glitched']; + } else { + optionValue = settings[option]; + } + const expectedValue = requirementMatch[2]; + optionEnabledRequirementValue = + optionValue !== undefined && + matcher.value(optionValue, expectedValue); + + return false; // break loop + } + return true; // continue + }); + + return optionEnabledRequirementValue; +} diff --git a/src/logic/Requirements.ts b/src/logic/Requirements.ts index b9b126f7..9c858493 100644 --- a/src/logic/Requirements.ts +++ b/src/logic/Requirements.ts @@ -15,7 +15,28 @@ export function parseItemCountRequirement(requirement: string) { return null; } -export function evaluateRequirement( +export function requirementImplies(firstRequirement: string, secondRequirement: string) { + if (firstRequirement === secondRequirement) { + return true; + } + if (firstRequirement === 'Impossible') { + return true; + } + + if (secondRequirement === 'Nothing') { + return true; + } + const firstItemCountRequirement = parseItemCountRequirement(firstRequirement); + const secondItemCountRequirement = parseItemCountRequirement(secondRequirement); + + if (!_.isNil(firstItemCountRequirement) && !_.isNil(secondItemCountRequirement) && + firstItemCountRequirement.itemName === secondItemCountRequirement.itemName) { + return firstItemCountRequirement.countRequired > secondItemCountRequirement.countRequired; + } + return false; +} + +export function isRequirementMet( requirement: string, itemCounts: Record, additionalItems: Record, @@ -57,11 +78,3 @@ export function areRequirementsMet( isRequirementMet(requirement, itemCounts, additionalItems), ); } - -export function isRequirementMet( - requirement: string, - itemCounts: Record, - additionalItems: Record, -) { - return evaluateRequirement(requirement, itemCounts, additionalItems); -} diff --git a/src/selectors/LogicOutput.ts b/src/selectors/LogicOutput.ts index 34ceaec1..cc2ca38a 100644 --- a/src/selectors/LogicOutput.ts +++ b/src/selectors/LogicOutput.ts @@ -8,15 +8,16 @@ import { createIsCheckBannedPredicate, } from '../logic/Locations'; import ItemLocation from '../logic/ItemLocation'; -import { ReadableRequirement, booleanExpressionForRequirements, createReadableRequirements, evaluateRequirements, requirementImplies } from '../logic/LogicHelper'; +import { ReadableRequirement, createReadableRequirements } from '../logic/LogicHelper'; import _ from 'lodash'; import BooleanExpression from '../logic/BooleanExpression'; import { inventorySelector } from './Inventory'; import { checkedChecksSelector } from './Locations'; import { discoveredDungeonEntrancesSelector, requiredDungeonsSelector } from './Dungeons'; import { settingsSelector } from './Settings'; -import { areRequirementsMet, isRequirementMet } from '../logic/Requirements'; +import { areRequirementsMet, isRequirementMet, requirementImplies } from '../logic/Requirements'; import { getPastRequirementsExpression, getSettingsRequirements } from '../logic/LogicTweaks'; +import { booleanExpressionForRequirements } from '../logic/LogicParser'; export type LogicalState = 'checked' | 'inLogic' | 'semiLogic' | 'outLogic'; @@ -129,9 +130,8 @@ const expandedExpressionsSelector = createSelector( secondRequirement, ), ); - const evaluatedRequirements = evaluateRequirements(simplifiedExpression); const needs = createReadableRequirements( - evaluatedRequirements, + simplifiedExpression, ); result[loc.id] = {