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

data.gno

13.09 Kb · 423 lines
  1package golf
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"crypto/sha256"
  7	"encoding/hex"
  8	"math"
  9	"strconv"
 10	"strings"
 11	"unicode"
 12	"unicode/utf8"
 13
 14	"gno.land/p/nym-alexiscolin000/gnogolf/course"
 15)
 16
 17// Holes as data: a version is a GG1 string, checked once at publish and kept
 18// as bytes; each call decodes it afresh and stores nothing of it. A version
 19// is never edited or removed: a new one takes its alias, and the old one is
 20// archived, still playable, its records kept.
 21
 22// wearGrid is the entry's wear: a count of balls at rest per cell of the
 23// WearW by WearH grid, row-major.
 24func (e *entry) wearGrid() []int {
 25	out := make([]int, wearCells)
 26	e.wear().Iterate("", "", func(k, v string) bool {
 27		if i := atoi(k); i >= 0 && i < wearCells {
 28			out[i] = atoi(v)
 29		}
 30		return false
 31	})
 32	return out
 33}
 34
 35const wearCells = course.WearW * course.WearH
 36
 37// wear is a version's wear counters: pad(cell, 3) -> balls at rest there, a
 38// row a cell played (one small write a stroke).
 39func (e *entry) wear() view { return view{"wear", e.id + " "} }
 40
 41// mark counts a ball come to rest in wear cell i.
 42func (e *entry) mark(i int) {
 43	if i < 0 || i >= wearCells {
 44		return // course.WearIndexOn never gives one
 45	}
 46	k := pad(i, 3)
 47	n := 0
 48	if v, ok := e.wear().Get(k); ok {
 49		n = atoi(v)
 50	}
 51	e.wear().Set(k, itoa(n+1))
 52}
 53
 54// maxData bounds a version's data: a hole at every limit of the format is
 55// about 21 KB.
 56const maxData = 32 << 10
 57
 58// Publish puts a course hole's data into its slot as its next version and
 59// returns the new id ("garden/7/v2"). Only the owner can. hexData is the hole
 60// as course.Encode wrote it, in hex. slot must be the data's own world and
 61// order ("garden/7", order 1 to 999), so a mis-edited file cannot replace the
 62// wrong hole; the course holds at most 9999 slots (maxCourse). Data identical
 63// to the current version's is refused. The version it replaces is archived:
 64// its records stay, its bests leave the course standings. note (at most 140
 65// characters, cleaned) says why.
 66func Publish(cur realm, slot, hexData, note string) string {
 67	begin()
 68	r := publishNow(cur, slot, hexData, note)
 69	flush(cur) // (only once it went through: a refusal writes nothing)
 70	return r
 71}
 72
 73func publishNow(cur realm, slot, hexData, note string) string {
 74	by := cur.Previous().Address()
 75	if !isOwner(by) {
 76		panic("golf: only the owner can publish a course hole")
 77	}
 78	h, data, sum := decodeData(hexData)
 79	if h.Order < 1 || h.Order != math.Trunc(h.Order) {
 80		panic("golf: a course hole's order is a whole number from 1 to 999")
 81	}
 82	own := h.World + "/" + strconv.Itoa(int(h.Order))
 83	if slot != own {
 84		panic("golf: this data is " + own + ", not " + slot)
 85	}
 86	key, ok := slotKeyOf(slot)
 87	if !ok {
 88		panic("golf: " + slot + " is not a course slot") // its data's own world/order: cannot happen
 89	}
 90	var old *entry
 91	n := 1
 92	if v, ok := slots.Get(key); ok {
 93		old = find(v)
 94		if old.sha == sum {
 95			panic(nothingNew(old))
 96		}
 97		n = old.version + 1
 98	} else if slots.Size() >= maxCourse {
 99		panic("golf: the course is full: 9999 slots")
100	}
101	e := dataEntry(slot, n, h, by, sum, note)
102	e.official = true
103	saveEntry(e)
104	holeData.Set(e.id, data)
105	slots.Set(key, e.id)
106	chain.Emit(EventHolePublished, "hole", e.id, "slot", slot, "sha", sum, "by", by.String(), "official", "true")
107	if old != nil {
108		retire(old, e.id)
109	}
110	return e.id
111}
112
113// PublishMine publishes a community hole of the caller's own and returns its
114// id: "<caller>/<slug>/v1", then v2 and on. Anyone can while publishing is
115// open (SetPublishing); only the same address adds versions. slug is 1 to 32
116// of a-z, 0-9 and "-". The data is checked as Publish checks it, its world
117// and order aside; its name and note carry no web address. It is playable and
118// recorded, in no cup and not in the course ranking; a new version of a
119// hidden hole stays hidden. The publisher pays the storage deposit (about
120// half a GNOT a version), and nothing published can be deleted.
121func PublishMine(cur realm, slug, hexData, note string) string {
122	begin()
123	r := publishMineNow(cur, slug, hexData, note)
124	flush(cur) // (only once it went through: a refusal writes nothing)
125	return r
126}
127
128func publishMineNow(cur realm, slug, hexData, note string) string {
129	if !publishing {
130		panic("golf: publishing community holes is not open yet")
131	}
132	by := cur.Previous().Address()
133	if !validSlug(slug) {
134		panic("golf: a slug is 1 to 32 of a-z, 0-9 and -")
135	}
136	alias := by.String() + "/" + slug
137	h, data, sum := decodeData(hexData)
138	var old *entry
139	n := 1
140	if v, ok := aliases.Get(alias); ok {
141		old = find(v)
142		if old.sha == sum {
143			panic(nothingNew(old))
144		}
145		n = old.version + 1
146	}
147	e := dataEntry(alias, n, h, by, sum, note)
148	if linkish(e.name) || linkish(e.note) {
149		panic("golf: a community hole's name and note carry no web address")
150	}
151	saveEntry(e)
152	holeData.Set(e.id, data)
153	aliases.Set(alias, e.id)
154	if old != nil {
155		old.next = e.id // archived; it never counted anywhere
156		saveEntry(old)
157	}
158	if !hidden.Has(alias) { // a hidden hole has no row (Hide)
159		a := by.String()
160		// the newest row: the author's own moves to its height
161		moveAuthor(a, newestRow(a), pad(int(e.height), 12))
162		if old != nil {
163			byAuthor.Remove(a + " " + recentKey(old.height, slug))
164		}
165		byAuthor.Set(a+" "+recentKey(e.height, slug), e.id)
166	}
167	chain.Emit(EventHolePublished, "hole", e.id, "slot", alias, "sha", sum, "by", by.String(), "official", "false")
168	return e.id
169}
170
171// newestRow is the height of an author's newest row in byAuthor
172// ("<address> <height> <slug>"), "" if they have none.
173func newestRow(a string) string {
174	latest := ""
175	byAuthor.ReverseIterate(a+" ", a+"!", func(k, _ string) bool {
176		latest = k[len(a)+1 : len(a)+13]
177		return true
178	})
179	return latest
180}
181
182// newestRowBut is newestRow with the row but left out ("" if it was their only one).
183func newestRowBut(a, but string) string {
184	latest := ""
185	byAuthor.ReverseIterate(a+" ", a+"!", func(k, _ string) bool {
186		if k == but {
187			return false
188		}
189		latest = k[len(a)+1 : len(a)+13]
190		return true
191	})
192	return latest
193}
194
195// moveAuthor moves an author's own row in byAuthor (recentKey(height,
196// address)) from the height was to now, "" for none: it stays at their
197// newest row's, and goes with their last.
198func moveAuthor(a, was, now string) {
199	if was == now {
200		return
201	}
202	if was != "" {
203		byAuthor.Remove(was + " " + a)
204	}
205	if now != "" {
206		byAuthor.Set(now+" "+a, "")
207	}
208}
209
210// linkish spots a web address in a community hole's name or note: "www.", a
211// dot between a letter or digit and two letters ("claim.xyz", "t.me"; not
212// "e.g."), or an IP address, look-alike dots (.。․﹒) read as dots, letters in
213// any case. Basic (the owner can Hide what gets past) and strict on purpose
214// ("St.Andrews" is refused). It reads s a byte at a time: every mark is
215// ASCII, so a non-ASCII character is decoded only to tell İ and the Kelvin
216// sign (which lower-case to ASCII), the look-alike dots and spaces apart.
217func linkish(s string) bool {
218	var c1, c2, c3 byte // the three characters before c, lower-cased
219	// the word so far (as strings.Fields splits): an IP is digits, three dots
220	word, ip, dots := false, true, 0
221	for i := 0; i < len(s); {
222		c, size, space := s[i], 1, false
223		switch {
224		case c >= 'A' && c <= 'Z':
225			c += 'a' - 'A'
226		case c >= utf8.RuneSelf:
227			var r rune
228			r, size = utf8.DecodeRuneInString(s[i:])
229			switch r {
230			case 'İ':
231				c = 'i'
232			case '\u212a': // the Kelvin sign
233				c = 'k'
234			case '.', '。', '․', '﹒':
235				c = '.'
236			default:
237				c, space = utf8.RuneSelf, unicode.IsSpace(r) // none of the marks
238			}
239		case c == ' ' || c >= '\t' && c <= '\r':
240			space = true
241		}
242		i += size
243		switch {
244		case c == '.' && c1 == 'w' && c2 == 'w' && c3 == 'w':
245			return true
246		case isLower(c) && isLower(c1) && c2 == '.' && (isLower(c3) || isDigit(c3)):
247			return true
248		}
249		c1, c2, c3 = c, c1, c2
250		if space {
251			if word && ip && dots == 3 {
252				return true
253			}
254			word, ip, dots = false, true, 0
255			continue
256		}
257		word = true
258		if c == '.' {
259			dots++
260		} else if !isDigit(c) {
261			ip = false
262		}
263	}
264	return word && ip && dots == 3
265}
266
267func isLower(c byte) bool { return c >= 'a' && c <= 'z' }
268func isDigit(c byte) bool { return c >= '0' && c <= '9' }
269
270// decodeData checks a version's data as it arrives: hex, GG1 within the
271// format's limits, stored lengths bit-exact (DecodeChecked: every later
272// Decode trusts them), and in the one form Encode gives.
273func decodeData(hexData string) (*course.Simple, string, string) {
274	if len(hexData) > 2*maxData {
275		panic("golf: the data is too long")
276	}
277	b, err := hex.DecodeString(hexData)
278	if err != nil {
279		panic("golf: the data is not hex")
280	}
281	data := string(b)
282	h, err := course.DecodeChecked(data)
283	if err != nil {
284		panic("golf: " + err.Error())
285	}
286	// one hole, one string: data that decodes to a hole Encode would write
287	// otherwise (a skin never used, a style run split, a stored length a
288	// zero-length wall never reads) is refused, so the same hole cannot come
289	// back under another sha
290	if course.Encode(h) != data {
291		panic("golf: the data is not as course.Encode writes it")
292	}
293	// refused if a transaction could not replay its heaviest shot, or if in
294	// its dearest weather a commit's first shot would be capped below
295	// minShotWork: every commit decodes the hole and draws its forecast first
296	if newWork(h, nil).bound > maxShotGas {
297		panic(errShotTooHeavy)
298	}
299	if worstGas(h, len(data)) > workBudget {
300		panic(errTooStormy)
301	}
302	sum := sha256.Sum256(b)
303	return h, data, hex.EncodeToString(sum[:])
304}
305
306// nothingNew refuses data identical to the alias's current version: it would
307// reset the hole's ranking for nothing.
308func nothingNew(old *entry) string {
309	return "golf: this data is already " + old.id + ": nothing to publish"
310}
311
312func dataEntry(alias string, n int, h *course.Simple, by address, sum, note string) *entry {
313	e := newEntry(alias+"/v"+strconv.Itoa(n), cleanName(h.Title), h.Par(), h.World, h.Order)
314	e.slot, e.version, e.by, e.height, e.sha, e.note = alias, n, by, runtime.ChainHeight(), sum, cleanText(note, maxNote)
315	return e
316}
317
318// worlds is the course's worlds in play order, with their names on the page.
319var worlds = []struct{ id, name string }{
320	{"garden", "Garden Cup"},
321	{"island", "Island Cup"},
322	{"town", "Mushroom Town"},
323	{"mountain", "Mountain Cup"},
324	{"mines", "Crystal Mines"},
325}
326
327// worldRank is a world's place among worlds; any other comes after.
328func worldRank(w string) int {
329	for i, x := range worlds {
330		if x.id == w {
331			return i
332		}
333	}
334	return len(worlds)
335}
336
337// slotKey is a slot's key: its world's rank, the world, and the order in
338// three digits, so the slots tree is in course order.
339func slotKey(world string, order int) string {
340	return string(rune('0'+worldRank(world))) + world + "/" + pad(order, 3)
341}
342
343// slotKeyOf reads a slot, "world/order" (as "garden/7"), into its key.
344func slotKeyOf(slot string) (string, bool) {
345	i := strings.Index(slot, "/")
346	if i < 0 || !course.IsWorld(slot[:i]) {
347		return "", false
348	}
349	n, err := strconv.Atoi(slot[i+1:])
350	if err != nil || n < 1 || n > course.MaxOrder || strconv.Itoa(n) != slot[i+1:] {
351		return "", false
352	}
353	return slotKey(slot[:i], n), true
354}
355
356func validSlug(s string) bool {
357	if len(s) < 1 || len(s) > 32 {
358		return false
359	}
360	for i := 0; i < len(s); i++ {
361		if c := s[i]; !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') {
362			return false
363		}
364	}
365	return true
366}
367
368// HoleData is a version's GG1, in hex, as it was published (an alias gives
369// its current version's): what a later version of the rules or an auditor
370// reads back.
371func HoleData(hole string) string {
372	e := readHole(hole)
373	data, _ := holeData.Get(e.id)
374	return hex.EncodeToString([]byte(data))
375}
376
377// Current is the id of the version an alias ("garden/7", "<address>/<slug>")
378// plays now, "" if the alias has none.
379func Current(alias string) string { return current(alias) }
380
381// Versions is every version of an alias, oldest first (the newest pageMax if
382// there are more): its id, number, the height it was published at, by whom,
383// its data's sha256, its note, and the version that took its place. slot is
384// the alias, "" (and no versions) for one that has none.
385func Versions(alias string) string {
386	now := find(current(alias))
387	slot := ""
388	if now != nil {
389		slot = now.slot // never the caller's own bytes
390	}
391	var sb strings.Builder
392	sb.WriteString("{" + versionJSON + `,"slot":` + jstr(slot) + `,"versions":[`)
393	if now != nil {
394		i := 0
395		for n := firstVersion(now); n <= now.version; n++ {
396			e := find(now.slot + "/v" + strconv.Itoa(n))
397			if e == nil {
398				continue // (one before this course's first here: imported from v1, with v1's number)
399			}
400			sb.WriteString(sep(i) + `{"id":` + jstr(e.id) + `,"v":` + strconv.Itoa(e.version) +
401				`,"height":` + strconv.FormatInt(e.height, 10) + `,"by":` + jstr(e.by.String()) +
402				`,"sha":` + jstr(e.sha) + `,"note":` + jstr(e.note) + `,"next":` + jstr(e.next) + "}")
403			i++
404		}
405	}
406	sb.WriteString("]}")
407	return sb.String()
408}
409
410// firstVersion is the oldest of now's versions a list shows: the newest
411// pageMax are (now is its alias's current version).
412func firstVersion(now *entry) int {
413	if now.version > pageMax {
414		return now.version - pageMax + 1
415	}
416	return 1
417}
418
419// BestOf is a player's best finished round on a hole in a mode, 0 if none.
420func BestOf(hole, mode string, player address) int {
421	e, m := readHole(hole), modeOf(mode)
422	return bestStrokes(bestOfPlayer(e, m, player.String()))
423}