export interface RollTableLimited { readonly full: false, readonly emoji: string, readonly title: string, readonly header: string, readonly ordinal: number, readonly results?: null, } export interface RollTableDetailsBase { readonly id: number, readonly identifier: string, readonly emoji: string, readonly name: string, readonly title: string, readonly header: string, readonly ordinal: number, } export type RollTable = RollTableLimited | RollTableDetails export type RollTableOrInput = RollTable | RollTableDetailsInput export function rollTableToString(v: RollTable) { if (v.full) { return `${v.header} (${v.id}/${v.identifier}/${v.name}/${v.emoji}/${v.title}/#${v.ordinal})${v.full === 'results' ? ` [${v.results.size} results]` : '' }` } else { return `${v.header} (???#${v.ordinal})` } } export function rollTableToStringShort(v: RollTable) { if (v.full) { return v.identifier } else { return v.header } } export const MAX_RESULT_LENGTH = 150; export const MAX_IDENTIFIER_LENGTH = 20; export const MAX_NAME_LENGTH = 50; export const MAX_URL_LENGTH = 100; export interface RollTableAuthor { readonly id: number; readonly name: string; readonly url: string | null; readonly relation: string; } export interface RollTableResultSet { readonly id: number; readonly name: string | null; readonly description: string | null; readonly global: boolean; } export interface RollTableResultLimited { readonly full: false, readonly text: string, readonly table: T, } export interface RollTableResultFull { readonly full: true, readonly textId: number, readonly mappingId: number, readonly table: T, readonly tableId?: never readonly text: string, readonly set: RollTableResultSet, readonly author: RollTableAuthor | null, readonly updated: Date, } export type RollTableResult = RollTableResultLimited | RollTableResultFull export type RollTableResultOrLookup = RollTableResultFull|RollTableResultLookup function setToString(v: RollTableResultSet): string { return `${v.global ? 'global' : 'local'} ${v.name ?? 'set'}` } function authorToString(v: RollTableAuthor): string { return `${v.relation} ${v.name} (${v.id})` } function resultToString(v: RollTableResult) { if (v.full) { return `${v.text} (${v.mappingId}: ${v.textId}/${rollTableToStringShort(v.table)}/${setToString(v.set)}/${v.author ? authorToString(v.author) : 'no author'})` } else { return `${v.text} (???: ${rollTableToStringShort(v.table)})` } } export interface RollTableResultLookup { readonly textId: number, readonly mappingId: number, readonly tableId: number, readonly table?: never, readonly text: string, readonly setId: number, readonly authorId: number | null, readonly updated: Date, } export interface RollTableDetailsInputResults extends RollTableDetailsBase { readonly results: Iterable|readonly [number, RollTableResultOrLookup]>; } function isResultArray(v: unknown): v is readonly [unknown, RollTableResultOrLookup] { return Array.isArray(v) && isRollTableResult(v[1]) } export interface RollTableDetailsInputNoResults extends RollTableDetailsBase { readonly results?: null } export type RollTableDetailsInput = RollTableDetailsInputResults | RollTableDetailsInputNoResults export type RollTableDetailsOrInput = RollTableDetails | RollTableDetailsInput export interface RollTableDetailsNoResults extends RollTableDetailsBase { readonly full: 'details' readonly results?: null; } export interface RollTableDetailsAndResults extends RollTableDetailsBase { readonly full: 'results' readonly results: ReadonlyMap>; } interface RollTableDetailsAndResultsInternal extends RollTableDetailsBase { readonly full: 'results' readonly results: Map>; } export type RollTableDetails = RollTableDetailsNoResults|RollTableDetailsAndResults function compareRollTables(a: RollTableOrInput, b: RollTableOrInput): number { return (a.ordinal - b.ordinal) || ("id" in a !== "id" in b ? "id" in a ? -1 : 1 : 0) || ("id" in a && "id" in b ? a.id - b.id : 0) || (a.header > b.header ? 1 : a.header < b.header ? -1 : 0); } function isRollTableResult(result: unknown): result is RollTableResult { return (typeof result === "object" && result !== null && 'table' in result && !('tableId' in result && typeof result.tableId !== 'undefined') && 'full' in result); } export class RollTableMap extends Map { [Symbol.iterator](): IterableIterator<[T extends RollTable ? number : (number|string), T]> { return this.entries(); } set(key: T extends RollTable ? number : (number|string), table: T): this set(table: T): this set(keyOrTable: (T extends RollTable ? number : (number|string))|T, table?: T): this { if (typeof keyOrTable === "object") { if ("id" in keyOrTable) { return super.set(keyOrTable.id, keyOrTable) } else { return super.set(keyOrTable.header as (T extends RollTable ? number : (number|string)), keyOrTable) } } else { return super.set(keyOrTable, table!) } } entries(): IterableIterator<[T extends RollTable ? number : (number|string), T]> { return Array.from(super.entries()).sort(([, a], [, b]) => compareRollTables(a, b))[Symbol.iterator](); } keys(): IterableIterator { return Array.from(this.entries()).map(([id]) => id)[Symbol.iterator](); } values(): IterableIterator { return Array.from(this.entries()).map(([, value]) => value)[Symbol.iterator](); } } export class RollTableDatabase implements Iterable { private readonly tablesById: RollTableMap = new RollTableMap(); private readonly setsById: Map = new Map(); private readonly authorsById: Map = new Map; private readonly mappingsByMappingId: Map> = new Map>(); private readonly mappingsByTextId: Map> = new Map>(); constructor({ tables = [], results = [], authors = [], sets = [] }: { tables?: Iterable, results?: Iterable | RollTableResultLookup>, authors?: Iterable, sets?: Iterable } = {}) { for (const table of tables) { this.addTable(table); } for (const author of authors) { this.addAuthor(author); } for (const set of sets) { this.addSet(set); } for (const result of results) { this.addResult(result); } } [Symbol.iterator](): IterableIterator { return this.tablesById.values(); } get tables(): ReadonlyMap { return this.tablesById; } get sets(): ReadonlyMap { return this.setsById; } get authors(): ReadonlyMap { return this.authorsById; } get mappings(): ReadonlyMap> { return this.mappingsByMappingId; } get results(): ReadonlyMap> { return this.mappingsByTextId; } addTable(table: RollTableDetailsInput): RollTableDetailsAndResults { return this.addTableInternal(table); } private addTableInternal(table: RollTableDetailsInput): RollTableDetailsAndResultsInternal { const existingTable = this.tablesById.get(table.id); if (existingTable) { if (table.results) { for (const result of table.results) { this.addResult(result); } } return existingTable; } const internalTable: RollTableDetailsAndResultsInternal = { ...table, full: 'results', results: new Map>() }; if (table.results) { for (const result of table.results) { this.addResult(result); } } this.tablesById.set(table.id, internalTable); return internalTable; } addAuthor(author: RollTableAuthor): RollTableAuthor { const existingAuthor = this.authorsById.get(author.id); if (existingAuthor) { return existingAuthor; } else { const result = { ...author }; this.authorsById.set(author.id, author); return result; } } addSet(set: RollTableResultSet): RollTableResultSet { const existingSet = this.setsById.get(set.id); if (existingSet) { return existingSet; } else { const result = { ...set }; this.setsById.set(set.id, set); return result; } } addResult(result: RollTableResultOrLookup|readonly [number, RollTableResultOrLookup]): RollTableResultFull { if (isResultArray(result)) { const [, innerResult] = result as [number, RollTableResultOrLookup]; return this.addResult(innerResult); } else if (isRollTableResult(result)) { const internalTable = this.tablesById.get(result.table.id) ?? this.addTableInternal({... result.table, results: null}); const internalAuthor = result.full && result.author ? (this.authorsById.get(result.author.id) ?? this.addAuthor(result.author)) : null; const internalSet = this.setsById.get(result.set.id) ?? this.addSet(result.set); const out: RollTableResultFull = { ...result, table: internalTable, author: internalAuthor, set: internalSet }; internalTable.results.set(out.textId, out); this.mappingsByTextId.set(out.textId, out); this.mappingsByMappingId.set(out.mappingId, out); return out; } else { const internalTable = this.tablesById.get(result.tableId); const internalAuthor = typeof result.authorId === 'number' ? this.authorsById.get(result.authorId) : null; const internalSet = this.setsById.get(result.setId); if (typeof internalTable === 'undefined') { throw Error(`no known table with ID ${result.tableId}`); } else if (typeof internalAuthor === 'undefined') { throw Error(`no known author with ID ${result.authorId}`); } else if (typeof internalSet === 'undefined') { throw Error(`no known set with ID ${result.setId}`); } const out: RollTableResultFull = { full: true, textId: result.textId, mappingId: result.mappingId, text: result.text, table: internalTable, author: internalAuthor, set: internalSet, updated: result.updated }; internalTable.results.set(out.textId, out); this.mappingsByTextId.set(out.textId, out); this.mappingsByMappingId.set(out.mappingId, out); return out; } } } function rollOn(table: RollTableDetailsAndResults): RollTableResult { const results = Array.from(table.results.values()); if (results.length === 0) { throw Error(`no results for table ${table.identifier}`); } return results[Math.floor(results.length * Math.random())]; } function rollOnAll(tables: Iterable): RolledValues { const result = new RolledValues(); for (const table of tables) { result.set(table, rollOn(table)); } return result; } function rerollOn(tables: Iterable, original: Iterable<[T, RollTableResult]>): RolledValues { const result = new RolledValues(); const tableSet = new Set(tables); for (const [table, originalValue] of original) { if (tableSet.has(table) && table.full === 'results') { const newValue = rollOn(table); result.set(table, newValue); } else { result.set(table, originalValue); } } return result; } export interface FinalGeneratedState { readonly final: true, readonly rolled: ReadonlyMap> } export interface InProgressGeneratedState { readonly final: false, readonly rolled: ReadonlyMap> readonly selected: ReadonlySet } export function generatedStateToString(contents: GeneratedState): string { if (contents.final) { return `Final state: ${Array.from(contents.rolled).map(([key, value]) => `${rollTableToString(key)} : ${resultToString(value)}`).join(" ::: ")}` } else { return `Current state: ${Array.from(contents.rolled).map(([key, value]) => `${rollTableToString(key)} : ${resultToString(value)}`).join(" ::: ")}. Selection: ${Array.from(contents.selected).map(v => `${rollTableToStringShort(v)}`).join(", ")}` } } export type GeneratedState = FinalGeneratedState | InProgressGeneratedState export interface FinalGeneratedContents { readonly final: true, readonly rolled: ReadonlyMap } export interface InProgressGeneratedContents { readonly final: false, readonly rolled: ReadonlyMap; readonly selected: ReadonlySet; } export type GeneratedContents = FinalGeneratedContents | InProgressGeneratedContents export function generatedContentsToString(contents: GeneratedContents): string { if (contents.final) { return `Final contents: ${Array.from(contents.rolled).map(([key, value]) => `${key} : ${value}`).join(" ::: ")}` } else { return `Current contents: ${Array.from(contents.rolled).map(([key, value]) => `${key} : ${value}`).join(" ::: ")}. Selection: ${Array.from(contents.selected).join(", ")}` } } export class RolledValues = RollTableResult> extends Map { [Symbol.iterator](): IterableIterator<[T, U]> { return this.entries(); } add(v: U): this { return this.set(v.table, v) } hasResult(v: U): boolean { return this.get(v.table) === v } entries(): IterableIterator<[T, U]> { return Array.from(super.entries()) .sort(([a], [b]) => compareRollTables(a, b))[Symbol.iterator](); } keys(): IterableIterator { return Array.from(this.entries()).map(([key]) => key)[Symbol.iterator](); } values(): IterableIterator { return Array.from(this.entries()).map(([, value]) => value)[Symbol.iterator](); } } export class RollSelections extends Set { [Symbol.iterator](): IterableIterator { return this.values(); } entries(): IterableIterator<[T, T]> { return Array.from(this.entries()).sort(([a], [b]) => compareRollTables(a, b))[Symbol.iterator](); } keys(): IterableIterator { return Array.from(this.entries()).map(([key]) => key)[Symbol.iterator](); } values(): IterableIterator { return super.values(); } }