From ea0e3b2ad0182445d2678470153aa198d54392e5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 20:36:48 +0200 Subject: [PATCH] test: cover testTileTexture --- tests/testTileTexture.cpp | 152 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/tests/testTileTexture.cpp b/tests/testTileTexture.cpp index fcba8f7..7e3c682 100644 --- a/tests/testTileTexture.cpp +++ b/tests/testTileTexture.cpp @@ -149,3 +149,155 @@ VF_TEST(theAtlasDoesNotDependOnTheOrderTilesAreGenerated) { CHECK(forwards == backwards); } +// The opaque pass draws without blending, so an opaque tile with a hole in it +// would put a stale colour on screen rather than a hole. +VF_TEST(opaqueTilesAreFullyOpaqueInEveryPixel) { + TileSampler sampler; + int size = sampler.params().tileSize; + for (int tile = 0; tile < renderedTiles(sampler); ++tile) { + if (sampler.palette().specFor(tile).cutout) continue; + int transparent = 0; + for (int y = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + if (sampler.pixelAt(tile, x, y).a != 255) ++transparent; + } + } + CHECK_EQ(transparent, 0); + if (transparent != 0) { + std::printf(" tile %d (%s) has %d see-through pixels\n", tile, + sampler.palette().specFor(tile).name, transparent); + } + } +} + +// A cutout tile that is solid everywhere is not a shape, and one that is hollow +// everywhere is not a tile. Both ends have to be present in the alpha channel. +VF_TEST(cutoutTilesCarryBothHolesAndSolidPixels) { + TileSampler sampler; + int size = sampler.params().tileSize; + int cutouts = 0; + for (int tile = 0; tile < renderedTiles(sampler); ++tile) { + const TileSpec& spec = sampler.palette().specFor(tile); + if (!spec.cutout) continue; + ++cutouts; + int holes = 0; + int solid = 0; + for (int y = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + uint8_t alpha = sampler.pixelAt(tile, x, y).a; + if (alpha == 0) ++holes; + if (alpha == 255) ++solid; + } + } + CHECK(holes > 0); + CHECK(solid > 0); + CHECK_EQ(holes + solid, size * size); + if (holes == 0 || solid == 0) { + std::printf(" tile %d (%s): %d holes, %d solid\n", tile, spec.name, holes, solid); + } + } + CHECK(cutouts >= 20); +} + +// The failure this whole wave exists to stop: a tile that renders as one flat +// plate of colour. Measured on the interior so the edge darkening cannot pass a +// flat tile off as textured. +VF_TEST(everyTileVariesAcrossItsInterior) { + TileSampler sampler; + const int minimumSpread = 12; + for (int tile = 0; tile < renderedTiles(sampler); ++tile) { + LuminanceRange range = interiorRange(sampler, tile); + CHECK(range.spread() >= minimumSpread); + if (range.spread() < minimumSpread) { + std::printf(" tile %d (%s) is flat: spread %d\n", tile, + sampler.palette().specFor(tile).name, range.spread()); + } + } +} + +// Brick courses are offset by half a brick. Without the offset every joint +// lines up and a brick wall reads as a grid, so the mortar columns of one +// course must not repeat in the next. +VF_TEST(brickCoursesShiftTheirJointsBetweenRows) { + TileSampler sampler; + TileSpec synthetic = syntheticBrick(); + int layouts = distinctJointLayouts(sampler, synthetic, 500); + CHECK(layouts >= 2); + if (layouts < 2) std::printf(" synthetic brick has %d joint layouts\n", layouts); + + // The two courses that actually sit next to each other, spelled out. + std::vector firstCourse = accentColumns(sampler, synthetic, 500, 2); + std::vector secondCourse = accentColumns(sampler, synthetic, 500, 6); + CHECK(!firstCourse.empty()); + CHECK(!secondCourse.empty()); + CHECK(firstCourse != secondCourse); + + for (int tile = 0; tile < renderedTiles(sampler); ++tile) { + const TileSpec& spec = sampler.palette().specFor(tile); + if (spec.pattern != TilePattern::Brick) continue; + int real = distinctJointLayouts(sampler, withoutGrain(spec), tile); + CHECK(real >= 2); + if (real < 2) std::printf(" tile %d (%s) has %d joint layouts\n", tile, spec.name, real); + } +} + +// Contrast is a real knob, not a field nobody reads: raising it has to widen +// what the tile actually puts on screen. +VF_TEST(raisingContrastWidensTheLuminanceRange) { + TileTextureParams plain; + TileTextureParams punchy; + punchy.contrast = 2.0f; + TileSampler flat(plain); + TileSampler bold(punchy); + + int widened = 0; + for (int tile = 0; tile < renderedTiles(flat); ++tile) { + int quiet = interiorRange(flat, tile).spread(); + int loud = interiorRange(bold, tile).spread(); + CHECK(loud >= quiet); + if (loud > quiet) ++widened; + } + CHECK(widened > tileCount / 2); +} + +// The seed reaches the grain, and only the grain: the palette a sampler reports +// is the same table whatever the seed is. +VF_TEST(changingTheNoiseSeedRedrawsTheGrainButNotThePalette) { + TileTextureParams first; + TileTextureParams second; + second.noiseSeed = first.noiseSeed ^ 0x5bf03635u; + TileSampler original(first); + TileSampler reseeded(second); + + int changed = 0; + for (int tile = 0; tile < renderedTiles(original); ++tile) { + if (renderTile(original, tile) != renderTile(reseeded, tile)) ++changed; + CHECK(original.palette().specFor(tile).base == reseeded.palette().specFor(tile).base); + CHECK(original.palette().specFor(tile).accent == reseeded.palette().specFor(tile).accent); + } + CHECK(changed > tileCount * 3 / 4); + + TileSampler sameSeedAgain(first); + CHECK(renderAtlas(original, false) == renderAtlas(sameSeedAgain, false)); +} + +// writeTile is the path render/Atlas.cpp takes and pixelAt is the path the +// tests take. They have to be the same generator. +VF_TEST(writeTileAgreesWithPixelAtEverywhere) { + TileSampler sampler; + int size = sampler.params().tileSize; + for (int tile = 0; tile < renderedTiles(sampler); ++tile) { + std::vector bytes = renderTile(sampler, tile); + for (int y = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + TilePixel texel = sampler.pixelAt(tile, x, y); + std::size_t offset = (static_cast(y) * size + x) * 4; + CHECK_EQ(bytes[offset + 0], texel.r); + CHECK_EQ(bytes[offset + 1], texel.g); + CHECK_EQ(bytes[offset + 2], texel.b); + CHECK_EQ(bytes[offset + 3], texel.a); + } + } + } +} +