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

94
test.js
View file

@ -1,11 +1,26 @@
import { generateMap, MAP_W, MAP_H, indexOf } from "./mapGenerator.js";
import { CUSTOM_NAMES } from "./names.js";
import {
CUSTOM_NAMES,
FORCED_NAMES,
NAME_KANJI_POOLS,
NAME_PARTS,
NAME_PROBABILITIES,
NAME_TEMPLATES,
NAME_TEMPLATE_WEIGHTS,
generateTemplateName,
} from "./names.js";
import { adminBoundaryMetrics, majorCityCoreIntegrity } from "./qualityMetrics.js";
const result = document.getElementById("result");
const logLines = [];
let failed = 0;
const [namesSource, mapGeneratorSource, testSource] = await Promise.all([
fetch("./names.js").then((response) => response.text()),
fetch("./mapGenerator.js").then((response) => response.text()),
fetch("./test.js").then((response) => response.text()),
]);
function assert(condition, message) {
if (condition) logLines.push(`OK: ${message}`);
else {
@ -79,16 +94,26 @@ try {
const allNameable = map.entitiesForNames || [];
const uniqueNames = new Set(allNameable.map((item) => item.name));
const duplicateNameRatio = allNameable.length ? 1 - uniqueNames.size / allNameable.length : 0;
const coastalSuffixes = ["\u6d5c", "\u6e4a", "\u6e2f", "\u6d66", "\u6d25", "\u5d0e", "\u6e7e"];
const mountainSuffixes = ["\u8c37", "\u5ce0", "\u5c3e\u6839", "\u5c71", "\u6ca2", "\u9e93"];
const farInlandCoastalNames = allNameable.filter((p) => {
const i = indexOf(p.x, p.y);
return !p.portClass && (map.coastalLowland[i] || 0) < 0.12 && coastalSuffixes.some((suffix) => p.name?.endsWith(suffix));
}).length;
const flatCoastalMountainNames = allNameable.filter((p) => {
const i = indexOf(p.x, p.y);
return (map.coastalLowland[i] || 0) > 0.35 && (map.slope[i] || 0) < 0.18 && mountainSuffixes.some((suffix) => p.name?.endsWith(suffix));
}).length;
const activePoolChars = new Set(Object.values(NAME_KANJI_POOLS).flat().flatMap((part) => Array.from(String(part))));
const namedEntityCount = [
...map.villages,
...map.ports,
...map.crossings,
...map.passes,
...map.markets,
...map.castles,
...map.castleTowns,
...map.modernCities,
...map.stations,
...map.industrialZones,
...map.interchanges,
...map.logisticsParks,
...map.satelliteCities,
...map.newTowns,
...map.castleRuins,
...map.externalGateways,
...map.adminCenters,
].filter((item) => item?.id && item?.name).length;
const villageClusterMean = map.villages.length
? map.villages.reduce((sum, p) => sum + (map.settlementCluster?.[indexOf(p.x, p.y)] || 0), 0) / map.villages.length
: 0;
@ -119,6 +144,20 @@ try {
const adminMetrics = adminBoundaryMetrics(map);
const cityCoreIntegrity = majorCityCoreIntegrity(map);
assert(NAME_KANJI_POOLS && Array.isArray(NAME_KANJI_POOLS.modifiers), "NAME_KANJI_POOLS exists");
assert(NAME_TEMPLATES && NAME_TEMPLATES.modifierTerrain?.slots?.length === 2, "NAME_TEMPLATES exists");
assert(NAME_TEMPLATE_WEIGHTS && NAME_TEMPLATE_WEIGHTS.generic?.modifierTerrain > 0, "NAME_TEMPLATE_WEIGHTS exists");
assert(NAME_PROBABILITIES && NAME_PROBABILITIES.contextCategoryWeights?.generic, "NAME_PROBABILITIES exists");
const removedContextModule = "placeName" + "Context.js";
assert(!namesSource.includes(removedContextModule) && !mapGeneratorSource.includes(removedContextModule) && !testSource.includes(removedContextModule), "removed name-context import is absent");
assert(Object.keys(NAME_KANJI_POOLS).every((key) => Array.isArray(NAME_KANJI_POOLS[key])), "name category pools are centralized arrays");
assert(Object.values(NAME_KANJI_POOLS).every((pool) => pool.length === 0), "default name category pools are empty");
assert(Object.keys(NAME_PARTS).length === 0, "legacy NAME_PARTS has no hidden candidates");
const removedContextSuffixConst = "CONTEXT" + "_SUFFIXES";
const removedContextSuffixKey = "context" + "Suffixes";
assert(!namesSource.includes(removedContextSuffixConst) && !namesSource.includes(removedContextSuffixKey), "hidden context suffix arrays are absent");
assert(!/export\s+const\s+NAME_PROBABILITIES[\s\S]*export\s+const\s+NAME_PROBABILITIES/.test(namesSource), "NAME_PROBABILITIES has one source");
assert(map.elevation.length === size, "elevation length matches map size");
assert(map.sea.length === size, "sea length matches map size");
assert(map.river.length === size, "river length matches map size");
@ -204,17 +243,24 @@ try {
assert(capitalInside, "prefectural capital is inside the prefecture");
assert(maxModernEndpointDegree <= 10, "modern transport endpoints are not over-centralized");
assert(map.entitiesForNames.every((item) => item.id && item.name), "nameable entities have ids and names");
assert(map.entitiesForNames.every((item) => typeof item.name === "string"), "nameable entity names are strings");
assert(map.entitiesForNames.every((item) => !String(item.name).includes("\uFFFD")), "generated names contain no replacement characters");
assert(map.entitiesForNames.every((item) => Array.from(String(item.name)).length >= 2), "one-character generated names are prevented");
assert(map.entitiesForNames.every((item) => String(item.name).length > 0), "empty generated names are prevented");
assert(duplicateNameRatio < 0.18, "generated place-name duplicates stay low");
assert(farInlandCoastalNames <= Math.max(2, Math.ceil(allNameable.length * 0.12)), "coastal suffixes are not overused far inland");
assert(flatCoastalMountainNames <= Math.max(2, Math.ceil(allNameable.length * 0.10)), "mountain suffixes are not overused on flat coastal lowlands");
assert(map.nameDebug && Array.isArray(map.nameDebug.emptyPools), "nameDebug reports empty pools");
assert(map.nameDebug.emptyPools.length === Object.keys(NAME_KANJI_POOLS).length, "empty default pools are visible in nameDebug");
assert(map.nameDebug.selectedTemplateCounts && typeof map.nameDebug.selectedTemplateCounts === "object", "nameDebug selectedTemplateCounts exists");
assert(map.nameDebug.selectedContextCounts && typeof map.nameDebug.selectedContextCounts === "object", "nameDebug selectedContextCounts exists");
assert(
map.nameDebug.generatedNamesUsed + map.nameDebug.customNamesUsed + map.nameDebug.forcedNamesUsed + map.nameDebug.fallbackAttempts === namedEntityCount,
"nameDebug accounting covers named entities"
);
assert(generateTemplateName(777, "probe-0", { x: 10, y: 10, kind: "Probe" }, {}, 0, new Set()) === null, "empty pools do not use hidden fallback candidates");
assert(activePoolChars.size === 0, "no active pool characters exist until configured");
assert(villageClusterMean > 0.16, "villages prefer clustered valley, basin, coastal, and agricultural cells");
assert(saneEndpointRatio >= 0.76, "transport endpoints stay near meaningful generated nodes");
if (Object.keys(CUSTOM_NAMES).length > 0) {
assert(map.entitiesForNames.every((item) => !CUSTOM_NAMES[item.id] || item.name === CUSTOM_NAMES[item.id]), "CUSTOM_NAMES override generated names");
} else {
assert(true, "CUSTOM_NAMES override hook remains available");
}
assert(Object.keys(CUSTOM_NAMES).length === 0 || NAME_PROBABILITIES.customName < 1, "CUSTOM_NAMES are probabilistic by default");
assert(
map.adminCenters.length !== other.adminCenters.length ||
@ -230,6 +276,18 @@ try {
assert(JSON.stringify(againA.adminCenters.map((item) => [item.id, item.x, item.y, item.name])) === JSON.stringify(againB.adminCenters.map((item) => [item.id, item.x, item.y, item.name])), "municipal centers are deterministic for the same seed");
assert(JSON.stringify([...againA.adminId]) === JSON.stringify([...againB.adminId]), "municipal adminId snapping is deterministic for the same seed");
CUSTOM_NAMES["city-0"] = "C1";
const customSameA = generateMap(321);
const customSameB = generateMap(321);
const sameTargetA = customSameA.modernCities.find((item) => item.id === "city-0");
const sameTargetB = customSameB.modernCities.find((item) => item.id === "city-0");
const customSeedMaps = [301, 302, 303, 304, 305, 306, 307, 308].map((seedValue) => generateMap(seedValue));
const customTargets = customSeedMaps.map((seeded) => seeded.modernCities.find((item) => item.id === "city-0")).filter(Boolean);
const customHits = customTargets.filter((item) => item.name === "C1").length;
assert(sameTargetA?.name === sameTargetB?.name, "custom-name probability is deterministic for the same seed");
assert(!FORCED_NAMES["city-0"] && customTargets.length > 0 && customHits < customTargets.length, "CUSTOM_NAMES do not force every seed");
delete CUSTOM_NAMES["city-0"];
for (const seed of [101, 2026, 54321]) {
const seeded = generateMap(seed);
const metrics = adminBoundaryMetrics(seeded);