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

payout.gno

5.62 Kb · 186 lines
  1package payout
  2
  3import "errors"
  4
  5// Death is one life lost in a round: Victim was shot down by Killer, or by
  6// nobody (a crash, a disconnect). A pilot dies up to Game.Lives times and is
  7// out after the last one.
  8type Death struct {
  9	Victim address
 10	Killer address // "" when nobody shot the victim down
 11}
 12
 13// Game is what a rule needs to know about the arena a round was played in.
 14type Game struct {
 15	Stake      int64 // ugnot, what each player paid
 16	Lives      int64 // deaths a player can take, the last one puts them out
 17	KillTarget int64 // kills that end the round, 0 for the clock only
 18	FeeBps     int64 // the house's share, in basis points
 19}
 20
 21// Payout is an amount of ugnot to send to an address.
 22type Payout struct {
 23	Address address
 24	Amount  int64 // ugnot
 25}
 26
 27// Result is what a rule decides: who gets what, and the house's cut.
 28type Result struct {
 29	Payouts []Payout // in the order of the players, players paid nothing left out
 30	Fee     int64
 31}
 32
 33// Rule turns what happened in a round into who gets what.
 34type Rule interface {
 35	Mode() string
 36	Settle(g Game, players []address, deaths []Death) (Result, error)
 37}
 38
 39// The modes of the rules shipped with the package.
 40const (
 41	ModeBattleRoyale = "battle-royale"
 42	ModeDeathmatch   = "deathmatch"
 43)
 44
 45// Bounds every rule enforces.
 46const (
 47	MinPlayers    = 2
 48	MaxPlayers    = 8
 49	MinLives      = 1
 50	MaxLives      = 3
 51	MaxKillTarget = 20
 52	MinStake      = 1             // ugnot
 53	MaxStake      = 1000000000000 // ugnot, 1e12
 54	BpsScale      = 10000         // basis points in a whole
 55	MaxFeeBps     = 5000          // the fee never takes more than half
 56	MaxDeaths     = MaxPlayers * MaxLives
 57)
 58
 59var (
 60	ErrStake           = errors.New("payout: stake must be between 1 and 1000000000000 ugnot")
 61	ErrLives           = errors.New("payout: lives must be between 1 and 3")
 62	ErrKillTarget      = errors.New("payout: the kill target must be between 0 and 20")
 63	ErrFee             = errors.New("payout: the fee must be between 0 and 5000 bps")
 64	ErrTooFewPlayers   = errors.New("payout: at least 2 players are required")
 65	ErrTooManyPlayers  = errors.New("payout: at most 8 players are allowed")
 66	ErrEmptyPlayer     = errors.New("payout: a player has no address")
 67	ErrDuplicatePlayer = errors.New("payout: a player is listed twice")
 68	ErrUnknownVictim   = errors.New("payout: a victim is not a player of the round")
 69	ErrDeadTooOften    = errors.New("payout: a player died more often than they have lives")
 70	ErrUnknownKiller   = errors.New("payout: a killer is not a player of the round")
 71	ErrSelfKill        = errors.New("payout: a player cannot be their own killer")
 72	ErrTooManyDeaths   = errors.New("payout: more deaths than lives in the round")
 73
 74	ErrNoPlayers     = errors.New("payout: players must list at least one address")
 75	ErrPlayersFormat = errors.New("payout: players must be comma-separated addresses")
 76	ErrDeathsFormat  = errors.New(`payout: deaths must be "victim:killer" entries separated by ";"`)
 77)
 78
 79// Lookup returns the rule of a mode, or nil for a mode nobody knows.
 80func Lookup(mode string) Rule {
 81	switch mode {
 82	case ModeBattleRoyale:
 83		return BattleRoyale{}
 84	case ModeDeathmatch:
 85		return Deathmatch{}
 86	}
 87	return nil
 88}
 89
 90// Modes lists the modes of the shipped rules. The slice is a fresh copy.
 91func Modes() []string {
 92	return []string{ModeBattleRoyale, ModeDeathmatch}
 93}
 94
 95// tally is what happened in a round, per player.
 96type tally struct {
 97	deaths  []int64 // lives each player lost
 98	kills   []int64 // deaths credited to each player
 99	lastOut int     // the player whose last life went last, -1 when nobody is out
100	first   int     // the first player to reach the kill target, -1 when nobody did
101}
102
103// out reports whether player i lost every life.
104func (t tally) out(i int, lives int64) bool {
105	return t.deaths[i] >= lives
106}
107
108// check enforces what every rule requires, and counts deaths and kills.
109//
110// A killer may be out already: a bullet still in flight. A dead pilot cannot
111// die again, though: a victim takes at most Lives deaths.
112func check(g Game, players []address, deaths []Death) (tally, error) {
113	if g.Stake < MinStake || g.Stake > MaxStake {
114		return tally{}, ErrStake
115	}
116	if g.Lives < MinLives || g.Lives > MaxLives {
117		return tally{}, ErrLives
118	}
119	if g.KillTarget < 0 || g.KillTarget > MaxKillTarget {
120		return tally{}, ErrKillTarget
121	}
122	if g.FeeBps < 0 || g.FeeBps > MaxFeeBps {
123		return tally{}, ErrFee
124	}
125	n := len(players)
126	if n < MinPlayers {
127		return tally{}, ErrTooFewPlayers
128	}
129	if n > MaxPlayers {
130		return tally{}, ErrTooManyPlayers
131	}
132	for i, p := range players {
133		if p == "" {
134			return tally{}, ErrEmptyPlayer
135		}
136		if indexOf(players[:i], p) >= 0 {
137			return tally{}, ErrDuplicatePlayer
138		}
139	}
140	if int64(len(deaths)) > int64(n)*g.Lives {
141		return tally{}, ErrTooManyDeaths
142	}
143
144	t := tally{deaths: make([]int64, n), kills: make([]int64, n), lastOut: -1, first: -1}
145	for _, d := range deaths {
146		victim := indexOf(players, d.Victim)
147		if victim < 0 {
148			return tally{}, ErrUnknownVictim
149		}
150		if t.out(victim, g.Lives) {
151			return tally{}, ErrDeadTooOften
152		}
153		if d.Killer != "" {
154			if d.Killer == d.Victim {
155				return tally{}, ErrSelfKill
156			}
157			killer := indexOf(players, d.Killer)
158			if killer < 0 {
159				return tally{}, ErrUnknownKiller
160			}
161			t.kills[killer]++
162			if g.KillTarget > 0 && t.first < 0 && t.kills[killer] >= g.KillTarget {
163				t.first = killer
164			}
165		}
166		t.deaths[victim]++
167		if t.out(victim, g.Lives) {
168			t.lastOut = victim
169		}
170	}
171	return t, nil
172}
173
174func indexOf(list []address, addr address) int {
175	for i, a := range list {
176		if a == addr {
177			return i
178		}
179	}
180	return -1
181}
182
183// afterFee returns what is left of amount once the fee is taken.
184func afterFee(amount, feeBps int64) int64 {
185	return amount * (BpsScale - feeBps) / BpsScale
186}