genesis.gno
12.31 Kb · 444 lines
1// Package genesis is the platform's first engine: a pure p/ port of
2// the prototype's gen1 engine. The RNG stream, trait tables,
3// draw order and SVG drawing are byte-for-byte nftgen1's, so a seed renders
4// the same artwork here as there; genesis_test.gno pins trait vectors
5// computed independently from the same algorithm.
6//
7// What changed is only the packaging (RA L-7): it is a stateless p/
8// package implementing engine/v1, the seed comes from core's commit–reveal
9// (so the salt/seed helpers and the gnoweb Render of nftgen1 are gone), and
10// the engine takes no params.
11package genesis
12
13import (
14 "errors"
15 "strconv"
16 "strings"
17
18 "gno.land/p/nt/ufmt/v0"
19
20 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/engine/v1"
21)
22
23// TraitTypes in the order Traits returns them.
24var traitTypes = []string{"Palette", "Shape", "Ground", "Frame", "Finish", "Aura", "Elements", "Symmetry", "Score", "Rarity"}
25
26// MaxSVGBytes: 8 symmetry x 14 elements of the widest primitive plus the
27// densest ground and frame stays well under this (gen1 tested < 32,000).
28const MaxSVGBytes = 32768
29
30// Manifest describes the engine. Author is left empty: the adapter realm
31// that registers this engine sets the address that will ClaimEngine.
32func Manifest() engine.Manifest {
33 return engine.Manifest{
34 Name: "Genesis",
35 Version: "1.0.0",
36 Description: "Fully on-chain generative engine, nftgen1 lineage: FNV-1a + splitmix64 stream, " +
37 "10 traits, 1,254,400 combinations, score-based rarity over every layer.",
38 License: "TBD",
39 ParamsSchema: `{"const":""}`,
40 TraitTypes: append([]string{}, traitTypes...),
41 Living: false,
42 MaxSVGBytes: MaxSVGBytes,
43 }
44}
45
46// ValidateParams: genesis has no parameters.
47func ValidateParams(params string) error {
48 if params != "" {
49 return errors.New("genesis takes no params")
50 }
51 return nil
52}
53
54// Traits returns the ten traits of in.Seed in TraitTypes order.
55func Traits(in engine.Input) []engine.Trait {
56 t := TraitsOf(in.Seed)
57 return []engine.Trait{
58 {Type: "Palette", Value: t.Palette},
59 {Type: "Shape", Value: t.Shape},
60 {Type: "Ground", Value: t.Ground},
61 {Type: "Frame", Value: t.Frame},
62 {Type: "Finish", Value: t.Finish},
63 {Type: "Aura", Value: t.Aura},
64 {Type: "Elements", Value: strconv.Itoa(t.Elements)},
65 {Type: "Symmetry", Value: strconv.Itoa(t.Symmetry)},
66 {Type: "Score", Value: strconv.Itoa(t.Score)},
67 {Type: "Rarity", Value: t.Rarity},
68 }
69}
70
71// Render returns the raw SVG for in.Seed.
72func Render(in engine.Input) string { return SVG(in.Seed) }
73
74// ---------------------------------------------------------------------------
75// Deterministic randomness — FNV-1a over the seed, then a splitmix64 mixer.
76// Pure integer math: no floats, no time, no block data.
77// ---------------------------------------------------------------------------
78
79const (
80 fnvOffset uint64 = 14695981039346656037
81 fnvPrime uint64 = 1099511628211
82)
83
84type rng struct {
85 state uint64
86}
87
88func newRNG(seed string) *rng {
89 h := fnvOffset
90 for i := 0; i < len(seed); i++ {
91 h ^= uint64(seed[i])
92 h *= fnvPrime
93 }
94 if h == 0 {
95 h = fnvPrime
96 }
97 return &rng{state: h}
98}
99
100func (r *rng) next() uint64 {
101 r.state += 0x9E3779B97F4A7C15
102 z := r.state
103 z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9
104 z = (z ^ (z >> 27)) * 0x94D049BB133111EB
105 return z ^ (z >> 31)
106}
107
108// intn returns a value in [0, n).
109func (r *rng) intn(n int) int {
110 if n <= 0 {
111 return 0
112 }
113 return int(r.next() % uint64(n))
114}
115
116// ---------------------------------------------------------------------------
117// Weighted trait tables (weights are explicit; one draw walks the sum).
118// ---------------------------------------------------------------------------
119
120type option struct {
121 name string
122 weight int
123 score int // contribution to the rarity score
124}
125
126func pick(r *rng, table []option) option {
127 total := 0
128 for _, o := range table {
129 total += o.weight
130 }
131 roll := r.intn(total)
132 acc := 0
133 for _, o := range table {
134 acc += o.weight
135 if roll < acc {
136 return o
137 }
138 }
139 return table[len(table)-1]
140}
141
142type palette struct {
143 option
144 bg string
145 ink string
146 c1 string
147 c2 string
148 c3 string
149}
150
151// Palette weights sum to 100, so a weight reads directly as a percent.
152var palettes = []palette{
153 {option{"Emerald", 18, 0}, "#04140f", "#7cf2c4", "#10b981", "#34d399", "#065f46"},
154 {option{"Amethyst", 18, 0}, "#0b0713", "#e9d5ff", "#8b5cf6", "#c084fc", "#4c1d95"},
155 {option{"Abyss", 16, 0}, "#02101c", "#bae6fd", "#0ea5e9", "#38bdf8", "#0c4a6e"},
156 {option{"Ember", 14, 0}, "#160806", "#fecaca", "#f97316", "#fb923c", "#7c2d12"},
157 {option{"Bone", 12, 0}, "#f5f5f4", "#1c1917", "#78716c", "#a8a29e", "#292524"},
158 {option{"Solar", 10, 0}, "#1a1403", "#fef08a", "#eab308", "#facc15", "#713f12"},
159 {option{"Obsidian", 8, 2}, "#050505", "#e5e5e5", "#404040", "#737373", "#171717"},
160 {option{"Prism", 4, 4}, "#0a0a12", "#fdf4ff", "#22d3ee", "#f472b6", "#a78bfa"},
161}
162
163var shapes = []option{
164 {"Orbit", 20, 0},
165 {"Lattice", 18, 0},
166 {"Bloom", 18, 0},
167 {"Shard", 16, 0},
168 {"Spiral", 14, 0},
169 {"Weave", 9, 2},
170 {"Rift", 5, 4},
171}
172
173var grounds = []option{
174 {"Flat", 30, 0},
175 {"Halo", 25, 0},
176 {"Grid", 22, 0},
177 {"Rings", 14, 1},
178 {"Scatter", 9, 2},
179}
180
181var frames = []option{
182 {"None", 55, 0},
183 {"Thin", 25, 1},
184 {"Double", 14, 2},
185 {"Ornate", 6, 4},
186}
187
188var finishes = []option{
189 {"Matte", 50, 0},
190 {"Gloss", 28, 1},
191 {"Glow", 16, 2},
192 {"Foil", 6, 4},
193}
194
195var auras = []option{
196 {"None", 78, 0},
197 {"Soft", 14, 2},
198 {"Bright", 6, 4},
199 {"Nova", 2, 7},
200}
201
202// TraitSet is the decoded properties of a seed. The SVG builder and the
203// trait list both read this one struct, so picture and metadata cannot
204// describe different artwork.
205type TraitSet struct {
206 Palette string
207 Shape string
208 Ground string
209 Frame string
210 Finish string
211 Aura string
212 Elements int
213 Symmetry int
214 Score int
215 Rarity string
216}
217
218// Tier cut-offs, chosen on nftgen1's 1000-seed simulation: Common 21.4% /
219// Uncommon 44.4% / Rare 25.2% / Legendary 7.6% / Mythic 1.4%.
220const (
221 scoreMythic = 12
222 scoreLegendary = 8
223 scoreRare = 5
224 scoreUncommon = 2
225)
226
227func derive(seed string) (TraitSet, palette) {
228 r := newRNG(seed)
229
230 // Draw order is part of the contract: changing it changes every token.
231 p := palettes[0]
232 {
233 total := 0
234 for _, q := range palettes {
235 total += q.weight
236 }
237 roll := r.intn(total)
238 acc := 0
239 for _, q := range palettes {
240 acc += q.weight
241 if roll < acc {
242 p = q
243 break
244 }
245 }
246 }
247 shape := pick(r, shapes)
248 ground := pick(r, grounds)
249 frame := pick(r, frames)
250 finish := pick(r, finishes)
251 aura := pick(r, auras)
252
253 t := TraitSet{
254 Palette: p.name,
255 Shape: shape.name,
256 Ground: ground.name,
257 Frame: frame.name,
258 Finish: finish.name,
259 Aura: aura.name,
260 Elements: 5 + r.intn(10), // 5..14
261 Symmetry: 2 + r.intn(7), // 2..8
262 }
263
264 score := p.score + shape.score + ground.score + frame.score + finish.score + aura.score
265 if t.Elements >= 13 {
266 score++
267 }
268 if t.Symmetry >= 7 {
269 score++
270 }
271 t.Score = score
272
273 switch {
274 case score >= scoreMythic:
275 t.Rarity = "Mythic"
276 case score >= scoreLegendary:
277 t.Rarity = "Legendary"
278 case score >= scoreRare:
279 t.Rarity = "Rare"
280 case score >= scoreUncommon:
281 t.Rarity = "Uncommon"
282 default:
283 t.Rarity = "Common"
284 }
285 return t, p
286}
287
288// TraitsOf returns the decoded traits for a seed.
289func TraitsOf(seed string) TraitSet {
290 t, _ := derive(seed)
291 return t
292}
293
294func finishStroke(finish string) int {
295 switch finish {
296 case "Gloss":
297 return 4
298 case "Glow":
299 return 5
300 case "Foil":
301 return 6
302 default: // Matte
303 return 3
304 }
305}
306
307func auraRing(aura string) (int, int) { // width, opacity percent
308 switch aura {
309 case "Soft":
310 return 4, 22
311 case "Bright":
312 return 8, 38
313 case "Nova":
314 return 14, 60
315 default:
316 return 0, 0
317 }
318}
319
320// SVG renders the artwork for a seed as a standalone SVG document.
321func SVG(seed string) string {
322 t, p := derive(seed)
323 r := newRNG(seed + "|art") // separate stream: trait draws never shift the art
324
325 var b strings.Builder
326 b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">`)
327 b.WriteString(ufmt.Sprintf(`<rect width="512" height="512" fill="%s"/>`, p.bg))
328
329 // --- ground layer ---
330 switch t.Ground {
331 case "Halo":
332 b.WriteString(ufmt.Sprintf(
333 `<circle cx="256" cy="256" r="200" fill="none" stroke="%s" stroke-width="1" opacity="0.35"/>`, p.c3))
334 case "Grid":
335 for i := 64; i < 512; i += 64 {
336 b.WriteString(ufmt.Sprintf(
337 `<line x1="%d" y1="0" x2="%d" y2="512" stroke="%s" stroke-width="1" opacity="0.18"/>`, i, i, p.c3))
338 b.WriteString(ufmt.Sprintf(
339 `<line x1="0" y1="%d" x2="512" y2="%d" stroke="%s" stroke-width="1" opacity="0.18"/>`, i, i, p.c3))
340 }
341 case "Rings":
342 for rad := 60; rad <= 240; rad += 45 {
343 b.WriteString(ufmt.Sprintf(
344 `<circle cx="256" cy="256" r="%d" fill="none" stroke="%s" stroke-width="1" opacity="0.22"/>`, rad, p.c3))
345 }
346 case "Scatter":
347 for i := 0; i < 40; i++ {
348 b.WriteString(ufmt.Sprintf(
349 `<circle cx="%d" cy="%d" r="2" fill="%s" opacity="0.30"/>`,
350 16+r.intn(480), 16+r.intn(480), p.c3))
351 }
352 }
353
354 // --- aura layer ---
355 if w, op := auraRing(t.Aura); w > 0 {
356 b.WriteString(ufmt.Sprintf(
357 `<circle cx="256" cy="256" r="230" fill="none" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
358 p.ink, w, op))
359 }
360
361 // --- composition: one pass, mirrored Symmetry times ---
362 stroke := finishStroke(t.Finish)
363 step := 360 / t.Symmetry
364 for s := 0; s < t.Symmetry; s++ {
365 b.WriteString(ufmt.Sprintf(`<g transform="rotate(%d 256 256)">`, s*step))
366 for e := 0; e < t.Elements; e++ {
367 color := p.c1
368 switch r.intn(3) {
369 case 1:
370 color = p.c2
371 case 2:
372 color = p.ink
373 }
374 x := 96 + r.intn(320)
375 y := 96 + r.intn(320)
376 size := 12 + r.intn(70)
377 opacity := 40 + r.intn(55) // percent, integer only
378 if t.Finish == "Glow" || t.Finish == "Foil" {
379 opacity = 55 + r.intn(40)
380 }
381
382 switch t.Shape {
383 case "Orbit":
384 b.WriteString(ufmt.Sprintf(
385 `<circle cx="%d" cy="%d" r="%d" fill="none" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
386 x, y, size, color, stroke, opacity))
387 case "Lattice":
388 b.WriteString(ufmt.Sprintf(
389 `<rect x="%d" y="%d" width="%d" height="%d" fill="none" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
390 x, y, size, size, color, stroke-1, opacity))
391 case "Bloom":
392 b.WriteString(ufmt.Sprintf(
393 `<circle cx="%d" cy="%d" r="%d" fill="%s" opacity="0.%d"/>`,
394 x, y, size/2, color, opacity))
395 case "Shard":
396 b.WriteString(ufmt.Sprintf(
397 `<polygon points="%d,%d %d,%d %d,%d" fill="%s" opacity="0.%d"/>`,
398 x, y, x+size, y+size/2, x+size/3, y+size, color, opacity))
399 case "Weave":
400 b.WriteString(ufmt.Sprintf(
401 `<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
402 x, y, x+size, y+size/3, color, stroke, opacity))
403 b.WriteString(ufmt.Sprintf(
404 `<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
405 x+size, y, x, y+size/3, color, stroke, opacity))
406 case "Rift":
407 b.WriteString(ufmt.Sprintf(
408 `<path d="M%d %d l %d %d l %d %d l %d %d" fill="none" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
409 x, y, size/2, -size/3, size/3, size, size/2, -size/2,
410 color, stroke, opacity))
411 default: // Spiral
412 b.WriteString(ufmt.Sprintf(
413 `<path d="M%d %d q %d %d %d %d" fill="none" stroke="%s" stroke-width="%d" opacity="0.%d"/>`,
414 x, y, size, -size, size*2, 0, color, stroke, opacity))
415 }
416 }
417 b.WriteString(`</g>`)
418 }
419
420 // --- frame layer, drawn last so it always sits on top ---
421 switch t.Frame {
422 case "Thin":
423 b.WriteString(ufmt.Sprintf(
424 `<rect x="12" y="12" width="488" height="488" fill="none" stroke="%s" stroke-width="2" opacity="0.55"/>`, p.ink))
425 case "Double":
426 b.WriteString(ufmt.Sprintf(
427 `<rect x="10" y="10" width="492" height="492" fill="none" stroke="%s" stroke-width="3" opacity="0.60"/>`, p.ink))
428 b.WriteString(ufmt.Sprintf(
429 `<rect x="24" y="24" width="464" height="464" fill="none" stroke="%s" stroke-width="1" opacity="0.45"/>`, p.c2))
430 case "Ornate":
431 b.WriteString(ufmt.Sprintf(
432 `<rect x="8" y="8" width="496" height="496" fill="none" stroke="%s" stroke-width="4" opacity="0.70"/>`, p.ink))
433 b.WriteString(ufmt.Sprintf(
434 `<rect x="22" y="22" width="468" height="468" fill="none" stroke="%s" stroke-width="2" opacity="0.55"/>`, p.c2))
435 for _, c := range [][2]int{{8, 8}, {504, 8}, {8, 504}, {504, 504}} {
436 b.WriteString(ufmt.Sprintf(
437 `<circle cx="%d" cy="%d" r="10" fill="none" stroke="%s" stroke-width="3" opacity="0.80"/>`,
438 c[0], c[1], p.c1))
439 }
440 }
441
442 b.WriteString(`</svg>`)
443 return b.String()
444}