new name system?

This commit is contained in:
33333-33333 2026-05-21 02:55:58 +09:00
commit db8265384c
5 changed files with 525 additions and 232 deletions

621
names.js
View file

@ -1,67 +1,233 @@
import { MAP_H, MAP_W, indexOf, inside } from "./grid.js";
import { hash2 } from "./random.js";
// Default and custom place-name resources.
// Edit only this file to change generated names or override individual IDs.
export const NAME_PARTS = {
// May appear only at the beginning of a name.
prefixOnly: ["青", "白", "黒", "赤", "藍", "大", "小", "新", "古", "浅", "深", "上", "下", "東", "西", "南", "北", "中", "本", "広", "元", "奥", "前", "高", "長", "丸", "一", "壱", "二", "三", "四", "五", "六", "七", "八", "九", "十", "五十", "百", "千", "万", "阿", "佐々", "代々", "千代", "御", "宇", "須", "伊", "牟", "美",],
// Generic morphemes that may appear at the beginning or in the middle.
free: [
//nature
"川", "山", "花", "森", "松", "竹", "梅", "柳", "栃", "杉", "萩", "荻", "柿", "栗", "椎", "榊", "篠", "桑", "笹", "蓮", "橘", "柏", "樫", "麻", "桜", "岡", "木", "林", "森", "根", "葉", "葦", "日", "月", "君",
"野", "尾", "砂", "鮎", "鯉", "鯖", "雁", "菊", "錦", "稲", "稗", "朝", "霧", "坂", "葵", "茜", "犬", "馬", "熊", "猿", "鶴", "鹿", "鷲", "鷹", "亀", "龍", "竜", "燕", "鴨",
//objects
"畠", "坪", "室", "門", "脇", "末", "硯", "釜", "眞", "綾", "倉", "蔵", "鎌", "窯", "弓", "巻", "槙", "薪", "秦", "幡", "井", "真", "田", "井", "口", "関", "矢", "手", "戸", "庄", "村", "土", "鋸", "酒", "笙", "彦", "比", "碑", "多", "垣", "辻",
//others
"豊", "志", "野辺", "那", "納", "延", "芳", "越", "喜", "久", "方", "賀", "雅", "角", "刈", "神", "乃", "曲", "成", "幸", "徳", "甲", "由", "和", "清", "弘", "保", "徳", "福", "富", "住", "幸", "弥", "住", "生", "昌", "相", "香", "吉", "𠮷", "佐", "多", "保", "加", "間", "衣"
],
// May appear only at the end of a name.
suffixOnly: ["宿", "口", "摩", "磨", "茂", "平", "幡", "島", "豊", "川", "多", "田", "谷", "和", "屋"],
// Context-aware morphemes that may appear at the beginning or in the middle.
contextFree: {
coastal: ["浜", "浦", "津", "湊", "潮", "磯", "島", "磯", "渚", "潮", "汐", "淵", "渕", "泊", "葦", "芦"],
river: ["瀬", "川", "橋", "渡", "沢", "澤", "淵", "沼", "堀", "渕", "淵"],
plain: ["原", "野", "田", "畠", "里", "平", "牧", "塚"],
mountain: ["山", "谷", "沢", "峰", "尾", "鞍", "久保", "迫", "岳", "窪", "玖保", "久芳", "檜", "桧", "入", "奥"],
historic: ["城", "館", "宮", "寺", "社", "陣", "府", "町", "妙", "忌", "地蔵", "裏", "表"],
suburban: [],
},
// Context-aware morphemes that may appear only at the end of a name.
contextSuffixes: {
coastal: ["浜", "湊", "灘", "浦", "津", "崎", "嶋", "潟", "洲"],
river: ["川", "瀬", "橋", "渡", "沼", "沢", "滝", "瀧"],
plain: ["原", "野", "田", "里", "村", "平", "鼻", "栖"],
mountain: ["谷", "峡", "尾根", "山", "沢", "麓", "嶽", "隅", "陰", "奥"],
historic: ["城", "通", "館", "宮", "町", "寺", "院", "門", "柵", "稲荷"],
suburban: ["丘", "台", "野", "原", "新町", "ヶ丘"],
},
export const NAME_KANJI_POOLS = {
modifiers: [],
inlandTerrain: [],
waterTerrain: [],
coastalTerrain: [],
plants: [],
postfixes: [],
archaicPrefixes: [],
archaicSuffixes: [],
settlementWords: [],
};
export const NAME_PROBABILITIES = {
prefixOnly: 0.36,
contextFreeAsMain: 0.34,
additionalContextFree: 0.22,
secondFree: 0.18,
finalContextSuffix: 0.10,
finalSuffixOnly: 0.50,
finalFree: 0.20,
twoCharacter: 0.80,
customName: 0.20,
forcedName: 1.0,
retryCount: 24,
categoryFallback: {
modifiers: 1.0,
inlandTerrain: 1.0,
waterTerrain: 1.0,
coastalTerrain: 1.0,
plants: 1.0,
postfixes: 1.0,
archaicPrefixes: 1.0,
archaicSuffixes: 1.0,
settlementWords: 1.0,
},
contextCategoryWeights: {
generic: {
inlandTerrain: 1.0,
waterTerrain: 0.6,
coastalTerrain: 0.2,
plants: 0.8,
postfixes: 0.7,
archaic: 0.25,
settlementWords: 0.6,
},
coastal: {
inlandTerrain: 0.25,
waterTerrain: 0.8,
coastalTerrain: 1.0,
plants: 0.5,
postfixes: 0.7,
archaic: 0.2,
settlementWords: 0.6,
},
river: {
inlandTerrain: 0.5,
waterTerrain: 1.0,
coastalTerrain: 0.1,
plants: 0.6,
postfixes: 0.7,
archaic: 0.25,
settlementWords: 0.6,
},
plain: {
inlandTerrain: 1.0,
waterTerrain: 0.35,
coastalTerrain: 0.05,
plants: 0.9,
postfixes: 0.8,
archaic: 0.25,
settlementWords: 0.7,
},
mountain: {
inlandTerrain: 1.0,
waterTerrain: 0.45,
coastalTerrain: 0.02,
plants: 0.85,
postfixes: 0.55,
archaic: 0.35,
settlementWords: 0.45,
},
historic: {
inlandTerrain: 0.65,
waterTerrain: 0.45,
coastalTerrain: 0.25,
plants: 0.6,
postfixes: 0.9,
archaic: 0.75,
settlementWords: 0.9,
},
suburban: {
inlandTerrain: 0.85,
waterTerrain: 0.25,
coastalTerrain: 0.15,
plants: 0.55,
postfixes: 0.85,
archaic: 0.10,
settlementWords: 1.0,
},
},
};
export const CONTEXT_SUFFIXES = NAME_PARTS.contextSuffixes;
export const CUSTOM_NAMES = {
// "city-0": "Aohara",
// "port-0": "Shirahama",
// "castle-0": "Kurono",
export const NAME_TEMPLATES = {
modifierTerrain: {
slots: ["modifier", "terrain"],
baseWeight: 1.0,
},
plantTerrain: {
slots: ["plant", "terrain"],
baseWeight: 0.85,
},
terrainPostfix: {
slots: ["terrain", "postfix"],
baseWeight: 0.75,
},
archaicPair: {
slots: ["archaicPrefix", "archaicSuffix"],
baseWeight: 0.35,
},
archaicTerrain: {
slots: ["archaicPrefix", "terrain"],
baseWeight: 0.30,
},
modifierSettlement: {
slots: ["modifier", "settlement"],
baseWeight: 0.65,
},
};
export const NAME_TEMPLATE_WEIGHTS = {
generic: {
modifierTerrain: 1.0,
plantTerrain: 0.8,
terrainPostfix: 0.7,
archaicPair: 0.25,
archaicTerrain: 0.25,
modifierSettlement: 0.55,
},
coastal: {
modifierTerrain: 0.8,
plantTerrain: 0.55,
terrainPostfix: 0.9,
archaicPair: 0.2,
archaicTerrain: 0.25,
modifierSettlement: 0.55,
},
river: {
modifierTerrain: 0.9,
plantTerrain: 0.85,
terrainPostfix: 0.85,
archaicPair: 0.25,
archaicTerrain: 0.25,
modifierSettlement: 0.55,
},
plain: {
modifierTerrain: 1.0,
plantTerrain: 0.95,
terrainPostfix: 0.8,
archaicPair: 0.25,
archaicTerrain: 0.25,
modifierSettlement: 0.7,
},
mountain: {
modifierTerrain: 1.0,
plantTerrain: 0.9,
terrainPostfix: 0.5,
archaicPair: 0.35,
archaicTerrain: 0.45,
modifierSettlement: 0.35,
},
historic: {
modifierTerrain: 0.6,
plantTerrain: 0.45,
terrainPostfix: 0.75,
archaicPair: 0.8,
archaicTerrain: 0.55,
modifierSettlement: 0.95,
},
suburban: {
modifierTerrain: 0.85,
plantTerrain: 0.45,
terrainPostfix: 0.65,
archaicPair: 0.1,
archaicTerrain: 0.1,
modifierSettlement: 1.0,
},
};
export const CUSTOM_NAMES = {};
export const FORCED_NAMES = {};
// Legacy export kept only so older imports do not fail.
export const NAME_PARTS = {};
const POOL_KEYS = Object.keys(NAME_KANJI_POOLS);
const TERRAIN_POOL_KEYS = ["inlandTerrain", "waterTerrain", "coastalTerrain"];
const ASCII_DIAGNOSTIC_PREFIX = "N";
function stableHash(value) {
const text = String(value || "");
let h = 2166136261;
for (let i = 0; i < text.length; i++) {
h ^= text.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return h >>> 0;
}
function roll(seed, id, attempt, salt) {
const h = stableHash(id);
return hash2(h + attempt * 1009 + salt * 9173, seed + salt * 1013, (h ^ seed ^ salt) >>> 0);
}
function pick(pool, seed, id, attempt, salt) {
if (!pool?.length) return null;
const index = Math.floor(roll(seed, id, attempt, salt) * pool.length) % pool.length;
return pool[index] || null;
}
function countChars(value) {
return Array.from(String(value || "")).length;
}
function incrementCounter(counter, key, amount = 1) {
counter[key] = (counter[key] || 0) + amount;
}
function boundedIndex(entity) {
const x = Math.max(0, Math.min(MAP_W - 1, Math.round(entity?.x || 0)));
const y = Math.max(0, Math.min(MAP_H - 1, Math.round(entity?.y || 0)));
return indexOf(x, y);
}
function isNearSea(x, y, sea, radius = 3) {
if (!sea) return false;
for (let dy = -radius; dy <= radius; dy++) {
@ -74,157 +240,234 @@ function isNearSea(x, y, sea, radius = 3) {
return false;
}
function boundedIndex(entity) {
return indexOf(Math.max(0, Math.min(MAP_W - 1, entity.x)), Math.max(0, Math.min(MAP_H - 1, entity.y)));
function contextWeightsFor(context, probabilities = NAME_PROBABILITIES) {
return probabilities.contextCategoryWeights?.[context] || probabilities.contextCategoryWeights?.generic || {};
}
function chooseNameContext(entity, fields) {
const kind = String(entity.kind || "").toLowerCase();
const i = boundedIndex(entity);
const coastal = (fields?.coastalLowland?.[i] || 0) > 0.35 || isNearSea(entity.x, entity.y, fields?.sea);
const river = (fields?.river?.[i] || 0) > 0.35 || (fields?.valleyField?.[i] || 0) > 0.42;
const plain = (fields?.plain?.[i] || 0) > 0.48 || (fields?.agriculture?.[i] || 0) > 0.46 || (fields?.basinField?.[i] || 0) > 0.35;
const mountain = (fields?.elevation?.[i] || 0) > 0.52 || (fields?.slope?.[i] || 0) > 0.35 || (fields?.ridgeField?.[i] || 0) > 0.42;
function poolWeight(poolKey, context, probabilities = NAME_PROBABILITIES) {
const weights = contextWeightsFor(context, probabilities);
const fallback = probabilities.categoryFallback?.[poolKey] ?? 1.0;
if (poolKey === "archaicPrefixes" || poolKey === "archaicSuffixes") return (weights.archaic ?? 1.0) * fallback;
return (weights[poolKey] ?? 1.0) * fallback;
}
if (kind.includes("port") || kind.includes("harbor") || entity.portClass) return "coastal";
function slotPoolKeys(slot, context, probabilities = NAME_PROBABILITIES) {
if (slot === "terrain") {
return weightedKeyOrder(TERRAIN_POOL_KEYS, context, probabilities, 113);
}
const map = {
modifier: "modifiers",
plant: "plants",
postfix: "postfixes",
archaicPrefix: "archaicPrefixes",
archaicSuffix: "archaicSuffixes",
settlement: "settlementWords",
};
return map[slot] ? [map[slot]] : [];
}
function weightedKeyOrder(keys, context, probabilities, salt) {
return [...keys].sort((a, b) => {
const wa = poolWeight(a, context, probabilities);
const wb = poolWeight(b, context, probabilities);
if (wb !== wa) return wb - wa;
return stableHash(`${a}:${salt}`) - stableHash(`${b}:${salt}`);
});
}
function weightedChoice(entries, seed, id, attempt, salt) {
const active = entries.filter((entry) => entry.weight > 0);
const total = active.reduce((sum, entry) => sum + entry.weight, 0);
if (total <= 0) return null;
let target = roll(seed, id, attempt, salt) * total;
for (const entry of active) {
target -= entry.weight;
if (target <= 0) return entry.key;
}
return active[active.length - 1]?.key || null;
}
function pickWeightedPoolKey(keys, context, pools, probabilities, seed, id, attempt, salt) {
const entries = keys.map((key) => ({
key,
weight: poolWeight(key, context, probabilities) * (pools[key]?.length ? 1 : 0),
}));
return weightedChoice(entries, seed, id, attempt, salt);
}
export function createNameDebug(probabilities = NAME_PROBABILITIES, pools = NAME_KANJI_POOLS) {
const emptyPools = POOL_KEYS.filter((key) => !pools[key]?.length);
const poolsPresent = Object.fromEntries(POOL_KEYS.map((key) => [key, Boolean(pools[key]?.length)]));
return {
effectiveCustomNameProbability: probabilities.customName,
poolsPresent,
emptyPools,
selectedTemplateCounts: {},
selectedContextCounts: {},
customNamesUsed: 0,
forcedNamesUsed: 0,
generatedNamesUsed: 0,
invalidNamesRejected: 0,
oneCharacterNamesPrevented: 0,
duplicateRetries: 0,
fallbackAttempts: 0,
legacyFallbackUsed: 0,
};
}
export function chooseNameContext(entity, fields) {
const kind = String(entity?.kind || "").toLowerCase();
const i = boundedIndex(entity);
const coastal = (fields?.coastalLowland?.[i] || 0) > 0.35 || isNearSea(entity?.x || 0, entity?.y || 0, fields?.sea);
const river = (fields?.river?.[i] || 0) > 0.35 || (fields?.flowAccum?.[i] || 0) > 0.55 || (fields?.valleyField?.[i] || 0) > 0.42;
const plain = (fields?.plain?.[i] || 0) > 0.48 || (fields?.agriculture?.[i] || 0) > 0.46;
const mountain = (fields?.elevation?.[i] || 0) > 0.52 || (fields?.slope?.[i] || 0) > 0.35 || (fields?.ridgeField?.[i] || 0) > 0.42;
const urban = (fields?.populationDensity?.[i] || 0) > 0.35 || [2, 3, 4, 7, 8].includes(fields?.landuse?.[i]);
if (kind.includes("port") || kind.includes("harbor") || entity?.portClass) return "coastal";
if (kind.includes("crossing") || kind.includes("bridge")) return "river";
if (kind.includes("pass") || kind.includes("mountain")) return "mountain";
if (kind.includes("castle") || kind.includes("market")) return "historic";
if (kind.includes("new town") || kind.includes("satellite")) return "suburban";
if (kind.includes("newtown") || kind.includes("new town") || kind.includes("satellite") || kind.includes("station") || urban) return "suburban";
if (coastal && !mountain) return "coastal";
if (river) return "river";
if (mountain && !plain) return "mountain";
if (plain) return "plain";
return "plain";
return "generic";
}
export function chooseNameSuffixPool(entity, fields) {
const context = chooseNameContext(entity, fields);
return NAME_PARTS.contextSuffixes[context] || NAME_PARTS.contextSuffixes.plain;
}
function chooseContextFreePool(entity, fields, nameParts) {
const context = chooseNameContext(entity, fields);
return nameParts.contextFree?.[context] || nameParts.contextFree?.plain || nameParts.free || [""];
}
function pick(pool, seed, n, salt) {
if (!pool?.length) return "";
const index = Math.floor(hash2(n + salt * 101, seed + salt * 1009, seed + n * 37 + salt) * pool.length) % pool.length;
return pool[index];
}
function chance(seed, n, salt, probability) {
return hash2(n + salt * 173, seed + salt * 811, seed + n * 43 + salt) < probability;
}
function characterLength(value) {
return Array.from(String(value || "")).length;
}
function oneCharacterPool(pool, fallbackPool = pool) {
const filtered = (pool || []).filter((part) => characterLength(part) === 1);
return filtered.length ? filtered : (fallbackPool || [""]);
}
function appendPart(parts, part) {
if (!part) return;
const previous = parts[parts.length - 1] || "";
if (previous && (previous === part || previous.endsWith(part) || part.startsWith(previous))) return;
parts.push(part);
}
function appendFinalPart(parts, part, fallbackPool, seed, n, salt) {
let finalPart = part;
if (finalPart && parts.some((existing) => existing === finalPart || existing.endsWith(finalPart))) {
finalPart = pick(fallbackPool, seed, n, salt + 1);
export function resolveSlotPool(slot, context, pools = NAME_KANJI_POOLS, probabilities = NAME_PROBABILITIES, seed = 0, id = "", attempt = 0) {
const keys = slotPoolKeys(slot, context, probabilities);
if (!keys.length) return null;
const selectedKey = pickWeightedPoolKey(keys, context, pools, probabilities, seed, id, attempt, 1501 + stableHash(slot));
if (selectedKey) return { key: selectedKey, pool: pools[selectedKey] };
for (const key of weightedKeyOrder(keys, context, probabilities, 1559)) {
if (pools[key]?.length) return { key, pool: pools[key] };
}
appendPart(parts, finalPart);
return null;
}
function trimNameParts(parts) {
if (parts.length <= 3) return parts;
return [parts[0], ...parts.slice(-2)];
export function validateGeneratedName(name, options = {}) {
const value = String(name || "");
const length = countChars(value);
if (!value) return { valid: false, reason: "empty" };
if (value.includes("\uFFFD")) return { valid: false, reason: "replacement" };
if (!options.allowAsciiDiagnostic && value.startsWith(ASCII_DIAGNOSTIC_PREFIX) && /^N[0-9A-Z]+$/.test(value)) {
return { valid: false, reason: "asciiDiagnostic" };
}
if (!options.allowOneCharacter && length < 2) return { valid: false, reason: "oneCharacter" };
if (!options.allowLong && length > 4) return { valid: false, reason: "tooLong" };
return { valid: true, reason: "valid" };
}
function chooseTemplate(context, seed, id, attempt) {
const contextWeights = NAME_TEMPLATE_WEIGHTS[context] || NAME_TEMPLATE_WEIGHTS.generic;
const entries = Object.entries(NAME_TEMPLATES).map(([key, template]) => ({
key,
weight: (template.baseWeight ?? 1.0) * (contextWeights?.[key] ?? 0),
}));
return weightedChoice(entries, seed, id, attempt, 2003);
}
function generateTemplateNameDetails(seed, id, entity, fields, attempt, usedNames, pools = NAME_KANJI_POOLS, probabilities = NAME_PROBABILITIES) {
const context = chooseNameContext(entity, fields);
const templateKey = chooseTemplate(context, seed, id, attempt);
const template = NAME_TEMPLATES[templateKey];
if (!template) return { name: null, context, templateKey: null };
const parts = [];
for (let slotIndex = 0; slotIndex < template.slots.length; slotIndex++) {
const slot = template.slots[slotIndex];
const slotPool = resolveSlotPool(slot, context, pools, probabilities, seed, id, attempt + slotIndex);
if (!slotPool?.pool?.length) return { name: null, context, templateKey };
const part = pick(slotPool.pool, seed, id, attempt, 2503 + slotIndex * 127 + stableHash(slotPool.key));
if (!part) return { name: null, context, templateKey };
parts.push(part);
}
const name = parts.join("");
const validation = validateGeneratedName(name);
if (!validation.valid) return { name: null, context, templateKey, invalidReason: validation.reason };
if (usedNames?.has(name)) return { name: null, context, templateKey, duplicate: true };
return { name, context, templateKey };
}
export function generateTemplateName(seed, id, entity, fields, attempt = 0, usedNames = null) {
return generateTemplateNameDetails(seed, id, entity, fields, attempt, usedNames).name;
}
function deterministicDiagnosticName(seed, id, attempt = 0) {
const h = Math.floor(roll(seed, id, attempt, 3001) * 0xb640).toString(36).toUpperCase();
return `${ASCII_DIAGNOSTIC_PREFIX}${h.padStart(3, "0")}`;
}
function uniqueDiagnosticName(seed, id, usedNames, debug, startAttempt = 0) {
for (let offset = 0; offset < 128; offset++) {
const name = deterministicDiagnosticName(seed, id, startAttempt + offset);
if (!usedNames?.has(name)) return name;
debug.duplicateRetries++;
}
return `${ASCII_DIAGNOSTIC_PREFIX}${stableHash(`${seed}:${id}`).toString(36).toUpperCase()}`;
}
function tryCustomName(seed, id, usedNames, debug) {
const customName = CUSTOM_NAMES[id];
if (!customName) return null;
if (roll(seed, id, 0, 3501) >= NAME_PROBABILITIES.customName) return null;
const validation = validateGeneratedName(customName, { allowAsciiDiagnostic: true });
if (!validation.valid) {
if (validation.reason === "oneCharacter") debug.oneCharacterNamesPrevented++;
else debug.invalidNamesRejected++;
return null;
}
if (usedNames?.has(customName)) {
debug.duplicateRetries++;
return null;
}
debug.customNamesUsed++;
return customName;
}
export function generateEntityName(seed, id, entity, fields, usedNames = null, debug = createNameDebug()) {
debug ||= createNameDebug();
const forcedName = FORCED_NAMES[id];
if (forcedName) {
debug.forcedNamesUsed++;
return forcedName;
}
const customName = tryCustomName(seed, id, usedNames, debug);
if (customName) return customName;
const retryCount = Math.max(1, NAME_PROBABILITIES.retryCount || 1);
for (let attempt = 0; attempt < retryCount; attempt++) {
const result = generateTemplateNameDetails(seed, id, entity, fields, attempt, usedNames);
if (result.context) incrementCounter(debug.selectedContextCounts, result.context);
if (result.templateKey) incrementCounter(debug.selectedTemplateCounts, result.templateKey);
if (result.duplicate) {
debug.duplicateRetries++;
continue;
}
if (result.invalidReason) {
if (result.invalidReason === "oneCharacter") debug.oneCharacterNamesPrevented++;
else debug.invalidNamesRejected++;
continue;
}
if (result.name) {
debug.generatedNamesUsed++;
return result.name;
}
}
debug.fallbackAttempts++;
return uniqueDiagnosticName(seed, id, usedNames, debug, retryCount);
}
export function contextualDefaultName(seed, id, entity, nameParts = NAME_PARTS, fields, attempt = 0) {
const prefixKey = id.split("-")[0];
const n = Number(id.split("-")[1] || 0) + attempt * 997;
const prefixOnly = nameParts.prefixOnly || nameParts.prefixes || [""];
const free = nameParts.free || nameParts.infixes || [""];
const suffixOnly = nameParts.suffixOnly || nameParts.suffixes || [""];
const contextFree = chooseContextFreePool(entity, fields, nameParts);
const contextSuffixes = chooseNameSuffixPool(entity, fields);
const probabilities = nameParts.probabilities || NAME_PROBABILITIES;
if (chance(seed, n, 7 + prefixKey.length, probabilities.twoCharacter ?? 0.80)) {
const onePrefixOnly = oneCharacterPool(prefixOnly);
const oneFree = oneCharacterPool(free);
const oneSuffixOnly = oneCharacterPool(suffixOnly, oneFree);
const oneContextFree = oneCharacterPool(contextFree, oneFree);
const oneContextSuffixes = oneCharacterPool(contextSuffixes, oneSuffixOnly);
const usePrefix = chance(seed, n, 11 + prefixKey.length, probabilities.prefixOnly);
const useContextAsMain = chance(seed, n, 13 + prefixKey.length, probabilities.contextFreeAsMain ?? probabilities.contextFree ?? 0.34);
const firstPart = usePrefix
? pick(onePrefixOnly, seed, n, 101)
: pick(useContextAsMain ? oneContextFree : oneFree, seed, n, useContextAsMain ? 211 : 223);
const finalContextProbability = probabilities.finalContextSuffix ?? 0.10;
const finalSuffixProbability = probabilities.finalSuffixOnly ?? 0.50;
const finalFreeProbability = probabilities.finalFree ?? Math.max(0, 1 - finalContextProbability - finalSuffixProbability);
const finalTotal = Math.max(0.0001, finalContextProbability + finalSuffixProbability + finalFreeProbability);
const finalRoll = hash2(n + prefixKey.length * 23, seed + n * 37, seed + 7919);
const contextThreshold = finalContextProbability / finalTotal;
const suffixThreshold = (finalContextProbability + finalSuffixProbability) / finalTotal;
let finalPart;
if (finalRoll < contextThreshold) {
finalPart = pick(oneContextSuffixes, seed, n, 503);
} else if (finalRoll < suffixThreshold) {
finalPart = pick(oneSuffixOnly, seed, n, 509);
} else {
finalPart = pick(oneFree, seed, n, 521);
}
if (finalPart === firstPart) finalPart = pick(oneSuffixOnly, seed, n, 601);
if (finalPart === firstPart) finalPart = pick(oneFree, seed, n, 607);
return `${firstPart || pick(oneFree, seed, n, 613)}${finalPart || pick(oneSuffixOnly, seed, n, 617)}`;
}
const parts = [];
if (chance(seed, n, 11 + prefixKey.length, probabilities.prefixOnly)) {
appendPart(parts, pick(prefixOnly, seed, n, 101));
}
const useContextAsMain = chance(seed, n, 13 + prefixKey.length, probabilities.contextFreeAsMain ?? probabilities.contextFree ?? 0.34);
appendPart(parts, pick(useContextAsMain ? contextFree : free, seed, n, useContextAsMain ? 211 : 223));
if (!useContextAsMain && chance(seed, n, 17 + prefixKey.length, probabilities.additionalContextFree ?? probabilities.contextFree ?? 0.22)) {
appendPart(parts, pick(contextFree, seed, n, 307));
}
if (chance(seed, n, 19 + prefixKey.length, probabilities.secondFree)) {
appendPart(parts, pick(free, seed, n, 401));
}
const finalRoll = hash2(n + prefixKey.length * 23, seed + n * 37, seed + 7919);
let finalPart;
const finalContextProbability = probabilities.finalContextSuffix ?? 0.30;
const finalSuffixProbability = probabilities.finalSuffixOnly ?? 0.50;
const finalFreeProbability = probabilities.finalFree ?? Math.max(0, 1 - finalContextProbability - finalSuffixProbability);
const finalTotal = Math.max(0.0001, finalContextProbability + finalSuffixProbability + finalFreeProbability);
const contextThreshold = finalContextProbability / finalTotal;
const suffixThreshold = (finalContextProbability + finalSuffixProbability) / finalTotal;
if (finalRoll < contextThreshold) {
finalPart = pick(contextSuffixes, seed, n, 503);
} else if (finalRoll < suffixThreshold) {
finalPart = pick(suffixOnly, seed, n, 509);
} else {
finalPart = pick(free, seed, n, 521);
}
appendFinalPart(parts, finalPart, suffixOnly, seed, n, 601);
const name = trimNameParts(parts).join("");
return name || pick(suffixOnly, seed, n, 601) || "原";
const name = generateTemplateName(seed, id, entity, fields, attempt, null);
return name || deterministicDiagnosticName(seed, id, attempt);
}
export function chooseNameSuffixPool() {
return NAME_KANJI_POOLS.postfixes;
}