ttrpg-tools/src/components/utils/weighted-random.ts

89 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 解析整数范围字符串
* @param label 标签字符串,可能是单个整数(如 "1")或整数范围(如 "1-3"
* @returns 如果成功解析返回 [min, max],否则返回 null
*/
export function parseIntegerRange(label: string): [number, number] | null {
const trimmed = label.trim();
// 尝试匹配整数范围格式 (如 "1-3")
const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
if (rangeMatch) {
const min = parseInt(rangeMatch[1]!, 10);
const max = parseInt(rangeMatch[2]!, 10);
if (min <= max) {
return [min, max];
}
}
// 尝试匹配单个整数格式 (如 "5")
const intMatch = trimmed.match(/^(\d+)$/);
if (intMatch) {
const num = parseInt(intMatch[1]!, 10);
return [num, num];
}
return null;
}
/**
* 计算加权随机的总权重
* @param labels 标签数组
* @returns 总权重值
*/
export function calculateTotalWeight(labels: string[]): number {
let total = 0;
for (const label of labels) {
const range = parseIntegerRange(label);
if (range) {
const [min, max] = range;
total += max - min + 1;
}
}
return total;
}
/**
* 根据权重随机选择一个索引
* @param labels 标签数组
* @param totalWeight 总权重(可选,会自行计算)
* @returns 选中的索引,如果没有有效权重则返回 -1
*/
export function weightedRandomIndex(labels: string[], totalWeight?: number): number {
const weight = totalWeight ?? calculateTotalWeight(labels);
if (weight === 0) {
return -1;
}
// 生成一个随机权重值 (1 到 totalWeight)
const roll = Math.floor(Math.random() * weight) + 1;
let cumulative = 0;
for (let i = 0; i < labels.length; i++) {
const range = parseIntegerRange(labels[i]!);
if (range) {
const [min, max] = range;
const labelWeight = max - min + 1;
cumulative += labelWeight;
if (roll <= cumulative) {
return i;
}
}
}
// 理论上不会到这里,但为了安全返回最后一个
return labels.length - 1;
}
/**
* 检查所有 label 是否都是整数或整数范围格式
* @param labels 标签数组
* @returns 如果所有 label 都是整数/整数范围格式返回 true
*/
export function areAllLabelsNumeric(labels: string[]): boolean {
if (labels.length === 0) return false;
return labels.every(label => parseIntegerRange(label) !== null);
}