UNPKG

brogue

Version:

A Grammar based generative text library based on Tracery.

188 lines (187 loc) 6.74 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExpansionContext = exports.expandLexeme = exports.pickLexeme = exports.expand = void 0; const modifiers_1 = require("./modifiers"); const parse_1 = require("./parse"); const MaxRecursionDepth = 20; class ExpansionContext { constructor(grammar) { this.recursionDepth = 0; this.variableStack = []; this.uniqueTrackers = []; this.grammar = grammar; } pushUniqueTracker() { this.uniqueTrackers.push(new Set()); } popUniqueTracker() { this.uniqueTrackers.pop(); } markLexemeAsSeen(lexeme) { this.uniqueTrackers.forEach((seen) => seen.add(lexeme)); } hasSeenLexeme(lexeme) { return this.uniqueTrackers.some((set) => set.has(lexeme)); } } exports.ExpansionContext = ExpansionContext; function pickLexeme(rule, context) { if (rule.weightedLexemes.length === 0) { return undefined; } const unseenLexemes = rule.weightedLexemes.filter((x) => !context.hasSeenLexeme(x.lexeme)); const totalWeight = unseenLexemes.reduce((acc, next) => acc + next.weight, 0); if (totalWeight === 0) { return undefined; } const weight = context.grammar.random.random() * totalWeight; let current = 0; for (const elem of unseenLexemes) { current += elem.weight; if (weight < current) { return elem.lexeme; } } throw new Error(`Failed to pick lexeme for rule ${rule.name}`); } exports.pickLexeme = pickLexeme; function callExpansionModifier(call, str, context) { if (context.grammar.modifiers.has(call.name)) { const func = context.grammar.modifiers.get(call.name); return func(str, ...call.args); } const builtInFunc = modifiers_1.getBuiltInModifier(call.name); if (builtInFunc) { return builtInFunc(str, context, ...call.args); } throw new Error(`Unrecognized function ${call.name}`); } function generateMarkovString(markovSymbol, context) { const settings = markovSymbol.markov.settings; for (let i = 0; i < settings.maxTries; ++i) { const str = markovSymbol.markov.generate(context); if (str === undefined) { continue; } if (str.length < settings.minCharacters) { continue; } if (settings.uniqueOutput && markovSymbol.markov.sentences.includes(str)) { continue; } return str; } console.log(`Failed to generate markov string for symbol ${markovSymbol.name} after ${settings.maxTries} tries`); return ''; } function findVariableExpansion(name, context) { for (let i = context.variableStack.length - 1; i >= 0; --i) { const value = context.variableStack[i].get(name); if (value !== undefined) { return value; } } return undefined; } function evaluateExpansion(expansion, context, trackUniqueExpansions) { const grammar = context.grammar; const expansionName = expansion.name; // Run expansion let expandedString = ''; const expandedVariable = findVariableExpansion(expansionName, context); if (expandedVariable !== undefined) { expandedString = expandedVariable; } else if (grammar.markovSymbols.has(expansionName)) { const markovSymbol = grammar.markovSymbols.get(expansionName); expandedString = generateMarkovString(markovSymbol, context); } else if (grammar.rules.has(expansionName)) { const rule = grammar.rules.get(expansionName); if (rule.weightedLexemes.every((x) => context.hasSeenLexeme(x.lexeme))) { return undefined; } const lexeme = pickLexeme(rule, context); if (!lexeme) { expandedString = ''; } else { const expandedLexeme = expandLexeme(lexeme, context, trackUniqueExpansions); if (expandedLexeme === undefined) { return undefined; } expandedString = expandedLexeme; } } else if (expansionName) { throw new Error(`Expansion of ${expansionName} failed -- Could not find associated variable or rule or markov symbol with same name.`); } // Process modifiers for (const call of expansion.modifierCalls) { expandedString = callExpansionModifier(call, expandedString, context); } return expandedString; } function formatString(format, args) { return format.replace(/{(\d+)}/g, (match, number) => { return typeof args[number] === 'undefined' ? match : args[number]; }); } function expandVariables(variables, context) { if (!variables) { return undefined; } const expandedVariables = new Map(); variables.forEach((variable) => { const expandedLexeme = expandLexeme(variable.lexeme, context, false); if (expandedLexeme !== undefined) { expandedVariables.set(variable.name, expandedLexeme); } }); return expandedVariables; } function expandLexeme(lexeme, context, trackUniqueExpansions) { try { context.variableStack.push(expandVariables(lexeme.variables, context)); if (context.recursionDepth > MaxRecursionDepth) { throw new Error(`Failed to expand lexeme ${lexeme.originalString}. Stack overflow.`); } context.recursionDepth++; const expansions = lexeme.expansions.map((expansion) => evaluateExpansion(expansion, context, trackUniqueExpansions)); if (trackUniqueExpansions && expansions.some((x) => x === undefined)) { context.markLexemeAsSeen(lexeme); return undefined; } if (trackUniqueExpansions && !lexeme.expansions.some((x) => x.name && !x.isDecorator)) { context.markLexemeAsSeen(lexeme); } context.recursionDepth--; return formatString(lexeme.formatString, expansions.map((x) => x)); } finally { context.variableStack.pop(); } } exports.expandLexeme = expandLexeme; let activeExpansionContext; let expansionDepth = 0; function expand(grammar, text) { const context = activeExpansionContext ?? new ExpansionContext(grammar); activeExpansionContext = context; expansionDepth++; try { const lexeme = parse_1.parseLexeme(text); // Expand global variables if (expansionDepth === 1) { context.variableStack.push(expandVariables(grammar.variables, context)); } // Expand lexeme return expandLexeme(lexeme, context, false) ?? ''; } finally { if (--expansionDepth === 0) { activeExpansionContext = undefined; } } } exports.expand = expand;