human tweaks

This commit is contained in:
33333-33333 2026-05-24 19:33:09 +09:00
commit f5e7a1df1d
9 changed files with 1201 additions and 45 deletions

View file

@ -286,7 +286,22 @@ function restoreSurvivedSeedsByCompartment(adminId, prefectureMask, sea, compart
return { changedCells, restoredSeeds };
}
function computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages }) {
function municipalityCountBoundsForRegion(landCells, meta = {}) {
const focused = meta.isFocusedRegion !== false;
if (focused) return { min: 20, max: 50 };
// Neighbor prefectures are often visible only as clipped map-edge slivers.
// Avoid giving every tiny visible fragment the full 20-municipality floor.
let min = 1;
if (landCells >= 500) min = 2;
if (landCells >= 950) min = 3;
if (landCells >= 1700) min = 5;
if (landCells >= 2800) min = 7;
if (landCells >= 4300) min = 10;
const max = clamp(Math.round(landCells / 260 + 2), Math.max(min, 2), 34);
return { min, max };
}
function computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta = {} }) {
let landCells = 0;
let habitableCells = 0;
let lowlandCells = 0;
@ -314,8 +329,9 @@ function computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope,
const basinBonus = Math.min(8, [...basinField].filter((v, i) => prefectureMask[i] && !sea[i] && v > 0.34).length / 520);
const mountainRatio = landCells ? mountainCells / landCells : 0;
const lowlandBonus = Math.min(7, lowlandCells / 430);
const target = Math.round(habitableCells / 230 + settlementWeight + coastlineComplexity * 0.03 + basinBonus * 0.55 + lowlandBonus - mountainRatio * 2.2);
return clamp(target, 20, 50);
const rawTarget = Math.round(habitableCells / 230 + settlementWeight + coastlineComplexity * 0.03 + basinBonus * 0.55 + lowlandBonus - mountainRatio * 2.2);
const { min, max } = municipalityCountBoundsForRegion(landCells, adminRegionMeta);
return clamp(rawTarget, min, max);
}
function lowlandAdminSeedScore(i, { elevation, slope, ridgeField, plain, basinField, coastalLowland, settlementScore, populationDensity, roadInfluence, railInfluence2, stationInfluence, landuse }) {
@ -620,14 +636,24 @@ function generateAdminLayoutForMask({
stations,
industrialZones,
logisticsParks,
adminRegionMeta = {},
adminProgress = null,
}) {
const boundaryRidgeField = naturalBarrierScore
? Float32Array.from(ridgeField, (value, i) => clamp(value * 0.72 + naturalBarrierScore[i] * 0.46))
: ridgeField;
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "classify satellites" });
const satelliteClassificationDebug = classifySatelliteMunicipalities(satelliteCities, modernCities, prefectureMask, sea, landuse, populationDensity, roadInfluence, railInfluence2, boundaryRidgeField, river, flowAccum);
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages });
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta });
const compartmentMultiplier = clamp(4.6 + rand(seed, 1320) * 2.4, 4.6, 7.0);
let targetCompartmentCount = clamp(Math.round(targetMunicipalityCount * compartmentMultiplier), 120, 360);
const regionLandArea = adminRegionMeta.landArea || maskLandArea(prefectureMask, sea);
const minCompartmentTarget = adminRegionMeta.isFocusedRegion === false
? clamp(Math.round(Math.max(targetMunicipalityCount * 3.2, regionLandArea / 75)), 18, 90)
: 120;
const maxCompartmentTarget = adminRegionMeta.isFocusedRegion === false
? clamp(Math.round(Math.max(targetMunicipalityCount * 5.8, regionLandArea / 38)), minCompartmentTarget, 220)
: 360;
let targetCompartmentCount = clamp(Math.round(targetMunicipalityCount * compartmentMultiplier), minCompartmentTarget, maxCompartmentTarget);
let adminCentersRaw = buildLowlandAdminSeeds({
seed,
targetMunicipalityCount,
@ -652,12 +678,14 @@ function generateAdminLayoutForMask({
newTowns,
stations,
});
if (adminCentersRaw.length < 20) targetCompartmentCount = Math.max(targetCompartmentCount, 120);
if (adminCentersRaw.length < 20) targetCompartmentCount = Math.max(targetCompartmentCount, minCompartmentTarget);
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "natural compartments", targetMunicipalityCount, targetCompartmentCount, seedCount: adminCentersRaw.length });
const compartmentAssignment = assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, {
seed,
targetMunicipalityCount,
targetCompartmentCount,
maxNaturalCompartmentArea: Math.max(32, Math.round(maskLandArea(prefectureMask, sea) / Math.max(1, targetCompartmentCount) * 1.65)),
progress: (step) => adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step }),
});
const adminId = compartmentAssignment.adminId;
let previousSnapshot = new Int16Array(adminId);
@ -710,6 +738,7 @@ function generateAdminLayoutForMask({
satelliteMunicipalityStats: satelliteClassificationDebug,
...compartmentAssignment.debug,
};
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "seed lifecycle" });
const seedLifecycle = buildSeedLifecycle(adminCentersRaw, adminId, prefectureMask, sea, 35);
const pendingSplitDebug = splitOversizedLowlandsWithPendingSeeds(adminId, prefectureMask, sea, {
plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation,
@ -735,6 +764,7 @@ function generateAdminLayoutForMask({
adminDebug[field] = changedCellsSince(previousSnapshot, adminId, prefectureMask, sea);
previousSnapshot = new Int16Array(adminId);
}
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "smooth boundaries" });
smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, populationDensity, landuse, 2);
markChanged("changedAfterSmooth");
@ -797,19 +827,34 @@ function generateAdminLayoutForMask({
const activeAdminCenters = () => adminCentersRaw.filter((_, id) => activeAdminIds.has(id));
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 120, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 100, protectedPoints: activeAdminCenters() });
markChanged("changedAfterInitialMerge");
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "topology cleanup" });
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, modernCities, 180);
markChanged("changedAfterInitialExclaveRemoval");
// The initial compartment graph assignment is now the primary natural partition.
// Re-running the older raw landscape-unit pass here collapses lowland seeds into a few broad owners.
markChanged("changedAfterLandscapePartition");
const oversizedSplitDebug = splitOversizedLowlandMunicipalities(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, [...(satelliteCities || []), ...newTowns, ...markets, ...villages]);
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "split oversized municipalities" });
// The older oversized-lowland pass rebuilds natural compartments a second time.
// The current pipeline already performs pending-seed lowland splitting on the active
// compartment graph above, so keep the full admin layout while avoiding the duplicate
// high-cost recomputation.
const oversizedSplitDebug = {
changedCells: 0,
splitMunicipalities: 0,
rejectedMunicipalities: 0,
skippedDuplicateCompartmentRebuild: true,
skippedForVisibleFragment: adminRegionMeta.isFocusedRegion === false && regionLandArea < 6500,
};
adminDebug.changedAfterOversizedRuralSplit = oversizedSplitDebug.changedCells;
adminDebug.oversizedRuralMunicipalitiesSplit = oversizedSplitDebug.splitMunicipalities;
adminDebug.oversizedRuralSplits = oversizedSplitDebug.splitMunicipalities;
adminDebug.oversizedLowlandSplits = oversizedSplitDebug.splitMunicipalities;
adminDebug.ruralSplitsAccepted = oversizedSplitDebug.splitMunicipalities;
adminDebug.ruralSplitsRejected = oversizedSplitDebug.rejectedMunicipalities || 0;
adminDebug.oversizedSplitSkippedForVisibleFragment = Boolean(oversizedSplitDebug.skippedForVisibleFragment);
adminDebug.oversizedSplitSkippedDuplicateCompartmentRebuild = Boolean(oversizedSplitDebug.skippedDuplicateCompartmentRebuild);
previousSnapshot = new Int16Array(adminId);
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "terrain snap" });
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 2);
markChanged("changedAfterSnap");
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 360);
@ -881,6 +926,7 @@ function generateAdminLayoutForMask({
adminDebug.seedLifecycle = seedLifecycle.map((seed) => ({ id: seed.id, state: seed.state, protected: seed.protected, area: seed.area }));
adminDebug.borderNaturalBarrierAverage = adminDebug.finalBorderNaturalBarrierAverage ?? 0;
adminDebug.voronoiLikeRate = adminDebug.voronoiLikeRateAfter ?? 0;
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "extract borders" });
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
@ -889,7 +935,9 @@ function generateAdminLayoutForMask({
function filterPointsForMask(points = [], mask, sea) {
return (points || []).filter((p) => p && inside(p.x, p.y) && mask[indexOf(p.x, p.y)] && !sea[indexOf(p.x, p.y)]);
return (points || [])
.filter((p) => p && inside(p.x, p.y) && mask[indexOf(p.x, p.y)] && !sea[indexOf(p.x, p.y)])
.map((p) => ({ ...p }));
}
function buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId) {
@ -907,6 +955,85 @@ function maskLandArea(mask, sea) {
return area;
}
function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields = {}) {
const activeIds = [...new Set([...adminId].filter((id, i) => id >= 0 && humanMask[i] && !sea[i]))].sort((a, b) => a - b);
const idMap = new Map(activeIds.map((oldId, newId) => [oldId, newId]));
const newAdminId = new Int16Array(SIZE);
newAdminId.fill(-1);
const cellsByNewId = Array.from({ length: activeIds.length }, () => []);
for (let i = 0; i < SIZE; i++) {
if (!humanMask[i] || sea[i]) continue;
const newId = idMap.get(adminId[i]);
if (newId === undefined) continue;
newAdminId[i] = newId;
cellsByNewId[newId].push(i);
}
const chooseOffice = (newId, oldId) => {
const cells = cellsByNewId[newId] || [];
const current = centers[oldId];
if (current && inside(current.x, current.y)) {
const ci = indexOf(current.x, current.y);
if (newAdminId[ci] === newId && humanMask[ci] && !sea[ci]) {
return { ...current, localAdminId: newId, oldAdminId: oldId, municipalityOffice: true };
}
}
let sx = 0, sy = 0;
for (const i of cells) {
const [x, y] = xyOf(i);
sx += x;
sy += y;
}
const cx = cells.length ? sx / cells.length : current?.x || 0;
const cy = cells.length ? sy / cells.length : current?.y || 0;
let bestI = cells[0] ?? -1;
let bestScore = -INF;
for (const i of cells) {
const [x, y] = xyOf(i);
const land = fields.landuse?.[i] ?? 0;
const urbanBonus = land === 3 ? 1.2 : land === 2 ? 1.0 : land === 4 || land === 7 || land === 8 ? 0.55 : land === 1 ? 0.24 : 0;
const density = fields.populationDensity?.[i] || 0;
const settlement = fields.settlementScore?.[i] || 0;
const score =
density * 2.25 +
settlement * 0.75 +
urbanBonus +
(fields.plain?.[i] || 0) * 0.32 +
(fields.basinField?.[i] || 0) * 0.24 +
(fields.coastalLowland?.[i] || 0) * 0.18 +
(fields.roadInfluence?.[i] || 0) * 0.34 +
(fields.stationInfluence?.[i] || 0) * 0.45 -
(fields.slope?.[i] || 0) * 0.52 -
Math.hypot(x - cx, y - cy) * 0.018 +
hash2(x, y, 91337 + newId) * 0.012;
if (score > bestScore) { bestScore = score; bestI = i; }
}
const [bx, by] = bestI >= 0 ? xyOf(bestI) : [Math.round(cx), Math.round(cy)];
return {
...(current || {}),
x: bx,
y: by,
score: bestScore > -INF ? bestScore : 0,
seedKind: current?.seedKind || "generatedMunicipalOffice",
invisibleLowlandAdminSeed: current?.invisibleLowlandAdminSeed ?? true,
localAdminId: newId,
oldAdminId: oldId,
municipalityOffice: true,
generatedOfficePoint: !current || !inside(current.x, current.y) || newAdminId[indexOf(current.x, current.y)] !== newId,
};
};
const adminCenters = activeIds.map((oldId, newId) => chooseOffice(newId, oldId));
return {
adminId: newAdminId,
adminCenters,
activeMunicipalityCount: activeIds.length,
removedUnusedAdminCenterCount: Math.max(0, centers.length - activeIds.length),
generatedOfficePointCount: adminCenters.filter((p) => p.generatedOfficePoint).length,
};
}
function discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea) {
const ids = new Set();
for (let i = 0; i < SIZE; i++) {
@ -918,16 +1045,17 @@ function discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea) {
}
export function generateAdminLayout(context) {
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope } = context;
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope, adminProgress } = context;
const minFullAdminRegionArea = 1500;
const regionIds = discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)
.filter((regionId) => maskLandArea(buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId), sea) >= 120);
.filter((regionId) => regionId === 0 || (regionId !== OUTER_ANCHOR_REGION_ID && maskLandArea(buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId), sea) >= minFullAdminRegionArea));
if (!prefectureRegionId || regionIds.length <= 1) return generateAdminLayoutForMask(context);
const combinedAdminId = new Int16Array(SIZE);
let combinedAdminId = new Int16Array(SIZE);
combinedAdminId.fill(-1);
const combinedHumanMask = new Uint8Array(SIZE);
const combinedCenters = [];
let combinedCenters = [];
const combinedCompartmentBorders = [];
const perRegion = [];
let idOffset = 0;
@ -935,7 +1063,7 @@ export function generateAdminLayout(context) {
for (const regionId of regionIds) {
const regionMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
const regionArea = maskLandArea(regionMask, sea);
if (regionArea < 120) continue;
if (regionId !== 0 && regionArea < minFullAdminRegionArea) continue;
const localContext = {
...context,
@ -950,9 +1078,18 @@ export function generateAdminLayout(context) {
stations: filterPointsForMask(context.stations, regionMask, sea),
industrialZones: filterPointsForMask(context.industrialZones, regionMask, sea),
logisticsParks: filterPointsForMask(context.logisticsParks, regionMask, sea),
adminRegionMeta: {
regionId,
landArea: regionArea,
isFocusedRegion: regionId === 0,
isOuterAnchorRegion: regionId === OUTER_ANCHOR_REGION_ID,
},
adminProgress,
};
adminProgress?.({ status: "region-start", regionId, area: regionArea });
const local = generateAdminLayoutForMask(localContext);
adminProgress?.({ status: "region-done", regionId, area: regionArea, municipalities: local.adminDebug?.finalMunicipalityCount || local.adminDebug?.actualMunicipalityCount || 0 });
if (local.adminDebug?.compartmentBorders?.length) combinedCompartmentBorders.push(...local.adminDebug.compartmentBorders);
let localMaxAdminId = -1;
for (let i = 0; i < SIZE; i++) if (regionMask[i] && !sea[i] && (local.adminId?.[i] ?? -1) > localMaxAdminId) localMaxAdminId = local.adminId[i];
@ -1036,8 +1173,21 @@ export function generateAdminLayout(context) {
perRegion.push({ regionId, area: cells.length, centerCount: 1, municipalityCount: 1, naturalCompartmentCount: 1, targetNaturalCompartmentCount: 1, averageCompartmentArea: cells.length, maxCompartmentArea: cells.length, maxCompartmentElongation: 1, singleCompartmentMunicipalityRatio: 1, tinyRegionFallback: true });
}
const compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, {
populationDensity,
plain,
slope,
settlementScore: context.settlementScore,
landuse: context.landuse,
basinField: context.basinField,
coastalLowland: context.coastalLowland,
roadInfluence: context.roadInfluence,
stationInfluence: context.stationInfluence,
});
combinedAdminId = compactedAdmin.adminId;
combinedCenters = compactedAdmin.adminCenters;
const adminBorders = extractAdminBorderSegments(combinedAdminId, combinedHumanMask);
const totalMunicipalityCount = new Set([...combinedAdminId].filter((id, i) => id >= 0 && combinedHumanMask[i] && !sea[i])).size;
const totalMunicipalityCount = compactedAdmin.activeMunicipalityCount;
const totalNaturalCompartmentCount = perRegion.reduce((sum, row) => sum + (row.naturalCompartmentCount || 0), 0);
const totalTargetNaturalCompartmentCount = perRegion.reduce((sum, row) => sum + (row.targetNaturalCompartmentCount || 0), 0);
const weightedCompartmentArea = perRegion.reduce((sum, row) => sum + (row.averageCompartmentArea || 0) * (row.naturalCompartmentCount || 0), 0);
@ -1045,10 +1195,14 @@ export function generateAdminLayout(context) {
const adminDebug = {
multiRegionAdmin: true,
adminRegionCount: perRegion.length,
minFullAdminRegionArea,
perRegion,
finalMunicipalityCount: totalMunicipalityCount,
actualMunicipalityCount: totalMunicipalityCount,
candidateSeedCount: combinedCenters.length,
municipalOfficePointCount: combinedCenters.length,
generatedOfficePointCount: compactedAdmin.generatedOfficePointCount,
removedUnusedAdminCenterCount: compactedAdmin.removedUnusedAdminCenterCount,
naturalCompartmentCount: totalNaturalCompartmentCount,
compartmentCount: totalNaturalCompartmentCount,
targetNaturalCompartmentCount: totalTargetNaturalCompartmentCount,