You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
temptress-bot/src/state/weightedText.ts

100 lines
3.5 KiB

export interface WeightedText {
readonly wt: number
readonly v: WeightedTextSelector
}
export type WeightedTextListItem = WeightedText | string | WeightedTextList
export type WeightedTextList = readonly WeightedTextListItem[]
export type WeightedTextSelector = string | WeightedTextList
export interface TextSource {
[key: string]: WeightedTextSelector
}
function isWeightedText(t: WeightedTextListItem): t is WeightedText {
return typeof t === 'object' && 'wt' in t
}
interface TotalWeightedText {
readonly minValue: number
readonly maxValue: number
readonly result: WeightedTextSelector
}
function totalWeightedText(minValue: number, t: WeightedTextListItem): TotalWeightedText {
if (isWeightedText(t)) {
return {
minValue,
maxValue: minValue + t.wt,
result: t.v,
}
}
return {
minValue,
maxValue: minValue + 1,
result: t,
}
}
export function selectAndFulfillWeightedText(
text: WeightedTextSelector,
data: TextSource,
random: () => number,
): string {
if (typeof text === 'string') {
if (textRequiresFulfillment(text)) {
return fulfillText(text, data, random)
} else {
return text
}
} else {
if (text.length === 0) {
return ''
} else if (text.length === 1) {
return selectAndFulfillWeightedText(isWeightedText(text[0]) ? text[0].v : text[0], data, random)
}
const totalWeightedTexts = text.reduce<readonly TotalWeightedText[]>((accumulated, next) => {
const weighted =
accumulated.length === 0
? totalWeightedText(0, next)
: totalWeightedText(accumulated[accumulated.length - 1].maxValue, next)
return [...accumulated, weighted]
}, [])
const maxValue = totalWeightedTexts[totalWeightedTexts.length - 1].maxValue
const selectedValue = random() * maxValue
const selectedItem =
totalWeightedTexts.find((value) => {
return value.minValue <= selectedValue && value.maxValue < selectedValue
})?.result ?? ''
return selectAndFulfillWeightedText(selectedItem, data, random)
}
}
const textReplacementRE = /\\(.)|\{\{\s*([A-Za-z0-9_]+)\s*}}/g
export function textRequiresFulfillment(text: string): boolean {
return textReplacementRE.test(text)
}
export function fulfillText(text: string, data: TextSource, random: () => number): string {
return text.replaceAll(
textReplacementRE,
(substring, escapedCharacter: string | undefined, fieldName: string | undefined): string => {
if (typeof escapedCharacter === 'string') {
if (escapedCharacter === '\\' || escapedCharacter === '{') {
return escapedCharacter
} else {
throw Error('unknown escape ' + substring)
}
}
if (typeof fieldName === 'string') {
const substitution = data[fieldName]
if (typeof substitution === 'undefined') {
throw Error('unknown variable or commonText ' + fieldName)
}
return selectAndFulfillWeightedText(substitution, data, random)
}
// We know this can't happen because one of the capturing groups always captures, but Typescript and Istanbul don't.
/* istanbul ignore next */ throw Error('unknown replacement ' + substring)
},
)
}