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}