This commit is contained in:
33333-33333 2026-05-20 22:29:14 +09:00
commit 53eef3522c
7 changed files with 40 additions and 182 deletions

View file

@ -25,7 +25,7 @@ function neighbors4(x, y) {
return out; return out;
} }
export function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, crestCrossingScore, populationDensity, landuse) { export function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse) {
const adminId = new Int16Array(SIZE); const adminId = new Int16Array(SIZE);
adminId.fill(-1); adminId.fill(-1);
const dist = new Float32Array(SIZE); const dist = new Float32Array(SIZE);
@ -55,16 +55,14 @@ export function generateAdminRegions(centers, prefectureMask, sea, elevation, sl
const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]); const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]);
const riverBarrier = Math.max(river[ni], river[curIndex]); const riverBarrier = Math.max(river[ni], river[curIndex]);
const highDivide = Math.max(elevation[ni], elevation[curIndex]); const highDivide = Math.max(elevation[ni], elevation[curIndex]);
const crest = Math.max(crestCrossingScore?.[ni] || 0, crestCrossingScore?.[curIndex] || 0);
const watershedBarrier = ridgeBarrier * (27.0 + Math.max(0, highDivide - 0.46) * 46.0); const watershedBarrier = ridgeBarrier * (27.0 + Math.max(0, highDivide - 0.46) * 46.0);
const crestPenalty = crest * (18.0 + Math.max(0, highDivide - 0.48) * 38.0) * (1 + Math.max(0, highDivide - 0.56) * 1.9);
const ridgePenalty = Math.max(0, highDivide - 0.36) * 16.0 + Math.abs(elevation[ni] - elevation[curIndex]) * 12.4 + watershedBarrier; const ridgePenalty = Math.max(0, highDivide - 0.36) * 16.0 + Math.abs(elevation[ni] - elevation[curIndex]) * 12.4 + watershedBarrier;
const slopePenalty = slope[ni] * 10.6; const slopePenalty = slope[ni] * 10.6;
const valleyBarrier = valleyField[ni] > 0.50 ? valleyField[ni] * (riverBarrier > 0.16 ? 7.2 : 2.6) : 0; const valleyBarrier = valleyField[ni] > 0.50 ? valleyField[ni] * (riverBarrier > 0.16 ? 7.2 : 2.6) : 0;
const riverPenalty = riverBarrier > 0.7 ? 22.0 : riverBarrier > 0.42 ? 14.8 : riverBarrier > 0.22 ? 7.4 : riverBarrier > 0.12 ? 2.2 : 0; const riverPenalty = riverBarrier > 0.7 ? 22.0 : riverBarrier > 0.42 ? 14.8 : riverBarrier > 0.22 ? 7.4 : riverBarrier > 0.12 ? 2.2 : 0;
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0; const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0;
const valleyLocalityBonus = valleyField[ni] * 0.16; const valleyLocalityBonus = valleyField[ni] * 0.16;
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + crestPenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step; const stepCost = Math.max(0.25, 0.72 + ridgePenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step;
const nextDist = dist[curIndex] + stepCost; const nextDist = dist[curIndex] + stepCost;
if (nextDist < dist[ni]) { if (nextDist < dist[ni]) {
@ -268,16 +266,15 @@ export function removeMunicipalExclaves(adminId, prefectureMask, sea, adminCente
} }
} }
export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse) { export function terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse) {
const urbanPenalty = urbanBoundaryPenalty(i, populationDensity, landuse); const urbanPenalty = urbanBoundaryPenalty(i, populationDensity, landuse);
const crest = crestCrossingScore?.[i] || 0;
const majorRiver = clamp(Math.max(river[i] - 0.32, 0) * 1.9 + Math.max(flowAccum[i] - 0.38, 0) * 0.75); const majorRiver = clamp(Math.max(river[i] - 0.32, 0) * 1.9 + Math.max(flowAccum[i] - 0.38, 0) * 0.75);
const minorStream = clamp(river[i] * 0.34 + flowAccum[i] * 0.18); const minorStream = clamp(river[i] * 0.34 + flowAccum[i] * 0.18);
const ridgeDivide = clamp(ridgeField[i] * 1.55 + Math.max(0, elevation[i] - 0.54) * ridgeField[i] * 0.95); const ridgeDivide = clamp(ridgeField[i] * 1.55 + Math.max(0, elevation[i] - 0.54) * ridgeField[i] * 0.95);
const slopeBreak = clamp(slope[i] * 0.58 + Math.max(0, slope[i] - 0.32) * 0.68); const slopeBreak = clamp(slope[i] * 0.58 + Math.max(0, slope[i] - 0.32) * 0.68);
const highGround = Math.max(0, elevation[i] - 0.56) * 0.22; const highGround = Math.max(0, elevation[i] - 0.56) * 0.22;
const valleyFloorPenalty = valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62); const valleyFloorPenalty = valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62);
return clamp(ridgeDivide + crest * 1.38 + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72); return clamp(ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72);
} }
function urbanBoundaryPenalty(i, populationDensity, landuse) { function urbanBoundaryPenalty(i, populationDensity, landuse) {
@ -347,7 +344,7 @@ function buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters = []
function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, populationDensity, landuse, river, valleyField) { function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, populationDensity, landuse, river, valleyField) {
const [x, y] = xyOf(i); const [x, y] = xyOf(i);
const oldId = labels[i]; const oldId = labels[i];
let energy = (centerDist[candidateId]?.[i] ?? 0) * 0.035; let energy = centerDist[candidateId]?.[i] ?? 0;
let same4 = 0; let same4 = 0;
let diff4 = 0; let diff4 = 0;
let diagDiff = 0; let diagDiff = 0;
@ -378,7 +375,7 @@ function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, po
if (diagDiff >= 3 && diff4 >= 2 && targetScore[i] < 0.50) energy += 0.42; if (diagDiff >= 3 && diff4 >= 2 && targetScore[i] < 0.50) energy += 0.42;
if (candidateId !== oldId && centerDist[candidateId] && centerDist[oldId]) { if (candidateId !== oldId && centerDist[candidateId] && centerDist[oldId]) {
const drift = centerDist[candidateId][i] - centerDist[oldId][i]; const drift = centerDist[candidateId][i] - centerDist[oldId][i];
if (drift > 0) energy += Math.min(0.55, drift * 0.006); if (drift > 0) energy += Math.min(0.9, drift * 0.012);
} }
return energy; return energy;
} }
@ -470,7 +467,7 @@ function classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyFie
return 11; return 11;
} }
function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse) { function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse) {
const unitId = new Int32Array(SIZE); const unitId = new Int32Array(SIZE);
unitId.fill(-1); unitId.fill(-1);
const cellClass = new Int16Array(SIZE); const cellClass = new Int16Array(SIZE);
@ -507,7 +504,7 @@ function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridge
} }
const targetScore = new Float32Array(SIZE); const targetScore = new Float32Array(SIZE);
for (let i = 0; i < SIZE; i++) if (cellClass[i] >= 0) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse); for (let i = 0; i < SIZE; i++) if (cellClass[i] >= 0) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse);
rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea); rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea);
mergeTinyLandscapeUnits(unitId, units, 10); mergeTinyLandscapeUnits(unitId, units, 10);
rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea); rebuildLandscapeUnitAdjacency(unitId, units, targetScore, prefectureMask, sea);
@ -572,8 +569,8 @@ function landscapeTransitionCost(a, b, edge) {
return Math.max(0.18, 0.70 + boundaryTarget * 4.2 + (a.classId === b.classId ? 0 : 0.75) + ((a.classId === 8 || b.classId === 8) ? 1.2 : 0) - urbanContinuity - (bothCorridor ? 0.55 : 0)); return Math.max(0.18, 0.70 + boundaryTarget * 4.2 + (a.classId === b.classId ? 0 : 0.75) + ((a.classId === 8 || b.classId === 8) ? 1.2 : 0) - urbanContinuity - (bothCorridor ? 0.55 : 0));
} }
export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse, adminCenters = []) { export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = []) {
const { unitId, units, targetScore } = buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse); const { unitId, units, targetScore } = buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse);
if (units.length === 0) return; if (units.length === 0) return;
for (let id = 0; id < adminCenters.length; id++) { for (let id = 0; id < adminCenters.length; id++) {
const center = adminCenters[id]; const center = adminCenters[id];
@ -624,11 +621,11 @@ export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, e
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse); repairAdminTopology(adminId, prefectureMask, sea, adminCenters, targetScore, populationDensity, landuse);
} }
export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse, adminCenters = [], protectedPoints = [], passes = 6) { export function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse, adminCenters = [], protectedPoints = [], passes = 6) {
const targetScore = new Float32Array(SIZE); const targetScore = new Float32Array(SIZE);
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, crestCrossingScore, populationDensity, landuse); for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) targetScore[i] = terrainBoundaryTargetScore(i, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse);
const protectedMask = buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters, protectedPoints, populationDensity, landuse); const protectedMask = buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters, protectedPoints, populationDensity, landuse);
const band = buildBoundaryBand(adminId, prefectureMask, sea, 8); const band = buildBoundaryBand(adminId, prefectureMask, sea, 5);
const adminIds = [...new Set([...adminId].filter((id) => id >= 0))]; const adminIds = [...new Set([...adminId].filter((id) => id >= 0))];
const centerDist = buildCenterDistanceFields(adminIds, adminCenters, prefectureMask, sea); const centerDist = buildCenterDistanceFields(adminIds, adminCenters, prefectureMask, sea);
let current = new Int16Array(adminId); let current = new Int16Array(adminId);

