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

kills.gno

3.07 Kb · 108 lines
  1package game
  2
  3import (
  4	"chain"
  5	"errors"
  6)
  7
  8const (
  9	maxKillEntries = 32   // players per SubmitKills call
 10	maxKillCount   = 1000 // kills per player per call
 11
 12	// maxKillsLen bounds a kills string, so that an oversized input is
 13	// refused before it is read. An address is 40 characters, a count 4 at
 14	// most, plus the two separators: 64 a player leaves room to spare.
 15	maxKillsLen = maxKillEntries * 64
 16)
 17
 18var errKillsFormat = errors.New(`gnofly: kills must be 1 to 32 entries "address:count" separated by ";"`)
 19
 20// SubmitKills adds kills to players' on-chain counts. Referee only.
 21//
 22// kills is 1 to 32 entries "address:count" separated by ";", for example
 23//
 24//	g1abc...:3;g1def...:1
 25//
 26// Each address must be valid and listed once, each count from 1 to 1000.
 27// Anything else aborts the transaction and nothing is recorded. The counts
 28// are added to what the players already have, and the kill leaderboard is
 29// updated. Kills are player actions: Version does not move.
 30func SubmitKills(cur realm, kills string) {
 31	referee := assertReferee(0, cur)
 32	addrs, counts, err := parseKills(kills)
 33	if err != nil {
 34		panic(err)
 35	}
 36
 37	size := leaderboardSize()
 38	added := int64(0)
 39	for i, addr := range addrs {
 40		total := recordedKills(addr) + counts[i]
 41		pilots.Set(addr.String(), total)
 42
 43		if next, changed := killers.raise(addr, total-counts[i], total, size); changed {
 44			killers = next
 45		}
 46		added += counts[i]
 47	}
 48	chain.Emit("KillsSubmitted",
 49		"referee", referee.String(),
 50		"players", itoa(int64(len(addrs))),
 51		"kills", itoa(added),
 52	)
 53}
 54
 55// parseKills parses and validates a kills string in a single pass. It
 56// returns the players and their counts, in the order of the string.
 57func parseKills(s string) ([]address, []int64, error) {
 58	if s == "" || len(s) > maxKillsLen {
 59		return nil, nil, errKillsFormat
 60	}
 61	var addrs []address
 62	var counts []int64
 63	seen := make(map[address]bool) // lookups only, never iterated
 64	i := 0
 65	for {
 66		index := len(addrs)
 67		if index == maxKillEntries {
 68			return nil, nil, errKillsFormat
 69		}
 70		colon := indexFrom(s, i, ':')
 71		if colon == len(s) {
 72			return nil, nil, errKillsFormat
 73		}
 74		end := indexFrom(s, colon+1, ';')
 75
 76		addr := address(s[i:colon])
 77		if !validAddress(addr) {
 78			return nil, nil, killsEntryError(index, "invalid address")
 79		}
 80		if seen[addr] {
 81			return nil, nil, killsEntryError(index, "address listed twice")
 82		}
 83		// At most 4 digits: 1000 is the highest count, and a bounded length
 84		// keeps the number from overflowing.
 85		if end-colon-1 > 4 {
 86			return nil, nil, killsEntryError(index, "count must be a number from 1 to 1000")
 87		}
 88		count, ok := digits(s[colon+1 : end])
 89		if !ok || count < 1 || count > maxKillCount {
 90			return nil, nil, killsEntryError(index, "count must be a number from 1 to 1000")
 91		}
 92		seen[addr] = true
 93		addrs = append(addrs, addr)
 94		counts = append(counts, count)
 95
 96		if end == len(s) {
 97			return addrs, counts, nil
 98		}
 99		i = end + 1
100		if i == len(s) {
101			return nil, nil, errKillsFormat // a trailing ";"
102		}
103	}
104}
105
106func killsEntryError(index int, reason string) error {
107	return errors.New("gnofly: kills entry " + itoa(int64(index)) + ": " + reason)
108}