Skip to content

Instantly share code, notes, and snippets.

@hasparus
Last active Apr 27, 2022
Embed
What would you like to do?
freebooter-generator/lib
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// generator.js
// written and released to the public domain by drow <drow@bin.sh>
// http://creativecommons.org/publicdomain/zero/1.0/
// Adapted and rewritten to TypeScript by hasparus <hasparus@gmail.com>
// - used PRNG instead of Math.random
// - added context and `ctx -> text` functions
import { random } from "./random";
export type GeneratedText = string;
export type TextLike = GeneratedText | ((context: Context) => GeneratedText);
export type Key = string;
/**
* @example "4-5", "6", "51-90"
*/
export type DiceRollRange = string;
export type Choices = Record<DiceRollRange, TextLike> | TextLike[];
export type RandomTables = Record<Key, Choices>;
type Context = Record<Key, GeneratedText>;
const randomTables: RandomTables = {};
/**
* register random tables
*/
export function addData(data: RandomTables) {
return Object.assign(randomTables, data);
}
/**
* generate text by type
*/
export function generateText(type: Key, context: Context = {}): GeneratedText {
const list = randomTables[type];
if (list) {
const selected = selectFrom(list);
if (selected) {
const text = typeof selected === "string" ? selected : selected(context);
context[type] = text;
return expandTokens(text, context);
}
}
return "[[ Error: Can't generate text for key: `" + type + "` ]]";
}
/**
* generate multiple texts by type
*/
export function generateMany(type: Key, n_of: number): GeneratedText[] {
const list = [];
let i;
for (i = 0; i < n_of; i++) {
list.push(generateText(type));
}
return list;
}
/**
* select from a many options
*/
function selectFrom(choices: Choices): TextLike {
if (Array.isArray(choices)) {
return selectFromArray(choices);
} else {
return selectFromTable(choices);
}
}
export function selectFromArray<T>(list: T[]): T {
return list[Math.floor(random() * list.length)];
}
export function selectFromTable(list: Record<Key, TextLike>) {
let len;
if ((len = scaleTable(list))) {
const idx = Math.floor(random() * len) + 1;
let key;
for (key in list) {
const r = keyRange(key);
if (idx >= r[0] && idx <= r[1]) {
return list[key];
}
}
}
return "";
}
function scaleTable(list: Record<Key, TextLike>) {
let len = 0;
let key;
for (key in list) {
const r = keyRange(key);
if (r[1] > len) {
len = r[1];
}
}
return len;
}
function keyRange(key: DiceRollRange): [number, number] {
let match;
if ((match = /(\d+)-00/.exec(key))) {
return [parseInt(match[1]), 100];
} else if ((match = /(\d+)-(\d+)/.exec(key))) {
return [parseInt(match[1]), parseInt(match[2])];
// eslint-disable-next-line eqeqeq
} else if (key == "00") {
return [100, 100];
} else {
return [parseInt(key), parseInt(key)];
}
}
/**
* expand {token} in string
*/
function expandTokens(string: GeneratedText, context: Context): GeneratedText {
let match;
while ((match = /{(\w+)}/.exec(string))) {
const token = match[1];
const replacement = generateText(token, context);
if (replacement) {
string = string.replace("{" + token + "}", replacement);
} else {
string = string.replace("{" + token + "}", token);
}
}
return string;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// name_generator.js
// written and released to the public domain by drow <drow@bin.sh>
// http://creativecommons.org/publicdomain/zero/1.0/
import { selectFromArray } from "./generator";
import { random } from "./random";
// Adapted and rewritten to TypeScript by hasparus <hasparus@gmail.com>
// - used PRNG instead of Math.random
// - using existing names from the list with probability 0.25
export type Language = string;
export type Name = string;
type Chain = Record<string, Record<string, number>>;
const name_set: Record<Language, Name[]> = {};
const chain_cache: Record<Language, Chain> = {};
export function addNames(language: string, names: string[]) {
name_set[language] = (name_set[language] || []).concat(names);
}
export function generateName(
language: Language,
mode: "only-existing" | "only-derived" | "both" = "both"
) {
switch (mode) {
case "only-existing":
return selectFromArray(name_set[language]);
case "only-derived":
return generateNameWithChain(language);
case "both":
return random() < 0.75
? generateNameWithChain(language)
: selectFromArray(name_set[language]);
}
}
/**
* generate name by type/language using a markov chain
*/
export function generateNameWithChain(language: Language) {
let chain: Chain | null;
if ((chain = markov_chain(language))) {
return markov_name(chain);
}
throw new Error("No names for language: " + language);
}
/**
* get markov chain by type
*/
function markov_chain(type: string) {
let chain;
if ((chain = chain_cache[type])) {
return chain;
} else {
let list;
if ((list = name_set[type]) && list.length) {
let chain;
if ((chain = construct_chain(list))) {
chain_cache[type] = chain;
return chain;
}
}
}
return null;
}
/**
* construct markov chain from list of names
*/
function construct_chain(list: string[]) {
let chain = {};
let i;
for (i = 0; i < list.length; i++) {
const names = list[i].split(/\s+/);
chain = incr_chain(chain, "parts", names.length);
let j;
for (j = 0; j < names.length; j++) {
const name = names[j];
chain = incr_chain(chain, "name_len", name.length);
const c = name.substr(0, 1);
chain = incr_chain(chain, "initial", c);
let string = name.substr(1);
let last_c = c;
while (string.length > 0) {
const c = string.substr(0, 1);
chain = incr_chain(chain, last_c, c);
string = string.substr(1);
last_c = c;
}
}
}
return scale_chain(chain);
}
function incr_chain(chain: Chain, key: string, token: number | string) {
if (chain[key]) {
if (chain[key][token]) {
chain[key][token]++;
} else {
chain[key][token] = 1;
}
} else {
chain[key] = {};
chain[key][token] = 1;
}
return chain;
}
function scale_chain(chain: Chain) {
const table_len: Record<string, number> = {};
Object.keys(chain).forEach((key) => {
table_len[key] = 0;
Object.keys(chain[key]).forEach((token) => {
const count = chain[key][token];
const weighted = Math.floor(Math.pow(count, 1.3));
chain[key][token] = weighted;
table_len[key] += weighted;
});
});
chain["table_len"] = table_len;
return chain;
}
/**
* construct name from markov chain
*/
function markov_name(chain: Chain) {
const parts = select_link(chain, "parts");
const names = [];
for (
let i = 0;
// @ts-ignore parts is string and i is number :>
i < parts;
i++
) {
const name_len = select_link(chain, "name_len") as any as number;
let c = select_link(chain, "initial");
let name = c;
let last_c = c;
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
while (name?.length! < name_len) {
c = select_link(chain, last_c!);
if (!c) break;
name += c;
last_c = c;
}
names.push(name);
}
return names.join(" ");
}
function select_link(chain: Chain, key: string) {
const len = chain["table_len"][key];
if (!len) return null;
const idx = Math.floor(random() * len);
const tokens = Object.keys(chain[key]);
let acc = 0;
let i;
for (i = 0; i < tokens.length; i++) {
const token = tokens[i];
acc += chain[key][token];
if (acc > idx) return token;
}
return null;
}
/**
* @file random number generator
* Based on https://stackoverflow.com/a/47593316/6003547
*/
let seeded: () => number = () => {
throw new Error(
"random generator was not seeded, please call `setSeed(str)`"
);
};
export function setSeed(str: string) {
const seed = xmur3(str);
seeded = sfc32(seed(), seed(), seed(), seed());
(seeded as any).seed = str;
}
export function random() {
return seeded();
}
type NumberOfDice = 1 | 2 | 3;
type DiceSize = 4 | 6 | 8 | 10 | 12 | 20 | 100;
export type Dice = `${NumberOfDice}d${DiceSize}`;
export function roll(dice: Dice) {
const [diceCount, diceSize] = dice.split("d").map(Number);
let result = 0;
for (let i = 0; i < diceCount; ++i) {
result += Math.floor(random() * diceSize) + 1;
}
return result;
}
/**
* Creates a seed function from string
*/
function xmur3(str: string) {
let i: number, h: number;
for (i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return function () {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
function sfc32(a: number, b: number, c: number, d: number) {
return function () {
a >>>= 0;
b >>>= 0;
c >>>= 0;
d >>>= 0;
let t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}
@hasparus
Copy link
Author

hasparus commented Apr 27, 2022

Original text generators by drow: https://donjon.bin.sh/code/random/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment