diff --git a/adminRegions.js b/adminRegions.js index dba3f00..fdbd166 100644 --- a/adminRegions.js +++ b/adminRegions.js @@ -25,7 +25,7 @@ function neighbors4(x, y) { 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); adminId.fill(-1); 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 riverBarrier = Math.max(river[ni], river[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 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 slopePenalty = slope[ni] * 10.6; 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 urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0; 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; 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 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 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 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 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) { @@ -347,7 +344,7 @@ function buildAdminProtectedMask(adminId, prefectureMask, sea, adminCenters = [] function localBoundaryEnergy(labels, i, candidateId, targetScore, centerDist, populationDensity, landuse, river, valleyField) { const [x, y] = xyOf(i); const oldId = labels[i]; - let energy = (centerDist[candidateId]?.[i] ?? 0) * 0.035; + let energy = centerDist[candidateId]?.[i] ?? 0; let same4 = 0; let diff4 = 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 (candidateId !== oldId && centerDist[candidateId] && centerDist[oldId]) { 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; } @@ -470,7 +467,7 @@ function classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyFie 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); unitId.fill(-1); const cellClass = new Int16Array(SIZE); @@ -507,7 +504,7 @@ function buildLandscapeUnits(prefectureMask, sea, elevation, slope, river, ridge } 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); mergeTinyLandscapeUnits(unitId, units, 10); 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)); } -export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, 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); +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, plain, agriculture, populationDensity, landuse); if (units.length === 0) return; for (let id = 0; id < adminCenters.length; id++) { const center = adminCenters[id]; @@ -624,11 +621,11 @@ export function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, e 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); - 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 band = buildBoundaryBand(adminId, prefectureMask, sea, 8); + const band = buildBoundaryBand(adminId, prefectureMask, sea, 5); const adminIds = [...new Set([...adminId].filter((id) => id >= 0))]; const centerDist = buildCenterDistanceFields(adminIds, adminCenters, prefectureMask, sea); let current = new Int16Array(adminId); diff --git a/fields.js b/fields.js index 5b432de..593080c 100644 --- a/fields.js +++ b/fields.js @@ -18,7 +18,6 @@ export function createMapFields() { basinField: new Float32Array(SIZE), coastalLowland: new Float32Array(SIZE), flowAccum: new Float32Array(SIZE), - crestCrossingScore: new Float32Array(SIZE), erosionField: new Float32Array(SIZE), depositionField: new Float32Array(SIZE), flowTo, diff --git a/mapGenerator.js b/mapGenerator.js index 2ec0252..23d5cc6 100644 --- a/mapGenerator.js +++ b/mapGenerator.js @@ -1,4 +1,4 @@ -import { CUSTOM_NAMES, NAME_PARTS } from "./names.js"; +import { CUSTOM_NAMES, NAME_PARTS, contextualDefaultName } from "./names.js"; import { applyLandscapeUnitAdminPartition, generateAdminRegions, @@ -569,25 +569,14 @@ function attachIdsAndNames(points, prefix, seed, kindOverride = null, nameFields return points.map((p, i) => { const id = `${prefix}-${i}`; const kind = kindOverride || p.kind; - - 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; - + let name = CUSTOM_NAMES[id]; if (!name) { for (let attempt = 0; attempt < 8; attempt++) { name = defaultName(seed + prefix.length * 1000, id, { ...p, kind }, nameFields, attempt); if (!usedNames || !usedNames.has(name)) break; } } - - if (usedNames && name) usedNames.add(name); - + if (usedNames) usedNames.add(name); return { ...p, id, diff --git a/names.js b/names.js index df76c42..18c29ed 100644 --- a/names.js +++ b/names.js @@ -6,34 +6,44 @@ import { hash2 } from "./random.js"; export const NAME_PARTS = { // May appear only at the beginning of a name. - prefixOnly: ["青", "白", "黒", "赤", "浅", "深", "大", "小", "新", "古", "上", "下", "東", "西", "南", "北", "中", "本", "元", "奥", "遠", "近", "前", "高", "佐々", "長", "丸", "伊", "阿", "千代", "茨", "美", "宇", "徳", "広", "", "牟", "一", "壱", "二", "三", "四", "五", "六", "七", "八", "九", "十", "五十", "百", "千", "万"], + prefixOnly: ["大", "小", "新", "古", "浅", "深", "上", "下", "東", "西", "南", "北", "中", "本", "広", "元", "奥", "前", "高", "長", "丸", "一", "壱", "二", "三", "四", "五", "六", "七", "八", "九", "十", "五十", "百", "千", "万", "阿", "佐々", "代々", "千代", "宇", "伊", "牟", "美",], // Generic morphemes that may appear at the beginning or in the middle. free: [ - "前", "内", "母", "父", - "川", "山", "森", "松", "原", "黍", "栃", "梨", "柏", "桜", "杉", "柳", "葛", "竹", "梅", "藤", "篠", "米", "馬", "石", "岩", "鷲", "鳥", "鷹", "犬", "猿", "鹿", "熊", "羽", "木", "林", "根", "葉", "葦", "岐","日", "月", "島", "畠", "野", "田", - "井", "守", "盛", "幡", "河内", "敷", "鍋", "凪", "太", "嘉", "倉", "昌", "芸", "相", "生", "折", "摂", "香", "佳", "加", "笠", "行", "油", "柚", "関", "伯", "桶", "久", "代", "旭", "手", "蓑", "仙", "豊", "飾", "戸", "矢", "玉", "須", "津", "佐", "志", "布", "蘇", "曾", "庄", "甲", "尾", "朝", "比古", "目", "賀", "彦", "姫", "輪", "和", "福", "岡", "富", "住", "由", "君", "飯", "那", "奈", "肥", "越", "曲輪", "車", "渋", "幸", + //pattern +"青", "白", "黒", "赤", "藍", + + //nature +"川", "山", "森", "松", "竹", "梅", "柳", "栃", "橘", "柏", "樫", "麻", "桜", "岡", "木", "林", "森", "根", "葉", "葦", "日", "月", "君", +"野", "尾", "旭", "朝", "霧", + + //objects +"畠", "秦", "幡", "井", "真", "田", "井", "口", "関", "矢", "手", "戸", "庄", "屋", "土", "鋸", "琴", "笙", "彦", "比", "碑", "多", "", "", + //others +"由", "志", "成", "幸", "徳", "甲", "由", "和", "清", "弘", "保", "徳", "福", "富", "住", "幸", "弥", "住", "生", "昌", "相", "香", "吉", "𠮷", "佐", "多", "保", "加" ], // May appear only at the end of a name. - suffixOnly: ["宿", "郷", "里", "新田", "磨", "摩", "原", "口", "泉"], + suffixOnly: ["宿", "口", "摩", "磨", "潟", "茂"], // Context-aware morphemes that may appear at the beginning or in the middle. contextFree: { - coastal: ["浜", "浦", "津", "湊", "潮", "磯", "島", "潮", "塩", "汐", "波", "江"], + coastal: ["浜", "浦", "津", "湊", "潮", "磯", "海", "島", "磯", "渚", "潮", "汐"], river: ["瀬", "川", "橋", "渡", "沢", "淵", "沼", "堀"], plain: ["原", "野", "田", "畠", "里", "平", "牧", "塚"], - mountain: ["山", "谷", "沢", "峰", "尾", "峠", "湯", "磐", "迫","久保","窪","玖保","硲"], - historic: ["館", "宮", "寺", "社", "陣", "府", "町"], + mountain: ["山", "谷", "沢", "峰", "尾", "久保", "迫", "岳"], + historic: ["城", "館", "宮", "寺", "社", "陣", "府", "町"], + suburban: [], }, // Context-aware morphemes that may appear only at the end of a name. contextSuffixes: { - coastal: ["浜", "湊", "灘", "浦", "津", "崎"], + coastal: ["浜", "湊", "灘", "浦", "津", "崎", "嶋"], river: ["川", "瀬", "橋", "渡", "沼", "沢"], plain: ["原", "野", "田", "里", "村", "平"], - mountain: ["谷", "峡", "峰", "山", "沢", "岳"], + mountain: ["谷", "峡", "尾根", "山", "沢", "麓"], historic: ["城", "通", "館", "宮", "町"], + suburban: ["丘", "台", "野", "原", "新町", "ヶ丘"], }, }; @@ -43,10 +53,9 @@ export const NAME_PROBABILITIES = { additionalContextFree: 0.22, secondFree: 0.18, finalContextSuffix: 0.10, - finalSuffixOnly: 0.10, + finalSuffixOnly: 0.50, finalFree: 0.20, - twoCharacter: 0.90, - customName: 0.20, + twoCharacter: 0.80, }; export const CONTEXT_SUFFIXES = NAME_PARTS.contextSuffixes; @@ -55,26 +64,6 @@ export const CUSTOM_NAMES = { // "city-0": "Aohara", // "port-0": "Shirahama", // "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) { diff --git a/placeNameContext.js b/placeNameContext.js deleted file mode 100644 index c13be0d..0000000 --- a/placeNameContext.js +++ /dev/null @@ -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"; -} diff --git a/qualityMetrics.js b/qualityMetrics.js index e0c98e8..d8dc82d 100644 --- a/qualityMetrics.js +++ b/qualityMetrics.js @@ -3,20 +3,18 @@ import { MAP_H, MAP_W, indexOf } from "./grid.js"; export function terrainBoundaryTargetForMetrics(map, 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 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 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 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 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) { let borderEdges = 0; let targetSum = 0; - let crestSum = 0; let denseUrbanEdges = 0; let rightAngleRuns = 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; borderEdges++; const edgeTarget = (terrainBoundaryTargetForMetrics(map, i) + terrainBoundaryTargetForMetrics(map, ni)) * 0.5; - const crestSupport = ((map.crestCrossingScore?.[i] || 0) + (map.crestCrossingScore?.[ni] || 0)) * 0.5; 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]); if (urban) denseUrbanEdges++; const ca = map.adminCenters[map.adminId[i]]; @@ -42,8 +38,7 @@ export function adminBoundaryMetrics(map) { const my = y + dy * 0.5; const dA = Math.hypot(mx - ca.x, my - ca.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 && naturalSupport < 0.42) voronoiLikeEdges++; + if (Math.abs(dA - dB) < 4.2 && edgeTarget < 0.40) 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) { lowScoreFlatEdges++; @@ -105,7 +100,6 @@ export function adminBoundaryMetrics(map) { return { borderEdges, avgTarget: borderEdges ? targetSum / borderEdges : 0, - avgCrest: borderEdges ? crestSum / borderEdges : 0, denseUrbanRate: borderEdges ? denseUrbanEdges / borderEdges : 0, rightAngleRate: borderEdges ? rightAngleRuns / borderEdges : 0, voronoiLikeRate: borderEdges ? voronoiLikeEdges / borderEdges : 0, diff --git a/test.js b/test.js index 7d384c9..7a70125 100644 --- a/test.js +++ b/test.js @@ -127,7 +127,6 @@ try { assert(map.prefectureMask.length === size, "prefecture mask 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.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.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"); @@ -183,7 +182,6 @@ try { 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.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.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"); @@ -242,7 +240,6 @@ try { assert(metrics.centerValidRatio >= 0.90, `seed ${seed}: municipality centers remain valid`); 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.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.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`);