clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/regenerate-vhs-gifs
puzzletea / pdfexport / render_cover_test.go
render_cover_test.go 6.8K
1package pdfexport23import (4 "math"5 "reflect"6 "testing"78 "codeberg.org/go-pdf/fpdf"9)1011func TestSplitCoverSubtitleLinesClampsToMaxLines(t *testing.T) {12 pdf := fpdf.New("P", "mm", "A4", "")13 if err := registerPDFFonts(pdf); err != nil {14 t.Fatalf("registerPDFFonts error: %v", err)15 }16 pdf.AddPage()17 pdf.SetFont(coverFontFamily, "", 18)1819 subtitle := "The Catacombs of the Last Bastion and Other Grim Architectural Delights"20 maxW := 78.021 got := splitCoverSubtitleLines(pdf, subtitle, maxW, 2)22 if len(got) != 2 {23 t.Fatalf("line count = %d, want 2 (%v)", len(got), got)24 }2526 gotAgain := splitCoverSubtitleLines(pdf, subtitle, maxW, 2)27 if !reflect.DeepEqual(got, gotAgain) {28 t.Fatalf("splitCoverSubtitleLines not stable: %v vs %v", got, gotAgain)29 }30}3132func TestSplitClampedTextLinesClampsToMaxLines(t *testing.T) {33 pdf := fpdf.New("P", "mm", "A4", "")34 if err := registerPDFFonts(pdf); err != nil {35 t.Fatalf("registerPDFFonts error: %v", err)36 }37 pdf.AddPage()38 pdf.SetFont(sansFontFamily, "", 10)3940 text := "This is a deliberately long advert line that should wrap across multiple lines in the PDF renderer."41 maxW := 55.042 got := splitClampedTextLines(pdf, text, maxW, 2)43 if len(got) != 2 {44 t.Fatalf("line count = %d, want 2 (%v)", len(got), got)45 }4647 gotAgain := splitClampedTextLines(pdf, text, maxW, 2)48 if !reflect.DeepEqual(got, gotAgain) {49 t.Fatalf("splitClampedTextLines not stable: %v vs %v", got, gotAgain)50 }51}5253func TestBuildCoverArtLayoutIsDeterministicForSameSeed(t *testing.T) {54 cfg := RenderConfig{55 ShuffleSeed: "zine-seed-01",56 VolumeNumber: 4,57 }5859 layoutA := buildCoverArtLayout(cfg, halfLetterWidthMM, halfLetterHeightMM)60 layoutB := buildCoverArtLayout(cfg, halfLetterWidthMM, halfLetterHeightMM)61 if !coverArtLayoutEqual(layoutA, layoutB) {62 t.Fatalf("cover layout should be stable for identical input: %#v vs %#v", layoutA, layoutB)63 }64 if !coverArtStaysWithinArea(layoutA) {65 t.Fatalf("cover layout escapes art area: %#v", layoutA)66 }67 if !coverArtRespectsLockup(layoutA) {68 t.Fatalf("cover layout overlaps lockup exclusion: %#v", layoutA)69 }70 if coverRotationSignature(layoutA) != coverRotationSignature(layoutB) {71 t.Fatalf("rotation signature changed: %q vs %q", coverRotationSignature(layoutA), coverRotationSignature(layoutB))72 }73}7475func TestBuildCoverArtLayoutChangesWhenSeedChanges(t *testing.T) {76 base := RenderConfig{77 ShuffleSeed: "zine-seed-01",78 VolumeNumber: 4,79 }80 other := RenderConfig{81 ShuffleSeed: "zine-seed-02",82 VolumeNumber: 4,83 }8485 layoutA := buildCoverArtLayout(base, halfLetterWidthMM, halfLetterHeightMM)86 layoutB := buildCoverArtLayout(other, halfLetterWidthMM, halfLetterHeightMM)87 if !coverArtDiffers(layoutA, layoutB) {88 t.Fatalf("cover layout should differ when seed changes: %#v", layoutA)89 }90 if coverRotationSignature(layoutA) == coverRotationSignature(layoutB) {91 t.Fatalf("rotation signature should differ when seed changes: %q", coverRotationSignature(layoutA))92 }93}9495func TestBuildCoverArtLayoutUsesMultipleTextureFamilies(t *testing.T) {96 cfg := RenderConfig{97 ShuffleSeed: "texture-seed",98 VolumeNumber: 2,99 }100101 layout := buildCoverArtLayout(cfg, halfLetterWidthMM, halfLetterHeightMM)102 if got := coverTextureCount(layout); got < 1 {103 t.Fatalf("texture count = %d, want at least 1", got)104 }105106 bounds := coverArtInkBounds(layout)107 if bounds.w <= 0 || bounds.h <= 0 {108 t.Fatalf("ink bounds = %#v, want positive extents", bounds)109 }110}111112func TestCoverSeedCorpusProducesMultipleSilhouetteFamilies(t *testing.T) {113 seeds := []string{114 "near-seed-00",115 "near-seed-01",116 "near-seed-02",117 "near-seed-03",118 "near-seed-04",119 "near-seed-05",120 "near-seed-06",121 "near-seed-07",122 "near-seed-08",123 "near-seed-09",124 }125126 families := map[string]struct{}{}127 aspectBuckets := map[string]struct{}{}128 directions := map[string]struct{}{}129 mixes := map[string]struct{}{}130 rotations := map[string]struct{}{}131 polarities := map[string]struct{}{}132133 for _, seed := range seeds {134 layout := buildCoverArtLayout(RenderConfig{135 ShuffleSeed: seed,136 VolumeNumber: 1,137 }, halfLetterWidthMM, halfLetterHeightMM)138139 if !coverArtStaysWithinArea(layout) {140 t.Fatalf("layout for %q escapes art area: %#v", seed, layout)141 }142 if !coverArtRespectsLockup(layout) {143 t.Fatalf("layout for %q overlaps lockup exclusion: %#v", seed, layout)144 }145146 families[coverFamilyName(layout.Family)] = struct{}{}147 aspectBuckets[coverAspectBucketName(coverInkAspectBucket(layout))] = struct{}{}148 directions[coverDirectionName(layout.Direction)] = struct{}{}149 mixes[coverPrimitiveMixSignature(layout)] = struct{}{}150 rotations[coverRotationSignature(layout)] = struct{}{}151 polarities[coverRotationPolaritySignature(layout)] = struct{}{}152 }153154 if got := len(families); got < 3 {155 t.Fatalf("family count = %d, want at least 3", got)156 }157 if got := len(aspectBuckets); got < 2 {158 t.Fatalf("aspect bucket count = %d, want at least 2", got)159 }160 if got := len(directions); got < 3 {161 t.Fatalf("direction count = %d, want at least 3", got)162 }163 if got := len(mixes); got < 3 {164 t.Fatalf("primitive mix count = %d, want at least 3", got)165 }166 if got := len(rotations); got < 4 {167 t.Fatalf("rotation signature count = %d, want at least 4", got)168 }169 if got := len(polarities); got < 2 {170 t.Fatalf("rotation polarity count = %d, want at least 2", got)171 }172}173174func TestRotatedRectAABBNinetyDegreesSwapsExtents(t *testing.T) {175 bounds := rectMM{x: 10, y: 20, w: 20, h: 10}176 aabb := rotatedRectAABB(bounds, 20, 25, 90)177 if math.Abs(aabb.w-10) > 0.001 {178 t.Fatalf("aabb.w = %.3f, want 10", aabb.w)179 }180 if math.Abs(aabb.h-20) > 0.001 {181 t.Fatalf("aabb.h = %.3f, want 20", aabb.h)182 }183 if math.Abs(aabb.x-15) > 0.001 || math.Abs(aabb.y-15) > 0.001 {184 t.Fatalf("aabb origin = (%.3f, %.3f), want (15, 15)", aabb.x, aabb.y)185 }186}187188func TestCoverRotationAngleBandRespectsStructuralFlag(t *testing.T) {189 seeds := []string{190 "rotation-band-00",191 "rotation-band-01",192 "rotation-band-02",193 "rotation-band-03",194 "rotation-band-04",195 "rotation-band-05",196 }197198 seenPositive := false199 seenNegative := false200 seenStructural := false201 for _, seed := range seeds {202 layout := buildCoverArtLayout(RenderConfig{203 ShuffleSeed: seed,204 VolumeNumber: 1,205 }, halfLetterWidthMM, halfLetterHeightMM)206207 for _, shape := range layout.Shapes {208 absAngle := math.Abs(shape.Rotate)209 if shape.Rotate > 0 {210 seenPositive = true211 }212 if shape.Rotate < 0 {213 seenNegative = true214 }215 if shape.Locked {216 seenStructural = true217 if absAngle < 4 || absAngle > 10 {218 t.Fatalf("structural angle = %.2f, want within [4,10]", absAngle)219 }220 continue221 }222 if absAngle < 8 || absAngle > 20 {223 t.Fatalf("standard angle = %.2f, want within [8,20]", absAngle)224 }225 }226 }227228 if !seenStructural {229 t.Fatal("expected at least one structural shape across seed corpus")230 }231 if !seenPositive || !seenNegative {232 t.Fatalf("expected both positive and negative rotations, got positive=%t negative=%t", seenPositive, seenNegative)233 }234}