Last active
April 27, 2022 08:12
-
-
Save hasparus/3cf4ae48c3c8073e9ca4d7fbc74f2938 to your computer and use it in GitHub Desktop.
freebooter-generator/lib
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
// generator.js | |
// written and released to the public domain by drow <[email protected]> | |
// http://creativecommons.org/publicdomain/zero/1.0/ | |
// Adapted and rewritten to TypeScript by hasparus <[email protected]> | |
// - 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |
// name_generator.js | |
// written and released to the public domain by drow <[email protected]> | |
// http://creativecommons.org/publicdomain/zero/1.0/ | |
import { selectFromArray } from "./generator"; | |
import { random } from "./random"; | |
// Adapted and rewritten to TypeScript by hasparus <[email protected]> | |
// - 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Original text generators by drow: https://donjon.bin.sh/code/random/