Transaction
CA35B82EA8B50E…B328CA103953
Block 677,879 · index 0 · indexed
Summary
- Hash
- CA35B82EA8B50EBD9E5C311F020A3FA786A9EAB1B2C5F946592EB328CA103953
- Block
- 677,879
- Size
- 16060 bytes
- Gas used
- 22,295,602 / 200,000,000
- Fee
- 200000ugnot
- Status
- success
Messages
- Attached funds
- 40000000ugnot
Arguments · 15
- #1payout
- #2battleroyale.gno
- #3package payout // BattleRoyale is the winner-takes-all rule. The pot is every stake; the fee is // taken from it, and the rest goes to: // // - the last player in; // - when the clock ran out with several in, the one of them with the most // kills, and players still in with as many kills share it equally; // - and when everyone is out, the player who went out last. // // It is won by staying in the air, not by scoring: the kill target is // ignored, and the arena realm refuses a battle-royale spec that has one. // // What the split leaves over goes to the fee. type BattleRoyale struct{} var _ Rule = BattleRoyale{} func (BattleRoyale) Mode() string { return ModeBattleRoyale } func (BattleRoyale) Settle(g Game, players []address, deaths []Death) (Result, error) { t, err := check(g, players, deaths) if err != nil { return Result{}, err } // The most kills among the players still in. best := int64(-1) for i := range players { if !t.out(i, g.Lives) && t.kills[i] > best { best = t.kills[i] } } won := make([]bool, len(players)) winners := int64(0) for i := range players { if !t.out(i, g.Lives) && t.kills[i] == best { won[i] = true winners++ } } if winners == 0 { won[t.lastOut] = true winners = 1 } pot := g.Stake * int64(len(players)) share := afterFee(pot, g.FeeBps) / winners res := Result{Fee: pot - share*winners} if share == 0 { return res, nil // a pot too small to split: nobody is paid } for i, p := range players { if won[i] { res.Payouts = append(res.Payouts, Payout{Address: p, Amount: share}) } } return res, nil }
- #4deathmatch.gno
- #5package payout // rankWeights are the shares of the top three by kills, in basis points. var rankWeights = [3]int64{6000, 3000, 1000} // Deathmatch pays the most kills. The pot is every stake; the fee is taken from // it, and the rest goes to the top three by kills: 60%, 30% and 10%. // // - Only players with at least one kill are ranked. With fewer than three // of them, the shares of the ranks they fill are rescaled to the whole: // one killer takes everything, two take 2/3 and 1/3. // - Players with as many kills share the ranks they hold: two tied first // split 60% + 30%, and the next one is third. // - When nobody killed anybody, every player gets their stake back and the // house takes nothing. // // Deaths cost nothing here beyond the lives they take. What rounding leaves // over goes to the fee. type Deathmatch struct{} var _ Rule = Deathmatch{} func (Deathmatch) Mode() string { return ModeDeathmatch } func (Deathmatch) Settle(g Game, players []address, deaths []Death) (Result, error) { t, err := check(g, players, deaths) if err != nil { return Result{}, err } pot := g.Stake * int64(len(players)) // Killers, most kills first; equal kills keep the order of the players. var ranked []int for i := range players { if t.kills[i] == 0 { continue } at := len(ranked) for at > 0 && t.kills[ranked[at-1]] < t.kills[i] { at-- } ranked = append(ranked, 0) copy(ranked[at+1:], ranked[at:]) ranked[at] = i } if len(ranked) == 0 { res := Result{} for _, p := range players { res.Payouts = append(res.Payouts, Payout{Address: p, Amount: g.Stake}) } return res, nil } filled := len(ranked) if filled > len(rankWeights) { filled = len(rankWeights) } whole := int64(0) for _, w := range rankWeights[:filled] { whole += w } prize := afterFee(pot, g.FeeBps) amounts := make([]int64, len(players)) for pos := 0; pos < filled; { end := pos + 1 for end < len(ranked) && t.kills[ranked[end]] == t.kills[ranked[pos]] { end++ } weight := int64(0) for r := pos; r < end && r < filled; r++ { weight += rankWeights[r] } group := int64(end - pos) // prize <= 8e12 and weight <= 1e4: the product stays far below 2^63. each := prize * weight / (whole * group) for _, i := range ranked[pos:end] { amounts[i] = each } pos = end } res := Result{Fee: pot} for i, p := range players { if amounts[i] > 0 { res.Payouts = append(res.Payouts, Payout{Address: p, Amount: amounts[i]}) res.Fee -= amounts[i] } } return res, nil }
- #6doc.gno
- #7// v0 - Unaudited: This is an initial version that has not yet been formally audited. // Use in production at your own risk. // // Package payout defines how the stakes of a gnofly arena round are shared // out once the round is over: a Rule turns what happened, who died and by // whose hand, into who gets what. // // The package is pure: it holds no state, moves no coin and knows nothing // about roles. A realm keeps the stakes, asks a rule for a Result and makes // the payments itself: // // players, err := payout.ParsePlayers("g1aaa...,g1bbb...,g1ccc...") // if err != nil { // panic(err) // } // deaths, err := payout.ParseDeaths("g1bbb...:g1aaa...;g1ccc...:") // if err != nil { // panic(err) // } // g := payout.Game{Stake: 10000000, Lives: 3, KillTarget: 0, FeeBps: 500} // res, err := payout.Lookup(payout.ModeDeathmatch).Settle(g, players, deaths) // // Two rules ship with the package, both stateless values with no field: // // - BattleRoyale: the pot goes to the players still in at the end, or to // the last one out; // - Deathmatch: the pot goes to the top three by kills, 60%, 30% and 10%. // // Every rule conserves the pot. For any input a rule accepts, // // sum(res.Payouts) + res.Fee == Stake * len(players) // // and nobody but a player of the round is ever paid. What is not paid to a // player is the house's cut, the fee. Amounts are rounded down, and what // rounding leaves over goes to the fee. A realm that pays out real coins // should still check both properties on the Result it gets before it sends // anything: that check is cheap, and it is what keeps a mistake in a rule // from becoming a loss. package payout
- #8gnomod.toml
- #9module = "gno.land/p/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/payout/v0" gno = "0.9"
- #10parse.gno
- #11package payout // Both parsers read their input in a single pass, byte by byte, and bound it // before they read it. On chain every interpreted operation is paid for, and // an oversized string must cost its sender nothing but a refusal. // // They only accept an address in its canonical form, see validAddress. const ( // addressLen is the length of a gno address: "g1" and 38 characters. addressLen = 40 // maxPlayersLen bounds a players string: MaxPlayers addresses, each // followed by a separator. maxPlayersLen = MaxPlayers * (addressLen + 1) // maxDeathsLen bounds a deaths string: MaxDeaths entries // of two addresses, each followed by a separator. maxDeathsLen = MaxDeaths * 2 * (addressLen + 1) ) // ParsePlayers parses a players string: comma-separated addresses, for // example // // g1aaa...,g1bbb...,g1ccc... // // It returns 1 to MaxPlayers addresses in the order of the string, each one // valid and listed once. Whitespace, empty entries and a trailing separator // are refused. Whether there are enough players for a round is decided by // Rule.Settle, and by the realm that starts the round. func ParsePlayers(s string) ([]address, error) { if s == "" { return nil, ErrNoPlayers } if len(s) > maxPlayersLen { return nil, ErrTooManyPlayers } var players []address i := 0 for { if len(players) == MaxPlayers { return nil, ErrTooManyPlayers } end := indexFrom(s, i, ',') addr := address(s[i:end]) if !validAddress(addr) { return nil, ErrPlayersFormat } if indexOf(players, addr) >= 0 { return nil, ErrDuplicatePlayer } players = append(players, addr) if end == len(s) { return players, nil } i = end + 1 if i == len(s) { return nil, ErrPlayersFormat // a trailing "," } } } // ParseDeaths parses a deaths string: "victim:killer" entries // separated by ";", in the order they happened, "victim:" when nobody shot // the victim down, for example // // g1bbb...:g1aaa...;g1ccc...: // // The empty string means nobody died and gives an empty list. At // most MaxDeaths entries are accepted, and every address must be valid. // Whitespace, empty entries and a trailing separator are refused. // // It only checks the syntax. Whether the victims and the killers are players // of the round, and how often they died, is decided by Rule.Settle. func ParseDeaths(s string) ([]Death, error) { if s == "" { return nil, nil } if len(s) > maxDeathsLen { return nil, ErrTooManyDeaths } var out []Death i := 0 for { if len(out) == MaxDeaths { return nil, ErrTooManyDeaths } colon := indexFrom(s, i, ':') if colon == len(s) { return nil, ErrDeathsFormat } end := indexFrom(s, colon+1, ';') victim := address(s[i:colon]) if !validAddress(victim) { return nil, ErrDeathsFormat } killer := address(s[colon+1 : end]) if killer != "" && !validAddress(killer) { return nil, ErrDeathsFormat } out = append(out, Death{Victim: victim, Killer: killer}) if end == len(s) { return out, nil } i = end + 1 if i == len(s) { return nil, ErrDeathsFormat // a trailing ";" } } } // validAddress reports whether addr is a valid address in its canonical, // lowercase form. // // Bech32 can also be written in all capitals, and address.IsValid accepts // that. To a realm the capitals would be a second name for the same account, // one that matches no player of a round. Bech32 does not allow mixing cases, // so looking at the first character is enough. The length is checked first: // it is free, and it keeps an overlong string away from the decoder. func validAddress(addr address) bool { return len(addr) == addressLen && addr[0] == 'g' && addr.IsValid() } // indexFrom returns the index of the first c in s at or after i, or len(s) // when there is none. func indexFrom(s string, i int, c byte) int { for i < len(s) && s[i] != c { i++ } return i }
- #12payout.gno
- #13package payout import "errors" // Death is one life lost in a round: Victim was shot down by Killer, or by // nobody (a crash, a disconnect). A pilot dies up to Game.Lives times and is // out after the last one. type Death struct { Victim address Killer address // "" when nobody shot the victim down } // Game is what a rule needs to know about the arena a round was played in. type Game struct { Stake int64 // ugnot, what each player paid Lives int64 // deaths a player can take, the last one puts them out KillTarget int64 // kills that end the round, 0 for the clock only FeeBps int64 // the house's share, in basis points } // Payout is an amount of ugnot to send to an address. type Payout struct { Address address Amount int64 // ugnot } // Result is what a rule decides: who gets what, and the house's cut. type Result struct { Payouts []Payout // in the order of the players, players paid nothing left out Fee int64 } // Rule turns what happened in a round into who gets what. type Rule interface { Mode() string Settle(g Game, players []address, deaths []Death) (Result, error) } // The modes of the rules shipped with the package. const ( ModeBattleRoyale = "battle-royale" ModeDeathmatch = "deathmatch" ) // Bounds every rule enforces. const ( MinPlayers = 2 MaxPlayers = 8 MinLives = 1 MaxLives = 3 MaxKillTarget = 20 MinStake = 1 // ugnot MaxStake = 1000000000000 // ugnot, 1e12 BpsScale = 10000 // basis points in a whole MaxFeeBps = 5000 // the fee never takes more than half MaxDeaths = MaxPlayers * MaxLives ) var ( ErrStake = errors.New("payout: stake must be between 1 and 1000000000000 ugnot") ErrLives = errors.New("payout: lives must be between 1 and 3") ErrKillTarget = errors.New("payout: the kill target must be between 0 and 20") ErrFee = errors.New("payout: the fee must be between 0 and 5000 bps") ErrTooFewPlayers = errors.New("payout: at least 2 players are required") ErrTooManyPlayers = errors.New("payout: at most 8 players are allowed") ErrEmptyPlayer = errors.New("payout: a player has no address") ErrDuplicatePlayer = errors.New("payout: a player is listed twice") ErrUnknownVictim = errors.New("payout: a victim is not a player of the round") ErrDeadTooOften = errors.New("payout: a player died more often than they have lives") ErrUnknownKiller = errors.New("payout: a killer is not a player of the round") ErrSelfKill = errors.New("payout: a player cannot be their own killer") ErrTooManyDeaths = errors.New("payout: more deaths than lives in the round") ErrNoPlayers = errors.New("payout: players must list at least one address") ErrPlayersFormat = errors.New("payout: players must be comma-separated addresses") ErrDeathsFormat = errors.New(`payout: deaths must be "victim:killer" entries separated by ";"`) ) // Lookup returns the rule of a mode, or nil for a mode nobody knows. func Lookup(mode string) Rule { switch mode { case ModeBattleRoyale: return BattleRoyale{} case ModeDeathmatch: return Deathmatch{} } return nil } // Modes lists the modes of the shipped rules. The slice is a fresh copy. func Modes() []string { return []string{ModeBattleRoyale, ModeDeathmatch} } // tally is what happened in a round, per player. type tally struct { deaths []int64 // lives each player lost kills []int64 // deaths credited to each player lastOut int // the player whose last life went last, -1 when nobody is out first int // the first player to reach the kill target, -1 when nobody did } // out reports whether player i lost every life. func (t tally) out(i int, lives int64) bool { return t.deaths[i] >= lives } // check enforces what every rule requires, and counts deaths and kills. // // A killer may be out already: a bullet still in flight. A dead pilot cannot // die again, though: a victim takes at most Lives deaths. func check(g Game, players []address, deaths []Death) (tally, error) { if g.Stake < MinStake || g.Stake > MaxStake { return tally{}, ErrStake } if g.Lives < MinLives || g.Lives > MaxLives { return tally{}, ErrLives } if g.KillTarget < 0 || g.KillTarget > MaxKillTarget { return tally{}, ErrKillTarget } if g.FeeBps < 0 || g.FeeBps > MaxFeeBps { return tally{}, ErrFee } n := len(players) if n < MinPlayers { return tally{}, ErrTooFewPlayers } if n > MaxPlayers { return tally{}, ErrTooManyPlayers } for i, p := range players { if p == "" { return tally{}, ErrEmptyPlayer } if indexOf(players[:i], p) >= 0 { return tally{}, ErrDuplicatePlayer } } if int64(len(deaths)) > int64(n)*g.Lives { return tally{}, ErrTooManyDeaths } t := tally{deaths: make([]int64, n), kills: make([]int64, n), lastOut: -1, first: -1} for _, d := range deaths { victim := indexOf(players, d.Victim) if victim < 0 { return tally{}, ErrUnknownVictim } if t.out(victim, g.Lives) { return tally{}, ErrDeadTooOften } if d.Killer != "" { if d.Killer == d.Victim { return tally{}, ErrSelfKill } killer := indexOf(players, d.Killer) if killer < 0 { return tally{}, ErrUnknownKiller } t.kills[killer]++ if g.KillTarget > 0 && t.first < 0 && t.kills[killer] >= g.KillTarget { t.first = killer } } t.deaths[victim]++ if t.out(victim, g.Lives) { t.lastOut = victim } } return t, nil } func indexOf(list []address, addr address) int { for i, a := range list { if a == addr { return i } } return -1 } // afterFee returns what is left of amount once the fee is taken. func afterFee(amount, feeBps int64) int64 { return amount * (BpsScale - feeBps) / BpsScale }
- #14/gno.MemPackageType
- #15 MPUserAll
Result log
msg:0,success:true,log:,events:[]