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.17 kB
JavaScript
import { RollTemplate } from '../domain/index.js';
/**
* Use case for rolling on a random table.
*/
export class RollOnTableUseCase {
/**
* Creates a new RollOnTableUseCase instance.
* @param repository The table repository to use.
* @param rng The random number generator to use.
*/
constructor(repository, rng) {
this.repository = repository;
this.rng = rng;
}
/**
* Executes the use case.
* @param tableId The ID of the table to roll on.
* @param count The number of rolls to perform (default: 1).
* @returns An array of roll results.
*/
async execute(tableId, count = 1) {
// Validate inputs
if (!tableId) {
throw new Error('Table ID is required');
}
if (count < 1) {
throw new Error('Count must be at least 1');
}
// Get the table from the repository
const table = await this.repository.getById(tableId);
if (!table) {
throw new Error(`Table with ID ${tableId} not found`);
}
// Perform the rolls
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 table using the random value
const result = table.roll(() => randomValue);
// If the result is a template, resolve it
if (result.isTemplate) {
const resolvedResult = await this.resolveTemplateResult(result);
results.push(resolvedResult);
}
else {
results.push(result);
}
}
return results;
}
/**
* Resolves a template result by rolling on referenced tables.
* @param result The roll result to resolve.
* @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 A resolved roll result.
*/
async resolveTemplateResult(result, maxDepth = RollTemplate.MAX_RESOLUTION_DEPTH, visitedTables = new Set()) {
// If not a template or max depth reached, return as is
if (!result.isTemplate || maxDepth <= 0) {
return result;
}
// Add current table to visited tables
visitedTables.add(result.tableId);
// Create a template from the content
let template = new RollTemplate(result.content);
// Extract all references from the template
const references = template.extractReferences();
// If no references, return the original result
if (references.length === 0) {
return result;
}
// Process each reference
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}]`;
template = template.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
template = template.replaceReference(reference, replacementValue);
}
// Check if there are still unresolved references and we haven't reached max depth
if (template.hasUnresolvedReferences() && maxDepth > 1) {
// Create a new result with the partially resolved template
const partialResult = result.withResolvedContent(template.toString());
// Resolve remaining references recursively
return this.resolveTemplateResult(partialResult, maxDepth - 1, visitedTables);
}
// Return the result with resolved content
return result.withResolvedContent(template.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.repository.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.repository.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, resolve it recursively
const resolvedRollResult = rollResult.isTemplate
? await this.resolveTemplateResult(rollResult, maxDepth, visitedTables)
: rollResult;
// Add the content (resolved if available, otherwise original)
results.push(resolvedRollResult.resolvedContent ?? resolvedRollResult.content);
}
return results;
}
}
//# sourceMappingURL=roll-on-table-use-case.js.map