test+feat: depth-slice map validation + tune cave thresholds to target
Add geological depth slices to the automated GameTest: it now samples the latitude world at three depths and renders one PNG per depth (a top-down 'view' of that Y layer): - latitude_map_surface.png (Y=64) - latitude_map_mid_cave.png (Y=-15) - latitude_map_deep.png (Y=-50) plus a report section per slice and new assertions: - Deep Dark must stay rare in the deep zone (<8%) - cave biomes (lush/dripstone) must stay a minority in mid-cave (<25%) Cave biome colors are now distinct (lush=green, dripstone=orange, deep_dark= teal) so the slices are readable. Tuned the cave thresholds using the test as a fast feedback loop (BiomeSource- only sampling, ~30s per run, mods-independent): - CAVE_THRESHOLD 0.55 -> 0.38 (lush/dripstone pockets now ~6.5%) - DEEP_DARK_THRESHOLD 0.88 -> 0.55 (Deep Dark now legendary ~1.3%; was 0%) Validated with BOP + Tectonic: spawn=birch_forest | north=100% | south=100% | swamp=1.26% | deepDark=1.30% | caveBiomes=6.5%
This commit is contained in:
@@ -95,8 +95,8 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
/** Frequency of the mid-cave pocket noise (lush/dripstone). */
|
||||
private static final double CAVE_SCALE = 0.003;
|
||||
|
||||
/** Above this value a lush/dripstone pocket overrides the surface biome. ~8% of the mid zone. */
|
||||
private static final double CAVE_THRESHOLD = 0.55;
|
||||
/** Above this value a lush/dripstone pocket overrides the surface biome. ~5-8% of the mid zone. */
|
||||
private static final double CAVE_THRESHOLD = 0.38;
|
||||
|
||||
/** Frequency of the Deep Dark noise (very low = large, rare regions). */
|
||||
private static final double DEEP_DARK_SCALE = 0.0011;
|
||||
@@ -104,8 +104,8 @@ public class LatitudeBiomeSource extends BiomeSource {
|
||||
/** Vertical frequency of the Deep Dark noise (keeps it coherent in tall sections). */
|
||||
private static final double DEEP_DARK_SCALE_Y = 0.012;
|
||||
|
||||
/** Above this value the Deep Dark overrides. Tuned for ~1% of the deep zone (legendary). */
|
||||
private static final double DEEP_DARK_THRESHOLD = 0.88;
|
||||
/** Above this value the Deep Dark overrides. Tuned for ~1-2% of the deep zone (legendary). */
|
||||
private static final double DEEP_DARK_THRESHOLD = 0.55;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Spawn safe zone
|
||||
|
||||
@@ -29,9 +29,9 @@ import java.util.Set;
|
||||
* {@link GameTest} so it executes inside a real game server with the full biome registry.
|
||||
*
|
||||
* <p>Launch with {@code ./gradlew runGameTestServer}. The test samples a large grid through a
|
||||
* freshly built {@link LatitudeBiomeSource}, renders a PNG map, writes a distribution report and
|
||||
* asserts the climate invariants (frozen north, hot south, safe spawn, rare swamp). No manual
|
||||
* in-game testing required.</p>
|
||||
* freshly built {@link LatitudeBiomeSource} at several depths and renders one PNG map per depth
|
||||
* (a "geological slice" view), writes a distribution report and asserts the climate + cave
|
||||
* invariants. No manual in-game testing required.</p>
|
||||
*/
|
||||
@GameTestHolder(CustomOreGenMod.MODID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
@@ -42,6 +42,11 @@ public class LatitudeGameTest {
|
||||
private static final long SEED = 0L;
|
||||
private static final Path OUT_DIR = Path.of("gametest-results", "latitude");
|
||||
|
||||
// Sampling depths (one PNG per depth = a top-down "slice").
|
||||
private static final int SURFACE_Y = 64;
|
||||
private static final int MID_CAVE_Y = -15; // -30 <= Y < 0 (lush/dripstone pockets)
|
||||
private static final int DEEP_Y = -50; // Y < -30 (legendary Deep Dark)
|
||||
|
||||
private static final Map<ResourceKey<Biome>, Integer> COLORS = new HashMap<>();
|
||||
|
||||
static {
|
||||
@@ -61,7 +66,10 @@ public class LatitudeGameTest {
|
||||
Biomes.JUNGLE, Biomes.SPARSE_JUNGLE, Biomes.BAMBOO_JUNGLE, Biomes.WARM_OCEAN);
|
||||
col(0x5B7A3A, Biomes.SWAMP);
|
||||
col(0x3A5A3A, Biomes.MANGROVE_SWAMP);
|
||||
col(0x6B4E3A, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK);
|
||||
// Cave biomes - distinct colors so the slices are readable.
|
||||
col(0x4DB05A, Biomes.LUSH_CAVES); // lush green
|
||||
col(0xC8782C, Biomes.DRIPSTONE_CAVES); // stalactite orange-brown
|
||||
col(0x0A4D54, Biomes.DEEP_DARK); // dark sculk teal
|
||||
}
|
||||
|
||||
private static void col(int rgb, ResourceKey<Biome>... keys) {
|
||||
@@ -85,6 +93,85 @@ public class LatitudeGameTest {
|
||||
LatitudeBiomeSource source = new LatitudeBiomeSource(SEED, getter);
|
||||
|
||||
Files.createDirectories(OUT_DIR);
|
||||
|
||||
// ---- Sample one slice per depth. Each slice produces a PNG + a biome-count map. ----
|
||||
Slice surface = sampleSlice(source, SURFACE_Y, "latitude_map_surface.png");
|
||||
Slice midCave = sampleSlice(source, MID_CAVE_Y, "latitude_map_mid_cave.png");
|
||||
Slice deep = sampleSlice(source, DEEP_Y, "latitude_map_deep.png");
|
||||
|
||||
// ---- Surface climate assertions ----
|
||||
ResourceKey<Biome> spawnKey = source.getNoiseBiome(0, SURFACE_Y >> 2, 0, null).unwrapKey().orElse(null);
|
||||
|
||||
Set<ResourceKey<Biome>> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag());
|
||||
Set<ResourceKey<Biome>> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag());
|
||||
Set<ResourceKey<Biome>> coldOceans = new HashSet<>(List.of(
|
||||
Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN,
|
||||
Biomes.FROZEN_RIVER));
|
||||
Set<ResourceKey<Biome>> hotOceans = new HashSet<>(List.of(
|
||||
Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN));
|
||||
|
||||
assertNotNull(spawnKey, "spawn biome resolved");
|
||||
assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey);
|
||||
|
||||
Set<ResourceKey<Biome>> northExpected = new HashSet<>();
|
||||
northExpected.addAll(coldTag);
|
||||
northExpected.addAll(coldOceans);
|
||||
double north = sliceBandRatio(surface, BiomeBand.FROZEN, northExpected);
|
||||
assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen (by tag), was %.1f%%".formatted(north));
|
||||
|
||||
Set<ResourceKey<Biome>> southExpected = new HashSet<>();
|
||||
southExpected.addAll(hotTag);
|
||||
southExpected.addAll(hotOceans);
|
||||
southExpected.add(Biomes.MANGROVE_SWAMP);
|
||||
double south = sliceBandRatio(surface, BiomeBand.HOT, southExpected);
|
||||
assertTrue(south > 60.0, "HOT band should be >60%% warm/hot (by tag), was %.1f%%".formatted(south));
|
||||
|
||||
// Swamp stays rare at the surface in the temperate band.
|
||||
double swampRatio = biomeRatioInBand(surface, BiomeBand.TEMPERATE, Biomes.SWAMP);
|
||||
assertTrue(swampRatio < 15.0, "swamp must be <15%% of temperate band, was %.2f%%".formatted(swampRatio));
|
||||
|
||||
// ---- Cave assertions ----
|
||||
// Deep Dark is legendary: a small fraction of the deep slice only.
|
||||
double deepDarkRatio = biomeRatio(deep, Biomes.DEEP_DARK);
|
||||
assertTrue(deepDarkRatio < 8.0,
|
||||
"Deep Dark must be rare in the deep zone (<8%%), was %.2f%%".formatted(deepDarkRatio));
|
||||
|
||||
// Cave biomes (lush/dripstone) should be a minority even in the mid-cave slice - the latitude
|
||||
// biome dominates underground by design.
|
||||
double caveBiomeRatio = biomeRatio(midCave, Biomes.LUSH_CAVES, Biomes.DRIPSTONE_CAVES);
|
||||
assertTrue(caveBiomeRatio < 25.0,
|
||||
"cave biomes must stay a minority in mid-cave (<25%%), was %.2f%%".formatted(caveBiomeRatio));
|
||||
|
||||
// ---- Report ----
|
||||
StringBuilder r = new StringBuilder();
|
||||
r.append("Latitude GameTest Report (seed=").append(SEED).append(")\n\n");
|
||||
r.append("Spawn @ (0,0): ").append(spawnKey).append("\n\n");
|
||||
|
||||
r.append("=== Depth slices (PNG) ===\n");
|
||||
r.append(String.format(" Surface Y=%3d : latitude_map_surface.png%n", SURFACE_Y));
|
||||
r.append(String.format(" Mid cave Y=%3d: latitude_map_mid_cave.png%n", MID_CAVE_Y));
|
||||
r.append(String.format(" Deep Y=%3d : latitude_map_deep.png%n%n", DEEP_Y));
|
||||
|
||||
r.append("=== Surface top biomes (top 12) ===\n");
|
||||
appendTop(r, surface.global, 12);
|
||||
|
||||
r.append("\n=== Mid cave (Y=").append(MID_CAVE_Y).append(") top biomes (top 10) ===\n");
|
||||
appendTop(r, midCave.global, 10);
|
||||
|
||||
r.append("\n=== Deep (Y=").append(DEEP_Y).append(") top biomes (top 10) ===\n");
|
||||
appendTop(r, deep.global, 10);
|
||||
|
||||
Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString());
|
||||
|
||||
// ---- One-line summary ----
|
||||
CustomOreGenMod.LOGGER.info(String.format(
|
||||
"Latitude game test OK: spawn=%s | north=%.0f%% | south=%.0f%% | swamp=%.2f%% | "
|
||||
+ "deepDark=%.2f%% | caveBiomes=%.1f%%",
|
||||
spawnKey, north, south, swampRatio, deepDarkRatio, caveBiomeRatio));
|
||||
}
|
||||
|
||||
/** Sample the whole grid at a single Y and write a PNG; returns the slice's biome counts. */
|
||||
private static Slice sampleSlice(LatitudeBiomeSource source, int blockY, String pngName) throws Exception {
|
||||
int dim = (2 * RADIUS) / STEP + 1;
|
||||
BufferedImage img = new BufferedImage(dim, dim, BufferedImage.TYPE_INT_RGB);
|
||||
Map<ResourceKey<Biome>, Integer> global = new HashMap<>();
|
||||
@@ -95,89 +182,31 @@ public class LatitudeGameTest {
|
||||
for (int xi = 0; xi < dim; xi++) {
|
||||
int blockX = -RADIUS + xi * STEP;
|
||||
int blockZ = -RADIUS + zi * STEP;
|
||||
Holder<Biome> holder = source.getNoiseBiome(blockX >> 2, 64 >> 2, blockZ >> 2, null);
|
||||
Holder<Biome> holder = source.getNoiseBiome(blockX >> 2, blockY >> 2, blockZ >> 2, null);
|
||||
ResourceKey<Biome> key = holder.unwrapKey().orElse(null);
|
||||
|
||||
global.merge(key, 1, Integer::sum);
|
||||
perBand.get(BiomeBand.fromTemperature(blockZ / 16000.0)).merge(key, 1, Integer::sum);
|
||||
|
||||
int rgb = key == null ? 0x888888 : COLORS.getOrDefault(key, 0x888888);
|
||||
img.setRGB(xi, dim - 1 - zi, rgb);
|
||||
img.setRGB(xi, dim - 1 - zi, rgb); // flip Z so north is at the top
|
||||
}
|
||||
}
|
||||
|
||||
ImageIO.write(img, "png", OUT_DIR.resolve("latitude_map.png").toFile());
|
||||
|
||||
ResourceKey<Biome> spawnKey = source.getNoiseBiome(0, 64 >> 2, 0, null).unwrapKey().orElse(null);
|
||||
|
||||
// Resolve the three surface tags so assertions can use tag membership (the source of
|
||||
// truth) instead of a hardcoded vanilla-only biome list. This correctly counts BOP biomes.
|
||||
Set<ResourceKey<Biome>> coldTag = resolveTagKeys(getter, BiomeBand.COLD.surfaceTag());
|
||||
Set<ResourceKey<Biome>> temperateTag = resolveTagKeys(getter, BiomeBand.TEMPERATE.surfaceTag());
|
||||
Set<ResourceKey<Biome>> hotTag = resolveTagKeys(getter, BiomeBand.HOT.surfaceTag());
|
||||
Set<ResourceKey<Biome>> coldOceans = new HashSet<>(List.of(
|
||||
Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN, Biomes.COLD_OCEAN, Biomes.DEEP_COLD_OCEAN,
|
||||
Biomes.FROZEN_RIVER));
|
||||
Set<ResourceKey<Biome>> hotOceans = new HashSet<>(List.of(
|
||||
Biomes.WARM_OCEAN, Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN));
|
||||
|
||||
StringBuilder r = new StringBuilder();
|
||||
r.append("Latitude GameTest Report (seed=").append(SEED).append(")\n\n");
|
||||
r.append("Spawn @ (0,0): ").append(spawnKey).append("\n\n");
|
||||
int total = global.values().stream().mapToInt(Integer::intValue).sum();
|
||||
r.append("Global biome distribution (top 20):\n");
|
||||
global.entrySet().stream()
|
||||
.sorted(Map.Entry.<ResourceKey<Biome>, Integer>comparingByValue().reversed())
|
||||
.limit(20)
|
||||
.forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total)));
|
||||
|
||||
// Per-band breakdown for diagnostics.
|
||||
for (BiomeBand band : BiomeBand.values()) {
|
||||
Map<ResourceKey<Biome>, Integer> m = perBand.get(band);
|
||||
int bt = m.values().stream().mapToInt(Integer::intValue).sum();
|
||||
if (bt == 0) continue;
|
||||
r.append("\nBand ").append(band).append(" (").append(bt).append(" samples):\n");
|
||||
m.entrySet().stream()
|
||||
.sorted(Map.Entry.<ResourceKey<Biome>, Integer>comparingByValue().reversed())
|
||||
.limit(8)
|
||||
.forEach(e -> r.append(String.format(" %-43s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / bt)));
|
||||
}
|
||||
Files.writeString(OUT_DIR.resolve("latitude_report.txt"), r.toString());
|
||||
|
||||
// ---- Assertions (tag-based: correctly counts vanilla + BOP biomes) ----
|
||||
assertNotNull(spawnKey, "spawn biome resolved");
|
||||
assertTrue(isSafeSpawn(spawnKey), "spawn must be a safe biome, was " + spawnKey);
|
||||
|
||||
Set<ResourceKey<Biome>> northExpected = new HashSet<>();
|
||||
northExpected.addAll(coldTag);
|
||||
northExpected.addAll(coldOceans);
|
||||
Set<ResourceKey<Biome>> undergroundCold = new HashSet<>(List.of(
|
||||
Biomes.DRIPSTONE_CAVES, Biomes.DEEP_DARK));
|
||||
northExpected.addAll(undergroundCold);
|
||||
double north = bandRatio(perBand.get(BiomeBand.FROZEN), northExpected);
|
||||
assertTrue(north > 60.0, "FROZEN band should be >60%% cold/frozen (by tag), was %.1f%%".formatted(north));
|
||||
|
||||
Set<ResourceKey<Biome>> southExpected = new HashSet<>();
|
||||
southExpected.addAll(hotTag);
|
||||
southExpected.addAll(hotOceans);
|
||||
southExpected.add(Biomes.MANGROVE_SWAMP);
|
||||
double south = bandRatio(perBand.get(BiomeBand.HOT), southExpected);
|
||||
assertTrue(south > 60.0, "HOT band should be >60%% warm/hot (by tag), was %.1f%%".formatted(south));
|
||||
|
||||
Map<ResourceKey<Biome>, Integer> temperate = perBand.get(BiomeBand.TEMPERATE);
|
||||
int temperateTotal = temperate.values().stream().mapToInt(Integer::intValue).sum();
|
||||
int temperateSwamp = temperate.getOrDefault(Biomes.SWAMP, 0);
|
||||
double swampRatio = 100.0 * temperateSwamp / temperateTotal;
|
||||
assertTrue(swampRatio < 15.0,
|
||||
"swamp must be <15%% of temperate band, was %.2f%%".formatted(swampRatio));
|
||||
|
||||
// Log a one-line summary so it shows up in the game test output.
|
||||
CustomOreGenMod.LOGGER.info(String.format(
|
||||
"Latitude game test OK: spawn=%s | north=%.0f%% | south=%.0f%% | swamp=%.2f%%",
|
||||
spawnKey, north, south, swampRatio));
|
||||
ImageIO.write(img, "png", OUT_DIR.resolve(pngName).toFile());
|
||||
return new Slice(global, perBand);
|
||||
}
|
||||
|
||||
private static double bandRatio(Map<ResourceKey<Biome>, Integer> m, Set<ResourceKey<Biome>> expected) {
|
||||
private static void appendTop(StringBuilder r, Map<ResourceKey<Biome>, Integer> counts, int limit) {
|
||||
int total = counts.values().stream().mapToInt(Integer::intValue).sum();
|
||||
counts.entrySet().stream()
|
||||
.sorted(Map.Entry.<ResourceKey<Biome>, Integer>comparingByValue().reversed())
|
||||
.limit(limit)
|
||||
.forEach(e -> r.append(String.format(" %-45s %6.2f%%%n", e.getKey(), 100.0 * e.getValue() / total)));
|
||||
}
|
||||
|
||||
/** Ratio of a set of biomes within a single band of a slice. */
|
||||
private static double sliceBandRatio(Slice slice, BiomeBand band, Set<ResourceKey<Biome>> expected) {
|
||||
Map<ResourceKey<Biome>, Integer> m = slice.perBand.get(band);
|
||||
int total = m.values().stream().mapToInt(Integer::intValue).sum();
|
||||
if (total == 0) return 0;
|
||||
int match = 0;
|
||||
@@ -185,6 +214,23 @@ public class LatitudeGameTest {
|
||||
return 100.0 * match / total;
|
||||
}
|
||||
|
||||
/** Ratio of one or more biomes across a whole slice. */
|
||||
private static double biomeRatio(Slice slice, ResourceKey<Biome>... keys) {
|
||||
int total = slice.global.values().stream().mapToInt(Integer::intValue).sum();
|
||||
if (total == 0) return 0;
|
||||
int match = 0;
|
||||
for (ResourceKey<Biome> k : keys) match += slice.global.getOrDefault(k, 0);
|
||||
return 100.0 * match / total;
|
||||
}
|
||||
|
||||
/** Ratio of a biome within a single band of a slice. */
|
||||
private static double biomeRatioInBand(Slice slice, BiomeBand band, ResourceKey<Biome> key) {
|
||||
Map<ResourceKey<Biome>, Integer> m = slice.perBand.get(band);
|
||||
int total = m.values().stream().mapToInt(Integer::intValue).sum();
|
||||
if (total == 0) return 0;
|
||||
return 100.0 * m.getOrDefault(key, 0) / total;
|
||||
}
|
||||
|
||||
/** Resolve all biome keys that belong to a tag, via the real registry. */
|
||||
private static Set<ResourceKey<Biome>> resolveTagKeys(HolderGetter<Biome> getter, net.minecraft.tags.TagKey<Biome> tag) {
|
||||
Set<ResourceKey<Biome>> keys = new HashSet<>();
|
||||
@@ -197,6 +243,11 @@ public class LatitudeGameTest {
|
||||
Biomes.FLOWER_FOREST, Biomes.MEADOW, Biomes.CHERRY_GROVE).contains(key);
|
||||
}
|
||||
|
||||
/** A sampled depth slice: global counts + per-band counts. */
|
||||
private record Slice(Map<ResourceKey<Biome>, Integer> global,
|
||||
Map<BiomeBand, Map<ResourceKey<Biome>, Integer>> perBand) {
|
||||
}
|
||||
|
||||
// Local assert helpers (no JUnit in the main classpath at runtime).
|
||||
private static void assertTrue(boolean condition, String msg) {
|
||||
if (!condition) throw new AssertionError(msg);
|
||||
|
||||
Reference in New Issue
Block a user