View file

@ -18,7 +18,6 @@ export function createMapFields() {
basinField: new Float32Array(SIZE), basinField: new Float32Array(SIZE),
coastalLowland: new Float32Array(SIZE), coastalLowland: new Float32Array(SIZE),
flowAccum: new Float32Array(SIZE), flowAccum: new Float32Array(SIZE),
crestCrossingScore: new Float32Array(SIZE),
erosionField: new Float32Array(SIZE), erosionField: new Float32Array(SIZE),
depositionField: new Float32Array(SIZE), depositionField: new Float32Array(SIZE),
flowTo, flowTo,

View file

@ -1,4 +1,4 @@
import { CUSTOM_NAMES, NAME_PARTS } from "./names.js"; import { CUSTOM_NAMES, NAME_PARTS, contextualDefaultName } from "./names.js";
import { import {
applyLandscapeUnitAdminPartition, applyLandscapeUnitAdminPartition,
generateAdminRegions, generateAdminRegions,
@ -569,25 +569,14 @@ function attachIdsAndNames(points, prefix, seed, kindOverride = null, nameFields
return points.map((p, i) => { return points.map((p, i) => {
const id = `${prefix}-${i}`; const id = `${prefix}-${i}`;
const kind = kindOverride || p.kind; const kind = kindOverride || p.kind;
let name = CUSTOM_NAMES[id];
const customName = CUSTOM_NAMES[id];
const customProbability = NAME_PROBABILITIES?.customName ?? 1.0;
const useCustomName =
Boolean(customName) &&
hash2(i + prefix.length * 101, seed + i * 37, seed + 99991) < customProbability;
let name = useCustomName ? customName : null;
if (!name) { if (!name) {
for (let attempt = 0; attempt < 8; attempt++) { for (let attempt = 0; attempt < 8; attempt++) {
name = defaultName(seed + prefix.length * 1000, id, { ...p, kind }, nameFields, attempt); name = defaultName(seed + prefix.length * 1000, id, { ...p, kind }, nameFields, attempt);
if (!usedNames || !usedNames.has(name)) break; if (!usedNames || !usedNames.has(name)) break;
} }
} }
if (usedNames) usedNames.add(name);
if (usedNames && name) usedNames.add(name);
return { return {
...p, ...p,
id, id,

View file

@ -6,34 +6,44 @@ import { hash2 } from "./random.js";
export const NAME_PARTS = { export const NAME_PARTS = {
// May appear only at the beginning of a name. // May appear only at the beginning of a name.
prefixOnly: ["青", "白", "黒", "赤", "浅", "深", "大", "小", "新", "古", "上", "下", "東", "西", "南", "北", "中", "本", "元", "奥", "遠", "近", "前", "高", "佐々", "長", "丸", "伊", "阿", "千代", "茨", "美", "宇", "徳", "広", "", "牟", "一", "壱", "二", "三", "四", "五", "六", "七", "八", "九", "十", "五十", "百", "千", "万"], prefixOnly: ["大", "小", "新", "古", "浅", "深", "上", "下", "東", "西", "南", "北", "中", "本", "広", "元", "奥", "前", "高", "長", "丸", "一", "壱", "二", "三", "四", "五", "六", "七", "八", "九", "十", "五十", "百", "千", "万", "阿", "佐々", "代々", "千代", "宇", "伊", "牟", "美",],
// Generic morphemes that may appear at the beginning or in the middle. // Generic morphemes that may appear at the beginning or in the middle.
free: [ free: [
"前", "内", "母", "父", //pattern
"川", "山", "森", "松", "原", "黍", "栃", "梨", "柏", "桜", "杉", "柳", "葛", "竹", "梅", "藤", "篠", "米", "馬", "石", "岩", "鷲", "鳥", "鷹", "犬", "猿", "鹿", "熊", "羽", "木", "林", "根", "葉", "葦", "岐","日", "月", "島", "畠", "野", "田", "青", "白", "黒", "赤", "藍",
"井", "守", "盛", "幡", "河内", "敷", "鍋", "凪", "太", "嘉", "倉", "昌", "芸", "相", "生", "折", "摂", "香", "佳", "加", "笠", "行", "油", "柚", "関", "伯", "桶", "久", "代", "旭", "手", "蓑", "仙", "豊", "飾", "戸", "矢", "玉", "須", "津", "佐", "志", "布", "蘇", "曾", "庄", "甲", "尾", "朝", "比古", "目", "賀", "彦", "姫", "輪", "和", "福", "岡", "富", "住", "由", "君", "飯", "那", "奈", "肥", "越", "曲輪", "車", "渋", "幸",
//nature
"川", "山", "森", "松", "竹", "梅", "柳", "栃", "橘", "柏", "樫", "麻", "桜", "岡", "木", "林", "森", "根", "葉", "葦", "日", "月", "君",
"野", "尾", "旭", "朝", "霧",
//objects
"畠", "秦", "幡", "井", "真", "田", "井", "口", "関", "矢", "手", "戸", "庄", "屋", "土", "鋸", "琴", "笙", "彦", "比", "碑", "多", "", "",
//others
"由", "志", "成", "幸", "徳", "甲", "由", "和", "清", "弘", "保", "徳", "福", "富", "住", "幸", "弥", "住", "生", "昌", "相", "香", "吉", "𠮷", "佐", "多", "保", "加"
], ],
// May appear only at the end of a name. // May appear only at the end of a name.
suffixOnly: ["宿", "郷", "里", "新田", "磨", "摩", "原", "口", "泉"], suffixOnly: ["宿", "口", "摩", "磨", "潟", "茂"],
// Context-aware morphemes that may appear at the beginning or in the middle. // Context-aware morphemes that may appear at the beginning or in the middle.
contextFree: { contextFree: {
coastal: ["浜", "浦", "津", "湊", "潮", "磯", "島", "潮", "塩", "汐", "波", "江"], coastal: ["浜", "浦", "津", "湊", "潮", "磯", "海", "島", "磯", "渚", "潮", "汐"],
river: ["瀬", "川", "橋", "渡", "沢", "淵", "沼", "堀"], river: ["瀬", "川", "橋", "渡", "沢", "淵", "沼", "堀"],
plain: ["原", "野", "田", "畠", "里", "平", "牧", "塚"], plain: ["原", "野", "田", "畠", "里", "平", "牧", "塚"],
mountain: ["山", "谷", "沢", "峰", "尾", "峠", "湯", "磐", "迫","久保","窪","玖保","硲"], mountain: ["山", "谷", "沢", "峰", "尾", "久保", "迫", "岳"],
historic: ["館", "宮", "寺", "社", "陣", "府", "町"], historic: ["城", "館", "宮", "寺", "社", "陣", "府", "町"],
suburban: [],
}, },
// Context-aware morphemes that may appear only at the end of a name. // Context-aware morphemes that may appear only at the end of a name.
contextSuffixes: { contextSuffixes: {
coastal: ["浜", "湊", "灘", "浦", "津", "崎"], coastal: ["浜", "湊", "灘", "浦", "津", "崎", "嶋"],
river: ["川", "瀬", "橋", "渡", "沼", "沢"], river: ["川", "瀬", "橋", "渡", "沼", "沢"],
plain: ["原", "野", "田", "里", "村", "平"], plain: ["原", "野", "田", "里", "村", "平"],
mountain: ["谷", "峡", "峰", "山", "沢", "岳"], mountain: ["谷", "峡", "尾根", "山", "沢", "麓"],
historic: ["城", "通", "館", "宮", "町"], historic: ["城", "通", "館", "宮", "町"],
suburban: ["丘", "台", "野", "原", "新町", "ヶ丘"],
}, },
}; };
@ -43,10 +53,9 @@ export const NAME_PROBABILITIES = {
additionalContextFree: 0.22, additionalContextFree: 0.22,
secondFree: 0.18, secondFree: 0.18,
finalContextSuffix: 0.10, finalContextSuffix: 0.10,
finalSuffixOnly: 0.10, finalSuffixOnly: 0.50,
finalFree: 0.20, finalFree: 0.20,
twoCharacter: 0.90, twoCharacter: 0.80,
customName: 0.20,
}; };
export const CONTEXT_SUFFIXES = NAME_PARTS.contextSuffixes; export const CONTEXT_SUFFIXES = NAME_PARTS.contextSuffixes;
@ -55,26 +64,6 @@ export const CUSTOM_NAMES = {
// "city-0": "Aohara", // "city-0": "Aohara",
// "port-0": "Shirahama", // "port-0": "Shirahama",
// "castle-0": "Kurono", // "castle-0": "Kurono",
"city-0": "加茂",
"city-1": "高砂",
"city-2": "八幡",
"city-3": "国府",
"city-4": "春日",
"city-5": "御幸",
"city-6": "住吉",
"city-7": "日向",
"city-8": "加茂",
"city-9": "加茂",
"city-10": "飛鳥",
"city-11": "諏訪",
"city-12": "相模",
"city-13": "鹿島",
"city-14": "香取",
"city-15": "住吉",
"city-16": "国文",
"city-17": "愛宕",
"city-18": "安曇",
"castle-0": "佐古",
}; };
function isNearSea(x, y, sea, radius = 3) { function isNearSea(x, y, sea, radius = 3) {

View file

@ -1,107 +0,0 @@
import { MAP_H, MAP_W, indexOf, inside } from "./grid.js";
import { hash2 } from "./random.js";
export const NAME_PROBABILITIES = {
contextSuffix: 0.42,
secondInfix: 0.22,
prefixOnly: 0.38,
customName: 1.0,
};
const CONTEXT_SUFFIXES = {
coastal: ["\u6d5c", "\u6e4a", "\u6e2f", "\u6d66", "\u6d25", "\u5d0e", "\u6e7e"],
river: ["\u5ddd", "\u702c", "\u6a4b", "\u6e21", "\u6cbc", "\u6ca2"],
plain: ["\u539f", "\u91ce", "\u7530", "\u91cc", "\u6751", "\u5e73"],
mountain: ["\u8c37", "\u5ce0", "\u5c3e\u6839", "\u5c71", "\u6ca2", "\u9e93"],
historic: ["\u57ce", "\u5e9c", "\u9928", "\u5bae", "\u753a"],
suburban: ["\u4e18", "\u53f0", "\u91ce", "\u539f", "\u65b0\u753a", "\u30f6\u4e18"],
};
function isNearSea(x, y, sea, radius = 3) {
if (!sea) return false;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const nx = x + dx;
const ny = y + dy;
if (inside(nx, ny) && sea[indexOf(nx, ny)]) return true;
}
}
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 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;
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 (coastal && !mountain) return "coastal";
if (river) return "river";
if (mountain && !plain) return "mountain";
if (plain) return "plain";
return "plain";
}
export function chooseNameSuffixPool(entity, fields) {
return CONTEXT_SUFFIXES[chooseNameContext(entity, fields)] || CONTEXT_SUFFIXES.plain;
}
function chooseContextFreePool(entity, fields, nameParts) {
const context = chooseNameContext(entity, fields);
return nameParts.contextFree?.[context] || nameParts.contextFree?.plain || nameParts.infixes || 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 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);
}
export function contextualDefaultName(seed, id, entity, nameParts, fields, attempt = 0) {
const prefixKey = id.split("-")[0];
const n = Number(id.split("-")[1] || 0) + attempt * 997;
const prefixes = nameParts.prefixes || [""];
const infixes = nameParts.infixes || nameParts.free || [""];
const suffixes = nameParts.suffixes || [""];
const contextFree = chooseContextFreePool(entity, fields, nameParts);
const contextSuffixes = chooseNameSuffixPool(entity, fields);
const parts = [];
if (chance(seed, n, 11 + prefixKey.length, NAME_PROBABILITIES.prefixOnly)) {
appendPart(parts, pick(prefixes, seed, n, 101));
}
const useContextMain = chance(seed, n, 13 + prefixKey.length, 0.42);
appendPart(parts, pick(useContextMain ? contextFree : infixes, seed, n, useContextMain ? 211 : 223));
if (chance(seed, n, 19 + prefixKey.length, NAME_PROBABILITIES.secondInfix)) {
appendPart(parts, pick(infixes, seed, n, 401));
}
const useContextSuffix = chance(seed, n, 23 + prefixKey.length, NAME_PROBABILITIES.contextSuffix);
appendPart(parts, pick(useContextSuffix ? contextSuffixes : suffixes, seed, n, useContextSuffix ? 503 : 509));
return parts.slice(-3).join("") || pick(suffixes, seed, n, 601) || "\u539f";
}

View file

@ -3,20 +3,18 @@ import { MAP_H, MAP_W, indexOf } from "./grid.js";
export function terrainBoundaryTargetForMetrics(map, i) { export function terrainBoundaryTargetForMetrics(map, i) {
const lu = map.landuse[i]; const lu = map.landuse[i];
const urbanPenalty = Math.min(1, (lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0) + map.populationDensity[i] * 1.35); const urbanPenalty = Math.min(1, (lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0) + map.populationDensity[i] * 1.35);
const crest = map.crestCrossingScore?.[i] || 0;
const majorRiver = Math.min(1, Math.max(map.river[i] - 0.32, 0) * 1.9 + Math.max(map.flowAccum[i] - 0.38, 0) * 0.75); const majorRiver = Math.min(1, Math.max(map.river[i] - 0.32, 0) * 1.9 + Math.max(map.flowAccum[i] - 0.38, 0) * 0.75);
const minorStream = Math.min(1, map.river[i] * 0.34 + map.flowAccum[i] * 0.18); const minorStream = Math.min(1, map.river[i] * 0.34 + map.flowAccum[i] * 0.18);
const ridgeDivide = Math.min(1, map.ridgeField[i] * 1.55 + Math.max(0, map.elevation[i] - 0.54) * map.ridgeField[i] * 0.95); const ridgeDivide = Math.min(1, map.ridgeField[i] * 1.55 + Math.max(0, map.elevation[i] - 0.54) * map.ridgeField[i] * 0.95);
const slopeBreak = Math.min(1, map.slope[i] * 0.58 + Math.max(0, map.slope[i] - 0.32) * 0.68); const slopeBreak = Math.min(1, map.slope[i] * 0.58 + Math.max(0, map.slope[i] - 0.32) * 0.68);
const highGround = Math.max(0, map.elevation[i] - 0.56) * 0.22; const highGround = Math.max(0, map.elevation[i] - 0.56) * 0.22;
const valleyFloorPenalty = map.valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62); const valleyFloorPenalty = map.valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62);
return Math.max(0, Math.min(1, ridgeDivide + crest * 1.38 + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72)); return Math.max(0, Math.min(1, ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72));
} }
export function adminBoundaryMetrics(map) { export function adminBoundaryMetrics(map) {
let borderEdges = 0; let borderEdges = 0;
let targetSum = 0; let targetSum = 0;
let crestSum = 0;
let denseUrbanEdges = 0; let denseUrbanEdges = 0;
let rightAngleRuns = 0; let rightAngleRuns = 0;
let voronoiLikeEdges = 0; let voronoiLikeEdges = 0;
@ -30,9 +28,7 @@ export function adminBoundaryMetrics(map) {
if (!map.prefectureMask[ni] || map.sea[ni] || map.adminId[ni] < 0 || map.adminId[ni] === map.adminId[i]) continue; if (!map.prefectureMask[ni] || map.sea[ni] || map.adminId[ni] < 0 || map.adminId[ni] === map.adminId[i]) continue;
borderEdges++; borderEdges++;
const edgeTarget = (terrainBoundaryTargetForMetrics(map, i) + terrainBoundaryTargetForMetrics(map, ni)) * 0.5; const edgeTarget = (terrainBoundaryTargetForMetrics(map, i) + terrainBoundaryTargetForMetrics(map, ni)) * 0.5;
const crestSupport = ((map.crestCrossingScore?.[i] || 0) + (map.crestCrossingScore?.[ni] || 0)) * 0.5;
targetSum += edgeTarget; targetSum += edgeTarget;
crestSum += crestSupport;
const urban = Math.max(map.populationDensity[i], map.populationDensity[ni]) > 0.58 || [2, 3, 4, 7, 8].includes(map.landuse[i]) || [2, 3, 4, 7, 8].includes(map.landuse[ni]); const urban = Math.max(map.populationDensity[i], map.populationDensity[ni]) > 0.58 || [2, 3, 4, 7, 8].includes(map.landuse[i]) || [2, 3, 4, 7, 8].includes(map.landuse[ni]);
if (urban) denseUrbanEdges++; if (urban) denseUrbanEdges++;
const ca = map.adminCenters[map.adminId[i]]; const ca = map.adminCenters[map.adminId[i]];
@ -42,8 +38,7 @@ export function adminBoundaryMetrics(map) {
const my = y + dy * 0.5; const my = y + dy * 0.5;
const dA = Math.hypot(mx - ca.x, my - ca.y); const dA = Math.hypot(mx - ca.x, my - ca.y);
const dB = Math.hypot(mx - cb.x, my - cb.y); const dB = Math.hypot(mx - cb.x, my - cb.y);
const naturalSupport = Math.max(edgeTarget, crestSupport, map.ridgeField[i], map.ridgeField[ni], map.river[i], map.river[ni]); if (Math.abs(dA - dB) < 4.2 && edgeTarget < 0.40) voronoiLikeEdges++;
if (Math.abs(dA - dB) < 4.2 && naturalSupport < 0.42) voronoiLikeEdges++;
} }
if (edgeTarget < 0.16 && Math.max(map.slope[i], map.slope[ni]) < 0.24 && Math.max(map.ridgeField[i], map.ridgeField[ni]) < 0.28 && Math.max(map.river[i], map.river[ni]) < 0.26) { if (edgeTarget < 0.16 && Math.max(map.slope[i], map.slope[ni]) < 0.24 && Math.max(map.ridgeField[i], map.ridgeField[ni]) < 0.28 && Math.max(map.river[i], map.river[ni]) < 0.26) {
lowScoreFlatEdges++; lowScoreFlatEdges++;
@ -105,7 +100,6 @@ export function adminBoundaryMetrics(map) {
return { return {
borderEdges, borderEdges,
avgTarget: borderEdges ? targetSum / borderEdges : 0, avgTarget: borderEdges ? targetSum / borderEdges : 0,
avgCrest: borderEdges ? crestSum / borderEdges : 0,
denseUrbanRate: borderEdges ? denseUrbanEdges / borderEdges : 0, denseUrbanRate: borderEdges ? denseUrbanEdges / borderEdges : 0,
rightAngleRate: borderEdges ? rightAngleRuns / borderEdges : 0, rightAngleRate: borderEdges ? rightAngleRuns / borderEdges : 0,
voronoiLikeRate: borderEdges ? voronoiLikeEdges / borderEdges : 0, voronoiLikeRate: borderEdges ? voronoiLikeEdges / borderEdges : 0,

View file

@ -127,7 +127,6 @@ try {
assert(map.prefectureMask.length === size, "prefecture mask length matches map size"); assert(map.prefectureMask.length === size, "prefecture mask length matches map size");
assert(map.populationDensity.length === size, "population density length matches map size"); assert(map.populationDensity.length === size, "population density length matches map size");
assert(map.ridgeField.length === size && map.valleyField.length === size && map.flowAccum.length === size, "causal terrain fields match map size"); assert(map.ridgeField.length === size && map.valleyField.length === size && map.flowAccum.length === size, "causal terrain fields match map size");
assert(map.crestCrossingScore.length === size, "crest crossing score field matches map size");
assert(map.erosionField.length === size && map.depositionField.length === size, "erosion and deposition fields match map size"); assert(map.erosionField.length === size && map.depositionField.length === size, "erosion and deposition fields match map size");
assert(map.settlementCluster.length === size, "settlement cluster field matches map size"); assert(map.settlementCluster.length === size, "settlement cluster field matches map size");
assert(Array.isArray(map.bridges) && Array.isArray(map.tunnels) && Array.isArray(map.harborWorks), "legacy bridge/tunnel arrays and harbor arrays exist"); assert(Array.isArray(map.bridges) && Array.isArray(map.tunnels) && Array.isArray(map.harborWorks), "legacy bridge/tunnel arrays and harbor arrays exist");
@ -183,7 +182,6 @@ try {
assert(adminMetrics.maxComponents <= 4, "municipal topology repair prevents excessive disconnected fragments"); assert(adminMetrics.maxComponents <= 4, "municipal topology repair prevents excessive disconnected fragments");
assert(adminMetrics.disconnectedMunicipalities <= Math.max(2, Math.ceil(adminMetrics.municipalityCount * 0.20)), "most municipalities remain connected after terrain snapping"); assert(adminMetrics.disconnectedMunicipalities <= Math.max(2, Math.ceil(adminMetrics.municipalityCount * 0.20)), "most municipalities remain connected after terrain snapping");
assert(adminMetrics.avgTarget > 0.18, "admin borders align with terrain target features often enough"); assert(adminMetrics.avgTarget > 0.18, "admin borders align with terrain target features often enough");
assert(adminMetrics.avgCrest > 0.006, "admin borders pick up crest crossings");
assert(adminMetrics.denseUrbanRate < 0.42, "admin borders avoid excessive dense urban crossings"); assert(adminMetrics.denseUrbanRate < 0.42, "admin borders avoid excessive dense urban crossings");
assert(adminMetrics.rightAngleRate < 0.46, "admin borders avoid excessive unsupported stair-step artifacts"); assert(adminMetrics.rightAngleRate < 0.46, "admin borders avoid excessive unsupported stair-step artifacts");
assert(adminMetrics.voronoiLikeRate < 0.58, "admin borders are not dominated by weak-terrain center bisectors"); assert(adminMetrics.voronoiLikeRate < 0.58, "admin borders are not dominated by weak-terrain center bisectors");
@ -242,7 +240,6 @@ try {
assert(metrics.centerValidRatio >= 0.90, `seed ${seed}: municipality centers remain valid`); assert(metrics.centerValidRatio >= 0.90, `seed ${seed}: municipality centers remain valid`);
assert(metrics.maxComponents <= 5, `seed ${seed}: topology repair limits disconnected fragments`); assert(metrics.maxComponents <= 5, `seed ${seed}: topology repair limits disconnected fragments`);
assert(metrics.avgTarget > 0.14, `seed ${seed}: borders retain terrain-boundary affinity`); assert(metrics.avgTarget > 0.14, `seed ${seed}: borders retain terrain-boundary affinity`);
assert(metrics.avgCrest > 0.004, `seed ${seed}: borders retain crest-crossing affinity`);
assert(metrics.denseUrbanRate < 0.50, `seed ${seed}: borders avoid excessive dense urban cuts`); assert(metrics.denseUrbanRate < 0.50, `seed ${seed}: borders avoid excessive dense urban cuts`);
assert(metrics.voronoiLikeRate < 0.66, `seed ${seed}: weak-terrain Voronoi-like border ratio stays bounded`); assert(metrics.voronoiLikeRate < 0.66, `seed ${seed}: weak-terrain Voronoi-like border ratio stays bounded`);
assert(metrics.lowScoreFlatRate < 0.50, `seed ${seed}: low-score flat border ratio stays bounded`); assert(metrics.lowScoreFlatRate < 0.50, `seed ${seed}: low-score flat border ratio stays bounded`);