This commit is contained in:
33333-33333 2026-05-26 00:45:01 +09:00
commit 5c82bfcab7
13 changed files with 1028 additions and 8801 deletions

110
names.js
View file

@ -4,24 +4,26 @@ export const NAME_KANJI_POOLS = {
modifiers: [
"大", "小", "上", "下", "中", "奥", "脇",
"東", "西", "南", "北",
"新", "古", "本", "元",
"新", "古", "本",
"高", "長", "広", "深", "浅",
"白", "黒", "青", "赤",
"白", "黒", "青", "赤", "藍",
"奥", "前", "後", "内", "外",
"美", "吉", "福", "幸", "徳",
"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "百", "千", "万",
"霧", "霞", "朝", "日", "天", "雨", "晴",
"早", "安", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠",
"霞", "朝", "日", "天", "晴",
"土", "砂", "石", "岩",
"丑", "卯", "辰", "巳", "酉",
"早", "安", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠", "掘", "鎚", "綾", "槌",
],
inlandTerrain: [
"山", "野", "荒", "野", "沢",
"森", "林", "岡", "丘", "坂",
"峰", "峠", "嶺", "尾", "平", "坪", "延", "燧",
"窪", "久", "迫", "久保", "玖保", "漥", "佐古", "作古", "峪",
"塚", "牧", "畑", "田", "森", "幡多", "幡", "畠", "秦",
"窪", "久", "迫", "久保", "玖保", "漥", "佐古", "作古",
"塚", "牧", "畑", "田", "森", "幡多", "幡", "畠", "秦", "植", "生",
"聡", "郷", "里",
"馬", "鹿", "亀", "鷲", "鷹",
"馬", "鹿", "亀", "鷲", "鷹", "鶴", "竜", "龍", "牛", "鳥",
"湯",
],
@ -36,10 +38,10 @@ export const NAME_KANJI_POOLS = {
coastalTerrain: [
"津", "浦", "津", "崎",
"島", "磯", "潟", "湊", "津",
"州", "洲", "瀬", "砂", "潮", "塩", "",
"州", "洲", "瀬", "砂", "潮", "塩", "",
"泊", "江", "浦", "灘", "入",
"戸", "門",
"鯵", "鰐", "漁", "魚"
"鯵", "鰐", "漁", "魚", "鮫", "鮎",
],
plants: [
@ -48,7 +50,7 @@ export const NAME_KANJI_POOLS = {
"菅", "榎", "椿", "桐", "柳",
"橘", "柏", "槙", "柿", "桃",
"梨", "桑", "麻", "芦", "茅",
"粟", "稲", "麦", "稗", "米", "飯", "糠",
"粟", "稲", "稗", "米", "飯", "糠",
"榊", "楢", "檜", "椎", "柚", "茨", "葛", "蘆", "菖", "蒲", "蓮", "桑"
],
@ -95,7 +97,7 @@ export const NAME_KANJI_POOLS = {
"陀", "芸", "雲",
"幡", "耆", "摩",
"張", "江", "河", "斐", "濃",
"岐", "防", "門", "隅", "向",
"岐", "門", "隅", "向",
"居", "前", "中", "後", "波",
"勢", "渡", "城", "紫", "野", "度",
"津", "島", "信", "登", "賀", "志",
@ -103,17 +105,16 @@ export const NAME_KANJI_POOLS = {
],
settlementWords: [
"里", "郷", "村", "町", "宿", "邑", "垣", "坪",
"里", "郷", "村", "町", "宿", "垣", "坪", "軒",
"庄", "ノ庄", "之庄", "院", "宮", "ノ宮", "之宮", "寺", "社", "堂",
"城", "館", "屋", "家", "所",
"市", "場", "府", "関", "地蔵", "辻", "角", "堰",
"市", "場", "関", "地蔵", "辻", "角", "堰",
"ヶ沢", "ヶ谷", "ヶ浜", "ヶ崎", "ヶ島", "ヶ浦", "ヶ津", "ヶ丘",
]
};
export const NAME_PROBABILITIES = {
customName: 0.20,
forcedName: 1.0,
customNameList: 0.25,
retryCount: 24,
categoryFallback: {
@ -281,8 +282,9 @@ export const NAME_TEMPLATE_WEIGHTS = {
},
};
export const CUSTOM_NAMES = {};
export const FORCED_NAMES = {};
// Add preferred reusable place names here. Each generated entity has a
// deterministic chance to use one before falling back to template kanji.
export const CUSTOM_NAME_LIST = [];
// Legacy export kept only so older imports do not fail.
export const NAME_PARTS = {};
@ -316,6 +318,20 @@ function countChars(value) {
return Array.from(String(value || "")).length;
}
function isKanjiChar(ch) {
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(ch);
}
function hasRepeatedKanji(value) {
const seen = new Set();
for (const ch of Array.from(String(value || ""))) {
if (!isKanjiChar(ch)) continue;
if (seen.has(ch)) return true;
seen.add(ch);
}
return false;
}
function incrementCounter(counter, key, amount = 1) {
counter[key] = (counter[key] || 0) + amount;
}
@ -397,15 +413,15 @@ export function createNameDebug(probabilities = NAME_PROBABILITIES, pools = NAME
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,
effectiveCustomNameListProbability: probabilities.customNameList,
poolsPresent,
emptyPools,
selectedTemplateCounts: {},
selectedContextCounts: {},
customNamesUsed: 0,
forcedNamesUsed: 0,
generatedNamesUsed: 0,
customNameListUsed: 0,
invalidNamesRejected: 0,
repeatedKanjiNamesRejected: 0,
oneCharacterNamesPrevented: 0,
rejectedOneCharacterNames: 0,
duplicateRetries: 0,
@ -459,6 +475,7 @@ export function validateGeneratedName(name, options = {}) {
}
if (!options.allowOneCharacter && length < 2) return { valid: false, reason: "oneCharacter" };
if (!options.allowLong && length > 4) return { valid: false, reason: "tooLong" };
if (!options.allowRepeatedKanji && hasRepeatedKanji(value)) return { valid: false, reason: "repeatedKanji" };
return { valid: true, reason: "valid" };
}
@ -512,35 +529,39 @@ function uniqueDiagnosticName(seed, id, usedNames, debug, startAttempt = 0) {
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++;
if (validation.reason === "oneCharacter") debug.rejectedOneCharacterNames++;
else debug.invalidNamesRejected++;
return null;
function tryCustomNameList(seed, id, usedNames, debug) {
if (!CUSTOM_NAME_LIST.length) return null;
if (roll(seed, id, 0, 3527) >= (NAME_PROBABILITIES.customNameList ?? 0)) return null;
const start = Math.floor(roll(seed, id, 0, 3539) * CUSTOM_NAME_LIST.length) % CUSTOM_NAME_LIST.length;
for (let offset = 0; offset < CUSTOM_NAME_LIST.length; offset++) {
const customName = CUSTOM_NAME_LIST[(start + offset) % CUSTOM_NAME_LIST.length];
const validation = validateGeneratedName(customName, { allowAsciiDiagnostic: true });
if (!validation.valid) {
if (validation.reason === "oneCharacter") {
debug.oneCharacterNamesPrevented++;
debug.rejectedOneCharacterNames++;
} else if (validation.reason === "repeatedKanji") {
debug.repeatedKanjiNamesRejected++;
} else {
debug.invalidNamesRejected++;
}
continue;
}
if (usedNames?.has(customName)) {
debug.duplicateRetries++;
continue;
}
debug.customNameListUsed++;
return customName;
}
if (usedNames?.has(customName)) {
debug.duplicateRetries++;
return null;
}
debug.customNamesUsed++;
return customName;
return null;
}
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 listedCustomName = tryCustomNameList(seed, id, usedNames, debug);
if (listedCustomName) return listedCustomName;
const retryCount = Math.max(1, NAME_PROBABILITIES.retryCount || 1);
for (let attempt = 0; attempt < retryCount; attempt++) {
@ -556,6 +577,7 @@ export function generateEntityName(seed, id, entity, fields, usedNames = null, d
debug.oneCharacterNamesPrevented++;
debug.rejectedOneCharacterNames++;
}
else if (result.invalidReason === "repeatedKanji") debug.repeatedKanjiNamesRejected++;
else debug.invalidNamesRejected++;
continue;
}