Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

price.gno

9.42 Kb · 286 lines
  1package nsskin
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nt/avl/v0"
  7	"gno.land/p/nt/ufmt/v0"
  8)
  9
 10/*
 11WHAT ONE COSTS, PER DESIGN.
 12
 13There used to be a single `price` for the whole collection, which is the
 14right shape for a launch batch and the wrong one the moment anything in
 15it is worth more or less than the rest — or worth nothing, which is the
 16interesting case.
 17
 18Three sources, in order, and only the first of them stores anything:
 19
 20	an ADMIN OVERRIDE, if one has been set for this id
 21	ZERO, if the id falls in the free share by the rule below
 22	the BASE PRICE otherwise
 23
 24# WHY THE FREE ONES ARE COMPUTED RATHER THAN LISTED
 25
 26Marking a third of sixty definitions free by storing a flag on each is
 27sixty writes and sixty entries that live forever, to record something
 28that never changes. The rule below derives it from the id, so it costs
 29nothing to store, nothing to keep in step, and cannot drift between the
 30contract, the shop and the collection file — all three ask the same
 31question of the same string and get the same answer.
 32
 33It is ARBITRARY, which is the point: nothing about a design implies its
 34price, and a rule nobody can lobby is fairer than a list somebody picked.
 35An admin who disagrees about a particular one sets an override, and that
 36override is then a deliberate, visible, single stored fact.
 37
 38# WHY A HASH AND NOT EVERY THIRD ONE
 39
 40Definitions are iterated and displayed in id order, so "every third"
 41would put the free ones in a visible stripe down the grid and make the
 42whole thing look like a pricing table rather than a collection.
 43*/
 44
 45/*
 46The threshold, out of a hundred, under which a hashed id is free.
 47
 48THIRTY-FIVE, NOT THIRTY, AND THE FOUR-POINT GAP IS THE HONEST PART. The
 49rule is a statement about ids, not a quota, so any particular collection
 50lands near the threshold rather than on it — sixty ids at a threshold of
 51thirty came out at thirteen, which is twenty-one per cent and not the
 52third that was asked for. Thirty-five puts the launch batch at eighteen,
 53which is thirty per cent exactly.
 54
 55So the constant was chosen to fit the collection that exists. That is
 56worth saying plainly rather than dressing up: what it buys is that every
 57FUTURE id still gets an answer nobody can lobby for, which is the
 58property that mattered, while the batch on sale today is the share that
 59was promised.
 60
 61A constant rather than a stored value, because changing it would
 62silently reprice every skin nobody has overridden — including ones
 63people have already been shown a price for. That should cost a deploy.
 64*/
 65const freeShare = 35
 66
 67// priceOverride holds ONLY the ids an admin has priced by hand. An id
 68// that is absent is not "free" and not "base" — it is unopinionated, and
 69// PriceOf decides. Storing a value for every skin would make the common
 70// case cost as much as the exception.
 71var priceOverride = avl.NewTree() // definition id -> int64 ugnot
 72
 73/*
 74freeByRule is FNV-1a over the id, taken modulo one hundred.
 75
 76freeShare of every hundred ids fall under the threshold, so the share
 77lands NEAR it rather than on it for any particular collection — see the
 78note on the constant. The launch batch of sixty comes out at eighteen,
 79and ListFreeSkins says so rather than asking anyone to trust the
 80arithmetic.
 81
 82The same hash is in nsview.faceForArt, deliberately not shared: one runs
 83over a sprite and one over an id, they answer different questions, and a
 84helper spanning both realms would be a dependency between them for four
 85lines of arithmetic.
 86*/
 87func freeByRule(id string) bool {
 88	h := uint32(2166136261)
 89	for i := 0; i < len(id); i++ {
 90		h ^= uint32(id[i])
 91		h *= 16777619
 92	}
 93	return h%100 < freeShare
 94}
 95
 96// IsFreeByRule reports what the rule alone says, ignoring any override.
 97// Exposed so an admin panel can show what it would revert to.
 98func IsFreeByRule(id string) bool {
 99	return freeByRule(strings.ToLower(strings.TrimSpace(id)))
100}
101
102// HasPriceOverride reports whether this id has been priced by hand. A
103// panel needs it to tell "set to zero" from "free by the rule", which
104// look identical in the price alone and are different decisions.
105func HasPriceOverride(id string) bool {
106	return priceOverride.Has(strings.ToLower(strings.TrimSpace(id)))
107}
108
109// PriceOf is what this design costs right now, in ugnot.
110//
111// It does NOT check that the id exists: a price for a definition that is
112// not there is meaningless rather than wrong, and Buy is the thing that
113// must refuse an unknown id — which it already does, by name, before it
114// ever asks what the price is.
115func PriceOf(id string) int64 {
116	id = strings.ToLower(strings.TrimSpace(id))
117	if v := priceOverride.Get(id); v != nil {
118		return v.(int64)
119	}
120	if freeByRule(id) {
121		return 0
122	}
123	return price
124}
125
126/*
127SetSkinPrice prices one design by hand, overriding the rule.
128
129Zero is a legitimate value and is NOT the same as removing the override:
130it says "this one is free because I said so", which survives a change to
131the base price and to the rule. ClearSkinPrice is how you go back to
132being unopinionated.
133*/
134func SetSkinPrice(cur realm, id string, p int64) {
135	assertIsAdmin(cur)
136	setSkinPrice(id, p)
137}
138
139// setSkinPrice is SetSkinPrice without the realm ceremony. Split for the
140// same reason attach.gno splits refresh from Refresh: the rules are
141// worth testing on their own, and the gate above them is the same
142// assertIsAdmin every other write in this realm goes through.
143func setSkinPrice(id string, p int64) {
144	id = strings.ToLower(strings.TrimSpace(id))
145	if !skins.Has(id) {
146		panic("nsskin: no such skin: " + id)
147	}
148	if p < 0 {
149		panic("nsskin: a negative price is not a discount")
150	}
151	priceOverride.Set(id, p)
152}
153
154// ClearSkinPrice drops an override, putting the design back under the
155// rule and the base price.
156func ClearSkinPrice(cur realm, id string) {
157	assertIsAdmin(cur)
158	clearSkinPrice(id)
159}
160
161func clearSkinPrice(id string) {
162	priceOverride.Remove(strings.ToLower(strings.TrimSpace(id)))
163}
164
165/*
166SetSkinPrices prices many designs in one call.
167
168A panel that lets somebody retune a collection produces a batch of
169changes, and sixty separate transactions is sixty flat fees for what is
170one decision. Same encoding as AllSkins reads back — tab between the id
171and the price, newline between records — so the two halves of the panel
172speak one format.
173
174An empty price field CLEARS the override rather than setting zero. The
175panel shows an empty box for "no opinion" and a 0 for "free on purpose",
176and those must not collapse into each other on the way here.
177*/
178func SetSkinPrices(cur realm, packed string) {
179	assertIsAdmin(cur)
180	setSkinPrices(packed)
181}
182
183func setSkinPrices(packed string) {
184	/* CHECKED IN FULL BEFORE ANYTHING IS WRITTEN.
185	   A panic reverts the whole transaction on chain, so applying as we
186	   go would come out right in production — but only because of a
187	   guarantee living somewhere else. Validating first makes the
188	   promise true of this function on its own, which is what the tests
189	   can actually hold it to, and it costs one pass over a list that is
190	   sixty lines at its largest. */
191	type change struct {
192		id    string
193		clear bool
194		p     int64
195	}
196	var pending []change
197
198	for _, raw := range strings.Split(packed, "\n") {
199		/* TRIMMED AROUND THE FIELDS, NOT AROUND THE LINE. Trimming the
200		   whole line first ate the trailing tab of "id\t", which is the
201		   form that CLEARS an override — so "no opinion" arrived here
202		   looking like a line with no tab at all and was rejected as
203		   malformed. Only the carriage return goes, because a form
204		   posting CRLF is not making a statement about pricing. */
205		line := strings.TrimRight(raw, "\r")
206		if strings.TrimSpace(line) == "" {
207			continue
208		}
209		tab := strings.Index(line, "\t")
210		if tab < 0 {
211			panic("nsskin: each line is <id>\\t<ugnot>, got: " + line)
212		}
213		id := strings.ToLower(strings.TrimSpace(line[:tab]))
214		val := strings.TrimSpace(line[tab+1:])
215		if !skins.Has(id) {
216			panic("nsskin: no such skin: " + id)
217		}
218		if val == "" {
219			pending = append(pending, change{id: id, clear: true})
220			continue
221		}
222		p, ok := parseU64(val)
223		if !ok {
224			panic("nsskin: not a number of ugnot: " + val)
225		}
226		pending = append(pending, change{id: id, p: p})
227	}
228
229	for _, c := range pending {
230		if c.clear {
231			priceOverride.Remove(c.id)
232			continue
233		}
234		priceOverride.Set(c.id, c.p)
235	}
236}
237
238// parseU64 is strconv.ParseInt without the import, and without accepting
239// a sign — every price here is a non-negative amount of ugnot, and "-0"
240// or "+5" arriving from a form should be refused rather than guessed at.
241func parseU64(s string) (int64, bool) {
242	if len(s) == 0 || len(s) > 18 {
243		return 0, false
244	}
245	var n int64
246	for i := 0; i < len(s); i++ {
247		c := s[i]
248		if c < '0' || c > '9' {
249			return 0, false
250		}
251		n = n*10 + int64(c-'0')
252	}
253	return n, true
254}
255
256/*
257ListFreeSkins names every definition that costs nothing right now,
258counting overrides — so it answers "what can I have for free", which is
259the question, rather than "what does the rule say", which is trivia.
260*/
261func ListFreeSkins() string {
262	var b strings.Builder
263	skins.Iterate("", "", func(k string, _ any) bool {
264		if PriceOf(k) == 0 {
265			b.WriteString(k + "\n")
266		}
267		return false
268	})
269	return b.String()
270}
271
272// AllPrices is every id and its price, for a shop that wants the lot in
273// one read. AllSkins carries the price too; this exists for a panel that
274// wants prices without the rest and for checking the two agree.
275func AllPrices() string {
276	var b strings.Builder
277	skins.Iterate("", "", func(k string, _ any) bool {
278		o := "0"
279		if priceOverride.Has(k) {
280			o = "1"
281		}
282		b.WriteString(k + "\t" + ufmt.Sprintf("%d", PriceOf(k)) + "\t" + o + "\n")
283		return false
284	})
285	return b.String()
286}