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.

codec.gno

15.36 Kb · 631 lines
  1package arena
  2
  3import (
  4	"encoding/binary"
  5	"encoding/hex"
  6
  7	"gno.land/p/g188vafzutzvlfw6wt7u3s62xtuvm0epmu8ufmfz/gnomepit/engine"
  8)
  9
 10// Everything stored by the arena is a byte string laid out here, big-endian. This file
 11// is the only code that reads or writes those bytes.
 12
 13const (
 14	chunkSize   = 64 // match ids per chunk
 15	addressSize = 40 // bytes in an address's text
 16	sealSize    = 32 // bytes in a raw sha256 seal
 17	lockSize    = addressSize + 4 + 4
 18	openSize    = 4 + 4
 19	standingFix = 2 + 2 + 2 + 2 + 1 // a standing's bytes before its per-clan counts
 20	clanSize    = 2 + 4 + 4 + 4
 21
 22	planBits   = 20 // bits per round in a side's plans: 2 for the length, 6 per card
 23	cardBits   = 6
 24	maxPlanLen = 3
 25	maxRound   = 3
 26	maxPool    = 16 // cards in a clan's pool; a deck is one bit per pool card
 27)
 28
 29// ---- Bit fields
 30
 31// bitsAt returns the width bits of v starting at bit at.
 32func bitsAt(v uint64, at, width uint) uint64 {
 33	return (v >> at) & mask(width)
 34}
 35
 36// withBits returns v with the width bits at bit at replaced by x.
 37func withBits(v uint64, at, width uint, x uint64) uint64 {
 38	if x&^mask(width) != 0 {
 39		panic("value does not fit")
 40	}
 41	return v&^(mask(width)<<at) | x<<at
 42}
 43
 44func mask(width uint) uint64 {
 45	if width >= 64 {
 46		return ^uint64(0)
 47	}
 48	return 1<<width - 1
 49}
 50
 51// ---- Plans
 52
 53// planAt returns the plan made for round, or nil when it is not yet made.
 54func planAt(plans uint64, round int) engine.Plan {
 55	at := planOffset(round)
 56	n := int(bitsAt(plans, at, 2))
 57	if n == 0 {
 58		return nil
 59	}
 60	p := make(engine.Plan, n)
 61	for i := range p {
 62		p[i] = engine.CardID(bitsAt(plans, at+2+uint(i)*cardBits, cardBits))
 63	}
 64	return p
 65}
 66
 67// withPlan returns plans with round's plan set to p.
 68func withPlan(plans uint64, round int, p engine.Plan) uint64 {
 69	if len(p) < 1 || len(p) > maxPlanLen {
 70		panic("invalid plan")
 71	}
 72	at := planOffset(round)
 73	v := withBits(0, 0, 2, uint64(len(p)))
 74	for i, id := range p {
 75		if id < 0 || id >= 1<<cardBits {
 76			panic("invalid plan")
 77		}
 78		v = withBits(v, 2+uint(i)*cardBits, cardBits, uint64(id))
 79	}
 80	return withBits(plans, at, planBits, 0) | v<<at
 81}
 82
 83// plansMade counts the rounds from the first that have a plan.
 84func plansMade(plans uint64) int {
 85	n := 0
 86	for n < maxRound && bitsAt(plans, planOffset(n+1), 2) != 0 {
 87		n++
 88	}
 89	return n
 90}
 91
 92func planOffset(round int) uint {
 93	if round < 1 || round > maxRound {
 94		panic("invalid plan")
 95	}
 96	return uint(round-1) * planBits
 97}
 98
 99// ---- Decks
