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}