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.

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}