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.

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}