This commit is contained in:
33333-33333 2026-05-28 00:30:09 +09:00
commit b17be0e0d2
21 changed files with 8034 additions and 2737 deletions

View file

@ -1,24 +1,13 @@
import { MAP_H, MAP_W, hash2, indexOf, inside } from "./mapUtils.js";
import { MAP_H, MAP_W, hash2, indexOf, inside } from "./mapUtils.js";
export const CUSTOM_NAME_LIST = ["加茂", "春日", "新庄", "常盤", "追分", "清水", "曙", "暁", "旭", "鳥羽", "長門", "吉野", "吉田", "美郷", "美里", "美山", "八幡", "相生",];
function nameCharCount(value) {
return Array.from(String(value || "")).length;
}
function isAtomicNamePart(value) {
// The generator used to create visibly synthetic three-part names by joining
// a prefix with a compound terrain word. For template generation, keep each
// lexical slot atomic so a generated root is at most two visible elements.
return nameCharCount(value) <= 1;
}
export const CUSTOM_NAME_LIST = ["加茂", "瑞穂", "天神", "弁天", "千歳", "朝日", "春日", "新庄", "常盤", "追分", "清水", "曙", "暁", "旭", "鳥羽", "長門", "吉野", "吉田", "美郷", "美里", "美山"];
export const NAME_KANJI_POOLS = {
modifiers: [
"大", "小", "上", "下", "中", "奥", "脇",
"東", "西", "南", "北",
"新", "古", "本",
"高", "長", "広", "深", "浅", "明", "重", "荒",
"高", "長", "広", "深", "浅", "明", "重", "荒", "富",
"白", "黒", "青", "赤", "藍",
"奥", "前", "後", "内", "外",
"美", "吉", "福", "幸", "徳",
@ -26,7 +15,7 @@ export const NAME_KANJI_POOLS = {
"霞", "朝", "日", "天",
"土", "砂", "石", "岩",
"卯", "辰",
"駒", "羽", "鳴", "横", "永", "早", "清", "芳", "安", "泰", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠", "掘", "鎚", "綾", "槌", "徳", "栄"
"串","駒", "来", "武", "箱", "羽", "鳴", "横", "永", "早", "清", "芳", "安", "泰", "真", "丸", "平", "勝", "吹", "舞", "鎌", "釜", "笠", "掘", "鎚", "綾", "槌", "徳", "栄"
],
inlandTerrain: [
@ -37,7 +26,7 @@ export const NAME_KANJI_POOLS = {
"塚", "牧", "畑", "田", "森", "幡多", "幡", "畠", "秦", "植", "生",
"郷", "里",
"馬", "鹿", "亀", "鷲", "鷹", "鶴", "鴨", "竜", "龍", "牛", "鳥",
"湯", "宍",
"湯",
],
waterTerrain: [
@ -60,20 +49,20 @@ export const NAME_KANJI_POOLS = {
"松", "杉", "桜", "梅", "栗",
"竹", "楠", "藤", "萩", "葦",
"菅", "榎", "椿", "桐", "柳",
"橘", "柏", "槙", "柿", "桃", "稲", "花", "草", "菊",
"橘", "柏", "槙", "柿", "桃", "稲",
"梨", "桑", "麻", "芦", "茅", "根",
"粟", "稲", "稗", "米", "飯", "糠", "茜", "葵", "篠",
"粟", "稲", "稗", "米", "飯", "糠", "茜", "葵", "篠", "芝", "柴",
"榊", "楢", "檜", "椎", "茨", "葛", "蘆", "菖", "蒲", "蓮", "桑", "瓜"
],
postfixes: [
"田", "川", "山", "岡", "森", "林",
"島",
"島", "尻",
"江", "瀬", "井", "戸", "口",
"辺", "里", "郷", "村", "町",
"宿", "庄", "台", "坂", "橋", "明",
"本", "内", "窪", "平", "塚", "根",
"畑", "牧", "前", "見", "中", "羽", "生", "駒", "塚", "部", "栄", "永", "平",
"本", "内", "窪", "平", "塚", "根", "串",
"畑", "牧", "前", "見", "中", "羽", "生", "駒", "来", "富", "塚", "部", "栄", "永", "平",
],
archaicPrefixes: [
@ -88,7 +77,7 @@ export const NAME_KANJI_POOLS = {
"和", "輪",
"出", "播", "但",
"因", "伯", "筑", "肥", "豊",
"日", "紀", "志", "尾", "駿",
"日", "紀", "志", "尾",
"甲", "信", "越", "備", "能",
"薩", "隠", "美", "三", "若",
"遠", "近", "能", "加", "賀", "度", "飾",
@ -117,7 +106,7 @@ export const NAME_KANJI_POOLS = {
],
settlementWords: [
"里", "郷", "村", "町", "宿", "垣", "坪", "軒", "惣", "條",
"里", "郷", "村", "町", "宿", "垣", "坪", "軒", "惣",
"庄", "ノ庄", "之庄", "院", "宮", "ノ宮", "之宮", "寺", "社", "堂",
"城", "館", "屋", "家", "所",
"市", "場", "関", "地蔵", "辻", "角", "堰", "通", "路", "道", "筋",
@ -429,8 +418,7 @@ export function createNameDebug(probabilities = NAME_PROBABILITIES, pools = NAME
customNameListUsed: 0,
invalidNamesRejected: 0,
repeatedKanjiNamesRejected: 0,
oneCharacterNamesPrevented: 0,
rejectedOneCharacterNames: 0,
shortNamesRejected: 0,
duplicateRetries: 0,
fallbackAttempts: 0,
legacyFallbackUsed: 0,
@ -480,9 +468,8 @@ export function validateGeneratedName(name, options = {}) {
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 (Number.isFinite(options.minLength) && length < options.minLength) return { valid: false, reason: "tooShort" };
if (Number.isFinite(options.maxLength) && length > options.maxLength) return { valid: false, reason: "tooLong" };
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" };
}
@ -500,8 +487,6 @@ function generateTemplateNameDetails(seed, id, entity, fields, attempt, usedName
const context = chooseNameContext(entity, fields);
const templateKey = chooseTemplate(context, seed, id, attempt);
const template = NAME_TEMPLATES[templateKey];
// Do not generate old-style three-part random toponyms; names are now either
// curated list entries or at most two lexical elements plus any administrative suffix.
if (!template || (template.slots?.length || 0) > 2) return { name: null, context, templateKey: null };
const parts = [];
@ -509,15 +494,13 @@ function generateTemplateNameDetails(seed, id, entity, fields, attempt, usedName
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 atomicPool = slotPool.pool.filter(isAtomicNamePart);
if (!atomicPool.length) return { name: null, context, templateKey };
const part = pick(atomicPool, seed, id, attempt, 2503 + slotIndex * 127 + stableHash(slotPool.key));
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, { allowLong: false, maxLength: 2 });
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 };
@ -548,15 +531,10 @@ function tryCustomNameList(seed, id, usedNames, debug) {
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];
if (nameCharCount(customName) > 2) {
debug.invalidNamesRejected++;
continue;
}
const validation = validateGeneratedName(customName, { allowAsciiDiagnostic: true });
if (!validation.valid) {
if (validation.reason === "oneCharacter") {
debug.oneCharacterNamesPrevented++;
debug.rejectedOneCharacterNames++;
if (validation.reason === "tooShort") {
debug.shortNamesRejected++;
} else if (validation.reason === "repeatedKanji") {
debug.repeatedKanjiNamesRejected++;
} else {
@ -589,9 +567,8 @@ export function generateEntityName(seed, id, entity, fields, usedNames = null, d
continue;
}
if (result.invalidReason) {
if (result.invalidReason === "oneCharacter") {
debug.oneCharacterNamesPrevented++;
debug.rejectedOneCharacterNames++;
if (result.invalidReason === "tooShort") {
debug.shortNamesRejected++;
}
else if (result.invalidReason === "repeatedKanji") debug.repeatedKanjiNamesRejected++;
else debug.invalidNamesRejected++;