settler_test.gno
5.83 Kb · 152 lines
1package art
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/uassert/v0"
8)
9
10// Settler #25, read from mainnet on 2026-09-29 and transcribed one character
11// per palette index (0-9 then a-j). 32x32, 20 colours.
12//
13// It is here because the package's central claim is about real indexed art and
14// a synthetic two-colour fixture cannot test it: the interesting thing about a
15// sprite is how many near-luminance colours it carries, and that is a property
16// of art somebody drew.
17//
18// gno.land/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft:token/25
19// The Settlers art is dedicated to the public domain under CC0 1.0.
20var settler25Pal = Palette{"#f3e3c9", "#2a1a2e", "#ffb45a", "#b5601c", "#f08a2a", "#b57650", "#d99a6c", "#7f5234", "#1b1626", "#f08c7c", "#ffb0a0", "#c8645a", "#a8744e", "#573622", "#5fb0e8", "#2b649a", "#3f8fd1", "#6b3f22", "#6a4126", "#46271a"}
21
22var settler25Rows = []string{
23 "00000000000000011000000000000000",
24 "00000000000000123100000000000000",
25 "00000000000000144100000000000000",
26 "00000000000001244310000000000000",
27 "00000000000001444410000000000000",
28 "00000000000012444431000000000000",
29 "00000000000014444441000000000000",
30 "00000000000124444443100000000000",
31 "00000000000144444444100000000000",
32 "00000000001244444444310000000000",
33 "00000000001444444444410000000000",
34 "00000000124444444444443100000000",
35 "00000000133333333333333100000000",
36 "00000000001555555555510000000000",
37 "00000000001677766777610000000000",
38 "00000000016686666668661000000000",
39 "00000000156686699668665100000000",
40 "00000000001666a99b66610000000000",
41 "0000000001c777bbbb777d1000000000",
42 "0000000011c7777777777d1100000000",
43 "00000001e1c777d77d777d1f10000000",
44 "00000001eg1c77d77d77d1gf10000000",
45 "00000001eg1c7d7777d7d1gf10000000",
46 "00000016egg1c7d77d7d1ggf61000000",
47 "00000015egg1c777777d1ggf51000000",
48 "00000001hhhh1c7dd7d1hhhh10000000",
49 "00000001egggg1c77d1ggggf10000000",
50 "00000001ffffff1cd1ffffff10000000",
51 "0000000001iiiii11iiiii1000000000",
52 "0000000001iiiii11iiiii1000000000",
53 "0000000001jjjjj11jjjjj1000000000",
54 "00000000011111111111111000000000",
55}
56
57func settler25() *Pix {
58 p := NewPix(len(settler25Rows[0]), len(settler25Rows), settler25Pal)
59 for y, row := range settler25Rows {
60 for x, r := range row {
61 var i int
62 if r >= '0' && r <= '9' {
63 i = int(r - '0')
64 } else {
65 i = int(r-'a') + 10
66 }
67 p.Set(x, y, uint8(i))
68 }
69 }
70 return p
71}
72
73// TestSettler25 is the finding measured on real art rather than on a fixture
74// chosen to show it.
75//
76// Settler #25 carries 20 palette entries. Keyed on the palette index, all 20
77// survive into the text. Keyed on luminance, a ten-step ramp resolves them to
78// 9: eleven of the artist's twenty colours stop existing, and the shapes
79// between them go with it.
80//
81// That is the whole argument for Glyphs being the default and Ramp being the
82// thing you ask for.
83func TestSettler25PaletteBeatsLuminance(t *testing.T) {
84 glyphs := settler25().Canvas(Glyphs).Text()
85 ramp := settler25().Canvas(Ramp).Text()
86
87 // distinctRunes counts the newline too, so both figures are colours + 1.
88 uassert.Equal(t, 21, distinctRunes(glyphs), "every palette entry survives")
89 uassert.Equal(t, 10, distinctRunes(ramp), "the ramp resolves 20 colours to 9")
90 uassert.True(t, distinctRunes(glyphs) > distinctRunes(ramp), "glyphs strictly finer")
91}
92
93// TestSettler25SVGIsOnePathPerColour pins the encoding that keeps a sprite
94// small enough for a realm to emit. 20 colours, 20 paths, and 4,532 bytes where
95// one rectangle per run cost 20,625.
96func TestSettler25SVGIsOnePathPerColour(t *testing.T) {
97 doc := settler25().SVG(8).String()
98 uassert.Equal(t, 20, strings.Count(doc, "<path"), "one path per palette entry")
99 uassert.Equal(t, 0, strings.Count(doc, "<rect"), "no rectangles at all")
100 uassert.True(t, len(doc) < 6000, "a 32x32 sprite stays well under 6 KB")
101 // Scaling is a viewBox change, so the path data, which is all of the size,
102 // is byte-identical at any scale. Only the canvas width and height move,
103 // and here that is one digit each between "32" and "256".
104 one := settler25().SVG(1).String()
105 uassert.Equal(t, paths(one), paths(doc), "path data is identical at any scale")
106 uassert.Equal(t, 2, len(doc)-len(one), "only the canvas dimensions grow")
107}
108
109// paths returns the document from its first <path> on, which is everything
110// scale cannot affect.
111func paths(doc string) string {
112 i := strings.Index(doc, "<path")
113 if i < 0 {
114 return ""
115 }
116 return doc[i:]
117}
118
119// TestSettler25TextIsSmallerThanItsPicture is the reason a text target is worth
120// having at all, beyond readability: the ASCII form of this NFT is a third of
121// the SVG.
122func TestSettler25TextIsSmallerThanItsPicture(t *testing.T) {
123 text := settler25().Canvas(Glyphs).Text()
124 uassert.Equal(t, 1469, len(text), "glyph text size")
125 uassert.True(t, len(text) < len(settler25().SVG(8).String()), "text is the cheaper form")
126}
127
128// TestSettler25RampConfusesHueForBrightness is the part of the finding that is
129// easy to miss: the ramp's collisions are not between near-identical shades.
130// Two colours a human would never confuse, an orange hat and a sky-blue tunic,
131// come out as the same character, because luminance cannot tell a hue from a
132// hue.
133func TestSettler25RampConfusesHueForBrightness(t *testing.T) {
134 orange := settler25Pal.Color(4) // #f08a2a, the hat
135 blue := settler25Pal.Color(16) // #3f8fd1, the tunic
136
137 uassert.Equal(t, 152, Luminance(orange), "orange hat")
138 uassert.Equal(t, 130, Luminance(blue), "sky tunic")
139 uassert.NotEqual(t, orange, blue, "they are different colours")
140 uassert.Equal(t, rampGlyph(orange), rampGlyph(blue), "and the same ramp glyph")
141
142 // Keyed on the palette index instead, they cannot collide: the indices are
143 // different, so the glyphs are.
144 uassert.NotEqual(t,
145 DefaultGlyphs[4%len(DefaultGlyphs)],
146 DefaultGlyphs[16%len(DefaultGlyphs)],
147 "glyphs keep them apart")
148}
149
150func rampGlyph(c int) rune {
151 return DefaultRamp[Luminance(c)*len(DefaultRamp)/256]
152}