random-tables-mcp
Version:
An MCP (Model Context Protocol) server for managing and rolling on random-table assets used in tabletop RPGs. Create, update, and roll on random tables with support for nested tables, weighted entries, and range-based results.
161 lines • 7.15 kB
JavaScript
import { RollTemplate } from '../domain/index.js';
/**
* Use case for evaluating a roll template.
*/
export class EvaluateTemplateUseCase {
/**
* Creates a new EvaluateTemplateUseCase instance.
* @param templateRepository The template repository to use.
* @param tableRepository The table repository to use.
* @param rng The random number generator to use.
*/
constructor(templateRepository, tableRepository, rng) {
this.templateRepository = templateRepository;
this.tableRepository = tableRepository;
this.rng = rng;
}
/**
* Executes the use case.
* @param templateId The ID of the template to evaluate.
* @param count The number of evaluations to perform (default: 1).
* @returns An array of template evaluation results.
*/
async execute(templateId, count = 1) {
// Validate inputs
if (!templateId) {
throw new Error('Template ID is required');
}
if (count < 1) {
throw new Error('Count must be at least 1');
}
// Get the template from the repository
const templateEntity = await this.templateRepository.getById(templateId);
if (!templateEntity) {
throw new Error(`Template with ID ${templateId} not found`);
}
// Get the template string
const templateString = templateEntity.template.toString();
// Perform the evaluations
const results = [];
for (let i = 0; i < count; i++) {
// Create a new template instance for each evaluation
const template = new RollTemplate(templateString);
// Evaluate the template
const evaluatedTemplate = await this.evaluateTemplate(template);
// Add the result
results.push({
originalTemplate: templateString,
evaluatedTemplate,
});
}
return results;
}
/**
* Evaluates a template by resolving all references.
* @param template The template to evaluate.
* @param maxDepth Maximum resolution depth to prevent infinite loops.
* @param visitedTables Set of table IDs and names that have been visited in this resolution chain.
* @returns The evaluated template string.
*/
async evaluateTemplate(template, maxDepth = RollTemplate.MAX_RESOLUTION_DEPTH, visitedTables = new Set()) {
// If max depth reached, return as is
if (maxDepth <= 0) {
return template.toString();
}
// Extract all references from the template
const references = template.extractReferences();
// If no references, return the original template
if (references.length === 0) {
return template.toString();
}
// Process each reference
let evaluatedTemplate = template;
for (const reference of references) {
// Skip empty references
if (!reference.tableId && !reference.tableName) {
continue;
}
// Find the referenced table
const referencedTable = await this.findReferencedTable(reference);
// If table not found, skip this reference
if (!referencedTable) {
continue;
}
// Check for circular references
if (visitedTables.has(referencedTable.id) ||
(referencedTable.name && visitedTables.has(referencedTable.name))) {
// Replace with a warning message instead of causing an infinite loop
const warningMessage = `[Circular reference detected: ${referencedTable.name || referencedTable.id}]`;
evaluatedTemplate = evaluatedTemplate.replaceReference(reference, warningMessage);
continue;
}
// Add this table to the visited set to track the resolution chain
visitedTables.add(referencedTable.id);
if (referencedTable.name) {
visitedTables.add(referencedTable.name);
}
// Roll on the referenced table and get the results
const referenceResults = await this.rollOnReferencedTable(referencedTable, reference.rollCount, maxDepth - 1, new Set(visitedTables));
// Join the results with the specified separator
const replacementValue = referenceResults.join(reference.separator);
// Replace the reference in the template
evaluatedTemplate = evaluatedTemplate.replaceReference(reference, replacementValue);
}
// Check if there are still unresolved references and we haven't reached max depth
if (evaluatedTemplate.hasUnresolvedReferences() && maxDepth > 1) {
// Resolve remaining references recursively
return this.evaluateTemplate(evaluatedTemplate, maxDepth - 1, visitedTables);
}
// Return the evaluated template
return evaluatedTemplate.toString();
}
/**
* Finds a table referenced by a template reference.
* @param reference The template reference
* @returns The referenced table, or null if not found
*/
async findReferencedTable(reference) {
// Try to get the referenced table by ID
if (reference.tableId) {
const table = await this.tableRepository.getById(reference.tableId);
if (table) {
return table;
}
}
// If not found by ID and name is provided, try to find by name
if (reference.tableName) {
const tables = await this.tableRepository.list();
return tables.find(t => t.name === reference.tableName) ?? null;
}
return null;
}
/**
* Rolls on a referenced table multiple times and resolves any nested templates.
* @param table The table to roll on
* @param count Number of times to roll
* @param maxDepth Maximum resolution depth for nested templates
* @param visitedTables Set of table IDs and names that have been visited in this resolution chain
* @returns Array of roll result contents
*/
async rollOnReferencedTable(table, count, maxDepth, visitedTables = new Set()) {
const results = [];
for (let i = 0; i < count; i++) {
// Use the RNG to generate a random number between 0 and 1
const randomValue = this.rng.getRandomNumber(0, 1);
// Roll on the referenced table
const rollResult = table.roll(() => randomValue);
// If the result is also a template, create a RollTemplate and evaluate it
if (rollResult.isTemplate) {
const template = new RollTemplate(rollResult.content);
const evaluatedContent = await this.evaluateTemplate(template, maxDepth, visitedTables);
results.push(evaluatedContent);
}
else {
// Add the content as is
results.push(rollResult.content);
}
}
return results;
}
}
//# sourceMappingURL=evaluate-template-use-case.js.map