UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

142 lines (141 loc) 4.37 kB
import RNG from "./rng.js"; /** * @class (Markov process)-based string generator. * Copied from a <a href="http://roguebasin.com/index.php/Names_from_a_high_order_Markov_Process_and_a_simplified_Katz_back-off_scheme">RogueBasin article</a>. * Offers configurable order and prior. */ export default class StringGenerator { constructor(options) { this._options = { words: false, order: 3, prior: 0.001 }; Object.assign(this._options, options); this._boundary = String.fromCharCode(0); this._suffix = this._boundary; this._prefix = []; for (let i = 0; i < this._options.order; i++) { this._prefix.push(this._boundary); } this._priorValues = {}; this._priorValues[this._boundary] = this._options.prior; this._data = {}; } /** * Remove all learning data */ clear() { this._data = {}; this._priorValues = {}; } /** * @returns {string} Generated string */ generate() { let result = [this._sample(this._prefix)]; while (result[result.length - 1] != this._boundary) { result.push(this._sample(result)); } return this._join(result.slice(0, -1)); } /** * Observe (learn) a string from a training set */ observe(string) { let tokens = this._split(string); for (let i = 0; i < tokens.length; i++) { this._priorValues[tokens[i]] = this._options.prior; } tokens = this._prefix.concat(tokens).concat(this._suffix); /* add boundary symbols */ for (let i = this._options.order; i < tokens.length; i++) { let context = tokens.slice(i - this._options.order, i); let event = tokens[i]; for (let j = 0; j < context.length; j++) { let subcontext = context.slice(j); this._observeEvent(subcontext, event); } } } getStats() { let parts = []; let priorCount = Object.keys(this._priorValues).length; priorCount--; // boundary parts.push("distinct samples: " + priorCount); let dataCount = Object.keys(this._data).length; let eventCount = 0; for (let p in this._data) { eventCount += Object.keys(this._data[p]).length; } parts.push("dictionary size (contexts): " + dataCount); parts.push("dictionary size (events): " + eventCount); return parts.join(", "); } /** * @param {string} * @returns {string[]} */ _split(str) { return str.split(this._options.words ? /\s+/ : ""); } /** * @param {string[]} * @returns {string} */ _join(arr) { return arr.join(this._options.words ? " " : ""); } /** * @param {string[]} context * @param {string} event */ _observeEvent(context, event) { let key = this._join(context); if (!(key in this._data)) { this._data[key] = {}; } let data = this._data[key]; if (!(event in data)) { data[event] = 0; } data[event]++; } /** * @param {string[]} * @returns {string} */ _sample(context) { context = this._backoff(context); let key = this._join(context); let data = this._data[key]; let available = {}; if (this._options.prior) { for (let event in this._priorValues) { available[event] = this._priorValues[event]; } for (let event in data) { available[event] += data[event]; } } else { available = data; } return RNG.getWeightedValue(available); } /** * @param {string[]} * @returns {string[]} */ _backoff(context) { if (context.length > this._options.order) { context = context.slice(-this._options.order); } else if (context.length < this._options.order) { context = this._prefix.slice(0, this._options.order - context.length).concat(context); } while (!(this._join(context) in this._data) && context.length > 0) { context = context.slice(1); } return context; } }