100
101// deckOnPool returns deck, a set of card ids, as one bit per card of pool in id order.
102func deckOnPool(pool, deck uint64) uint16 {
103	if cardsIn(pool) > maxPool {
104		panic("pool holds more than 16 cards")
105	}
106	if deck&^pool != 0 {
107		panic("deck outside the pool")
108	}
109	var bits uint16
110	k := uint(0)
111	for id := uint(0); id < 64; id++ {
112		if pool&(1<<id) == 0 {
113			continue
114		}
115		if deck&(1<<id) != 0 {
116			bits |= 1 << k
117		}
118		k++
119	}
120	return bits
121}
122
123// deckFromPool is the inverse of deckOnPool.
124func deckFromPool(pool uint64, bits uint16) uint64 {
125	if cardsIn(pool) > maxPool {
126		panic("pool holds more than 16 cards")
127	}
128	var deck uint64
129	k := uint(0)
130	for id := uint(0); id < 64; id++ {
131		if pool&(1<<id) == 0 {
132			continue
133		}
134		if bits&(1<<k) != 0 {
135			deck |= 1 << id
136		}
137		k++
138	}
139	if uint64(bits)>>k != 0 {
140		panic("deck outside the pool")
141	}
142	return deck
143}
144
145func cardsIn(pool uint64) uint {
146	n := uint(0)
147	for ; pool != 0; pool &= pool - 1 {
148		n++
149	}
150	return n
151}
152
153// ---- Byte reading and writing
154
155type writer struct{ b []byte }
156
157func (w *writer) u8(x uint64)   { w.b = append(w.b, byte(x)) }
158func (w *writer) text(s string) { w.b = append(w.b, s...) }
159
160func (w *writer) u16(x uint64) {
161	var t [2]byte
162	binary.BigEndian.PutUint16(t[:], uint16(x))
163	w.b = append(w.b, t[:]...)
164}
165
166func (w *writer) u32(x uint64) {
167	var t [4]byte
168	binary.BigEndian.PutUint32(t[:], uint32(x))
169	w.b = append(w.b, t[:]...)
170}
171
172func (w *writer) u64(x uint64) {
173	var t [8]byte
174	binary.BigEndian.PutUint64(t[:], x)
175	w.b = append(w.b, t[:]...)
176}
177
178// addr writes a's 40 characters.
179func (w *writer) addr(a address) {
180	if len(a) != addressSize {
181		panic("invalid address")
182	}
183	w.text(string(a))
184}
185
186// seal writes a seal, 64 hex characters, as 32 raw bytes.
187func (w *writer) seal(s string) {
188	raw, err := hex.DecodeString(s)
189	if err != nil || len(raw) != sealSize {
190		panic("invalid seal")
191	}
192	w.b = append(w.b, raw...)
193}
194
195func (w *writer) String() string { return string(w.b) }
196
197type reader struct {
198	s  string
199	at int
200}
201
202func (r *reader) left() int { return len(r.s) - r.at }
203
204func (r *reader) text(n int) string {
205	if r.left() < n {
206		panic("truncated record")
207	}
208	s := r.s[r.at : r.at+n]
209	r.at += n
210	return s
211}
212
213func (r *reader) u8() uint64    { return uint64(r.text(1)[0]) }
214func (r *reader) u16() uint64   { return uint64(binary.BigEndian.Uint16([]byte(r.text(2)))) }
215func (r *reader) u32() uint64   { return uint64(binary.BigEndian.Uint32([]byte(r.text(4)))) }
216func (r *reader) u64() uint64   { return binary.BigEndian.Uint64([]byte(r.text(8))) }
217func (r *reader) addr() address { return address(r.text(addressSize)) }
218
219// seal reads 32 raw bytes as 64 hex characters.
220func (r *reader) seal() string { return hex.EncodeToString([]byte(r.text(sealSize))) }
221
222// ---- Match record
223
224// record is a match as the arena works on it, decoded from its stored bytes. clans holds
225// each side's index in the ruleset plus one, 0 for none; decks and plans are the engine's
226// card sets and the packed plans of withPlan; hp is the HP after each round, per side;
227// carried holds only the Vulnerable and Strength left after the last resolved round.
228// deckSeal and seal are 64 hex characters; box is the raw bytes, up to maxBox, stored with a 2-byte length.
229type record struct {
230	phase, result, reason string
231	clans                 [2]int
232	deadline              int64
233	decks                 [2]uint64
234	plans                 [2]uint64
235	hp                    [3][2]int
236	carried               [2]engine.Vitals
237	challenger, acceptor  address
238	opponent              address
239	deckSeal, seal, box   string
240}
241
242const (
243	flagOpponent = 1 << iota
244	flagDeckSeal
245	flagSealBox
246)
247
248// The value of each name is its index; 0 of result and reason is none.
249var (
250	phaseNames  = []string{phaseOpen, phaseReveal, phasePlay, phaseEnded}
251	resultNames = []string{"", "a", "b", "draw"}
252	reasonNames = []string{"", "hp", "knockout", "forfeit", "resign"}
253)
254
255func nameIndex(names []string, s, what string) uint64 {
256	for i, n := range names {
257		if n == s {
258			return uint64(i)
259		}
260	}
261	panic("unknown " + what)
262}
263
264func nameAt(names []string, i uint64, what string) string {
265	if i >= uint64(len(names)) {
266		panic("unknown " + what)
267	}
268	return names[i]
269}
270
271func encodeMatch(rules *engine.Ruleset, m record) string {
272	var flags uint64
273	if m.opponent != "" {
274		flags |= flagOpponent
275	}
276	if m.deckSeal != "" {
277		flags |= flagDeckSeal
278	}
279	if m.seal != "" || m.box != "" {
280		flags |= flagSealBox
281	}
282
283	w := &writer{}
284	w.u8(nameIndex(phaseNames, m.phase, "phase") |
285		nameIndex(resultNames, m.result, "result")<<3 |
286		nameIndex(reasonNames, m.reason, "reason")<<5)
287	w.u8(clanNibble(rules, m.clans[0])<<4 | clanNibble(rules, m.clans[1]))
288	w.u8(flags)
289	w.u32(uint64(m.deadline))
290	for side := 0; side < 2; side++ {
291		w.u16(uint64(deckOnPool(poolOf(rules, m.clans[side]), m.decks[side])))
292	}
293	w.u64(m.plans[0])
294	w.u64(m.plans[1])
295	for _, round := range m.hp {
296		for _, hp := range round {
297			if hp < -128 || hp > 127 {
298				panic("hp does not fit a byte")
299			}
300			w.u8(uint64(int8(hp)))
301		}
302	}
303	for _, v := range m.carried {
304		if v.Vulnerable < 0 || v.Vulnerable > 15 || v.Strength < 0 || v.Strength > 15 {
305			panic("carried state does not fit")
306		}
307		w.u8(uint64(v.Vulnerable)<<4 | uint64(v.Strength))
308	}
309	w.addr(m.challenger)
310	if m.phase != phaseOpen {
311		w.addr(m.acceptor)
312	}
313	if flags&flagOpponent != 0 {
314		w.addr(m.opponent)
315	}
316	if flags&flagDeckSeal != 0 {
317		w.seal(m.deckSeal)
318	}
319	if flags&flagSealBox != 0 {
320		if len(m.box) > maxBox {
321			panic("box too long")
322		}
323		w.seal(m.seal)
324		w.u16(uint64(len(m.box)))
325		w.text(m.box)
326	}
327	return w.String()
328}
329
330func decodeMatch(rules *engine.Ruleset, b string) record {
331	var m record
332	r := &reader{s: b}
333	first := r.u8()
334	m.phase = nameAt(phaseNames, first&7, "phase")
335	m.result = nameAt(resultNames, first>>3&3, "result")
336	m.reason = nameAt(reasonNames, first>>5, "reason")
337	clans := r.u8()
338	m.clans = [2]int{int(clans >> 4), int(clans & 15)}
339	flags := r.u8()
340	m.deadline = int64(r.u32())
341	for side := 0; side < 2; side++ {
342		m.decks[side] = deckFromPool(poolOf(rules, m.clans[side]), uint16(r.u16()))
343	}
344	m.plans[0] = r.u64()
345	m.plans[1] = r.u64()
346	for round := range m.hp {
347		for side := range m.hp[round] {
348			m.hp[round][side] = int(int8(r.u8()))
349		}
350	}
351	for side := range m.carried {
352		v := r.u8()
353		m.carried[side] = engine.Vitals{Vulnerable: int(v >> 4), Strength: int(v & 15)}
354	}
355	m.challenger = r.addr()
356	if m.phase != phaseOpen {
357		m.acceptor = r.addr()
358	}
359	if flags&flagOpponent != 0 {
360		m.opponent = r.addr()
361	}
362	if flags&flagDeckSeal != 0 {
363		m.deckSeal = r.seal()
364	}
365	if flags&flagSealBox != 0 {
366		m.seal = r.seal()
367		m.box = r.text(int(r.u16()))
368	}
369	if r.left() != 0 {
370		panic("trailing bytes in record")
371	}
372	return m
373}
374
375// clanNibble is a clan's stored form, its ruleset index plus one, checked against rules.
376func clanNibble(rules *engine.Ruleset, clan int) uint64 {
377	poolOf(rules, clan)
378	return uint64(clan)
379}
380
381// poolOf returns the card pool of clan, 0 for no clan.
382func poolOf(rules *engine.Ruleset, clan int) uint64 {
383	if clan == 0 {
384		return 0
385	}
386	if clan < 0 || clan > len(rules.Clans) {
387		panic("unknown clan")
388	}
389	return rules.Clans[clan-1].Pool
390}
391
392// ---- Chunks
393
394// A chunk holds, for each match id in order, a 2-byte record length and the record. A
395// removed match keeps its place with length 0.
396
397// chunkRecord returns the record in slot, or "" when it is removed or beyond the chunk.
398func chunkRecord(chunk string, slot int) string {
399	r := &reader{s: chunk}
400	for i := 0; r.left() > 0; i++ {
401		rec := r.text(int(r.u16()))
402		if i == slot {
403			return rec
404		}
405	}
406	return ""
407}
408
409// withChunkRecord returns chunk with slot's record set to rec, adding empty slots up to it.
410func withChunkRecord(chunk string, slot int, rec string) string {
411	if slot < 0 || slot >= chunkSize {
412		panic("slot outside the chunk")
413	}
414	if len(rec) > 0xFFFF {
415		panic("record too long")
416	}
417	r := &reader{s: chunk}
418	w := &writer{}
419	for i := 0; i <= slot || r.left() > 0; i++ {
420		var old string
421		if r.left() > 0 {
422			old = r.text(int(r.u16()))
423		}
424		if i == slot {
425			old = rec
426		}
427		w.u16(uint64(len(old)))
428		w.text(old)
429	}
430	return w.String()
431}
432
433// ---- Players
434
435// playerRecord is what the arena keeps per player: the ids of their matches, oldest
436// first, their standing, nil until their first rated match, and the session locks that
437// bind a session key to one match.
438type playerRecord struct {
439	ids      []int
440	standing *standing
441	locks    []sessionLock
442}
443
444// standing is a player's rated record. lastClan is the clan number, index plus one, of
445// their last rated match; perClan[i] counts their rated matches with clan number i + 1.
446type standing struct {
447	rating, wins, losses, draws int
448	lastClan                    int
449	perClan                     []int
450}
451
452type sessionLock struct {
453	session address
454	id      int
455	expires int64
456}
457
458// A player's bytes are a 2-byte id count, the ids at 4 bytes each, a 1-byte standing
459// length, 0 for none, the standing, then the locks. A standing is the rating, wins, losses
460// and draws at 2 bytes each, the last clan at 1, then a 2-byte count per clan.
461// ceiling: joining a match or changing a lock rewrites all of a player's ids, up to 65,535, so
462// gas grows with their matches; an avl index of ids per player if some play thousands.
463func decodePlayer(s string) playerRecord {
464	var p playerRecord
465	if s == "" {
466		return p
467	}
468	r := &reader{s: s}
469	for n := r.u16(); n > 0; n-- {
470		p.ids = append(p.ids, int(r.u32()))
471	}
472	p.standing = readStanding(r)
473	if r.left()%lockSize != 0 {
474		panic("malformed player")
475	}
476	for r.left() > 0 {
477		p.locks = append(p.locks, sessionLock{session: r.addr(), id: int(r.u32()), expires: int64(r.u32())})
478	}
479	return p
480}
481
482// decodeStanding reads only the standing of a player's bytes, skipping their ids, so
483// its cost does not grow with the player's matches; nil when they have none.
484func decodeStanding(s string) *standing {
485	if s == "" {
486		return nil
487	}
488	r := &reader{s: s}
489	r.text(4 * int(r.u16()))
490	return readStanding(r)
491}
492
493// withStanding returns a player's bytes s with their standing replaced by st. It copies the
494// ids and locks without reading them, so its cost does not grow with the player's matches.
495func withStanding(s string, st *standing) string {
496	if s == "" {
497		return encodePlayer(playerRecord{standing: st})
498	}
499	r := &reader{s: s}
500	r.text(4 * int(r.u16()))
501	head := r.at
502	readStanding(r)
503	w := &writer{}
504	w.text(s[:head])
505	writeStanding(w, st)
506	w.text(s[r.at:])
507	return w.String()
508}
509
510func readStanding(r *reader) *standing {
511	n := int(r.u8())
512	if n == 0 {
513		return nil
514	}
515	if n < standingFix || (n-standingFix)%2 != 0 {
516		panic("malformed player")
517	}
518	st := &standing{rating: int(r.u16()), wins: int(r.u16()), losses: int(r.u16()), draws: int(r.u16()), lastClan: int(r.u8())}
519	for i := standingFix; i < n; i += 2 {
520		st.perClan = append(st.perClan, int(r.u16()))
521	}
522	return st
523}
524
525func encodePlayer(p playerRecord) string {
526	if len(p.ids) > 0xFFFF {
527		panic("too many matches")
528	}
529	w := &writer{}
530	w.u16(uint64(len(p.ids)))
531	for _, id := range p.ids {
532		w.u32(uint64(id))
533	}
534	writeStanding(w, p.standing)
535	for _, l := range p.locks {
536		w.addr(l.session)
537		w.u32(uint64(l.id))
538		w.u32(uint64(l.expires))
539	}
540	return w.String()
541}
542
543func writeStanding(w *writer, st *standing) {
544	if st == nil {
545		w.u8(0)
546		return
547	}
548	w.u8(uint64(standingFix + 2*len(st.perClan)))
549	w.u16(uint64(st.rating))
550	w.u16(uint64(st.wins))
551	w.u16(uint64(st.losses))
552	w.u16(uint64(st.draws))
553	w.u8(uint64(st.lastClan))
554	for _, n := range st.perClan {
555		w.u16(uint64(n))
556	}
557}
558
559// ladderKey orders the ladder: highest rating first, then address order.
560func ladderKey(rating int, player address) string {
561	w := &writer{}
562	w.u16(uint64(maxRating - rating))
563	w.text(string(player))
564	return w.String()
565}
566
567// ---- Clan records
568
569// clanRecord is a clan's rating and its record in rated matches against other clans.
570type clanRecord struct {
571	rating, wins, losses, draws int
572}
573
574// The clan records are one string holding, per clan in ruleset order, the rating at 2
575// bytes and the wins, losses and draws at 4 each. decodeClans returns n clans; a clan
576// past the end of s has no rated match yet.
577func decodeClans(s string, n int) []clanRecord {
578	if len(s)%clanSize != 0 {
579		panic("malformed clans")
580	}
581	r := &reader{s: s}
582	c := make([]clanRecord, n)
583	for i := range c {
584		if r.left() == 0 {
585			c[i] = clanRecord{rating: startRating}
586			continue
587		}
588		c[i] = clanRecord{rating: int(r.u16()), wins: int(r.u32()), losses: int(r.u32()), draws: int(r.u32())}
589	}
590	return c
591}
592
593func encodeClans(c []clanRecord) string {
594	w := &writer{}
595	for _, x := range c {
596		w.u16(uint64(x.rating))
597		w.u32(uint64(x.wins))
598		w.u32(uint64(x.losses))
599		w.u32(uint64(x.draws))
600	}
601	return w.String()
602}
603
604// ---- Open challenges
605
606// openEntry is a challenge on the board: its match id and expiry in Unix seconds.
607type openEntry struct {
608	id      int
609	expires int64
610}
611
612func openEntries(s string) []openEntry {
613	if len(s)%openSize != 0 {
614		panic("malformed open list")
615	}
616	r := &reader{s: s}
617	var entries []openEntry
618	for r.left() > 0 {
619		entries = append(entries, openEntry{id: int(r.u32()), expires: int64(r.u32())})
620	}
621	return entries
622}
623
624func encodeOpen(entries []openEntry) string {
625	w := &writer{}
626	for _, e := range entries {
627		w.u32(uint64(e.id))
628		w.u32(uint64(e.expires))
629	}
630	return w.String()
631}