stakes.gno
5.11 Kb · 169 lines
1package gnoslinger
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "strconv"
8
9 "gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/engine/v0"
10)
11
12// Stakes (this project's, not the board game's): a showdown may be played
13// for GNOT. The creator sends the stake with CreateGame, and every joiner
14// sends the same with JoinGame; the realm holds them, the pot. When the
15// showdown ends the house keeps HouseCutPercent of the pot for events, and
16// the winner takes the rest; a draw splits it between the seats tied for
17// the most victory points (what does not divide evenly goes to the house).
18// A showdown that ends with nobody left in the fight, everyone out of time,
19// or cancelled before it starts, gives every stake back in full. The admin, the deployer at
20// first, may send the house's funds out (WithdrawHouse): a later version of
21// the realm cannot reach this one's balance.
22
23const (
24 // Denom is the only coin a stake is paid in.
25 Denom = "ugnot"
26 // HouseCutPercent is the share of a pot the house keeps, for events.
27 HouseCutPercent = 5
28)
29
30var (
31 // house is what the realm holds for events: the house's cuts, less
32 // what the admin withdrew. The rest of the realm's balance is the pots
33 // of the showdowns not over yet.
34 house int64
35 // admin may withdraw the house's funds; the deployer at first.
36 admin = unsafe.OriginCaller()
37)
38
39// sentStake returns the ugnot sent with the call, 0 for none. Coins can
40// only come straight from a user's call: a realm in between could have
41// spent the send it reports (unsafe.OriginSend is the whole message's).
42func sentStake(cur realm) int64 {
43 sent := unsafe.OriginSend()
44 if len(sent) == 0 {
45 return 0
46 }
47 if !cur.Previous().IsUserCall() {
48 panic("gnoslinger: a stake can only be sent by a direct call")
49 }
50 if len(sent) != 1 || sent[0].Denom != Denom || sent[0].Amount <= 0 {
51 panic("gnoslinger: a stake is paid in " + Denom + " only")
52 }
53 return sent[0].Amount
54}
55
56// assertNoSend refuses coins sent with a call that takes none: they would
57// sit in the realm's balance with no showdown to pay them out.
58func assertNoSend() {
59 if len(unsafe.OriginSend()) > 0 {
60 panic("gnoslinger: this call takes no coins")
61 }
62}
63
64// pay sends amount ugnot from the realm to addr.
65func pay(cur realm, to address, amount int64) {
66 if amount <= 0 {
67 return
68 }
69 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(cur.Address(), to, chain.Coins{chain.NewCoin(Denom, amount)})
70}
71
72// refundStakes gives every seated player its stake back: the showdown
73// was cancelled, or nobody was left to win it. Payouts records it, so a finished game says
74// what each seat got.
75func (g *Game) refundStakes(cur realm) {
76 if g.Pot == 0 {
77 return
78 }
79 g.Payouts = make([]int64, g.seats())
80 for i, p := range g.Players {
81 if p.Addr == "" {
82 continue
83 }
84 pay(cur, p.Addr, g.Stake)
85 g.Payouts[i] = g.Stake
86 emit("StakeRefunded", "id", g.ID, "seat", strconv.Itoa(i), "player", p.Addr.String(), "amount", strconv.FormatInt(g.Stake, 10))
87 }
88}
89
90// payPot pays the pot of a finished showdown: the house's cut, then the
91// winner, or the seats tied for the most victory points in equal shares.
92func (g *Game) payPot(cur realm) {
93 if g.Pot == 0 || g.Payouts != nil {
94 return
95 }
96 if g.nobodyWon() {
97 g.refundStakes(cur)
98 return
99 }
100 best := g.VP[0]
101 for _, v := range g.VP {
102 if v > best {
103 best = v
104 }
105 }
106 var winners []int
107 for i, v := range g.VP {
108 if v == best {
109 winners = append(winners, i)
110 }
111 }
112 cut := g.Pot * HouseCutPercent / 100
113 share := (g.Pot - cut) / int64(len(winners))
114 cut = g.Pot - share*int64(len(winners))
115 g.Payouts = make([]int64, g.seats())
116 for _, w := range winners {
117 pay(cur, g.Players[w].Addr, share)
118 g.Payouts[w] = share
119 emit("PotPaid", "id", g.ID, "seat", strconv.Itoa(w), "player", g.Players[w].Addr.String(), "amount", strconv.FormatInt(share, 10))
120 }
121 g.HouseCut = cut
122 house += cut
123}
124
125// nobodyWon tells a showdown that ended with nobody left in the fight
126// because everyone ran out of time: nobody played it to the end, nothing
127// was won.
128func (g *Game) nobodyWon() bool {
129 return g.EndReason == "timeout" && engine.AliveCount(g.State) == 0
130}
131
132// WithdrawHouse sends amount ugnot of the house's funds to an address.
133// Admin only.
134func WithdrawHouse(cur realm, to address, amount int64) {
135 assertNoSend()
136 assertAdmin(cur)
137 if !to.IsValid() {
138 panic("gnoslinger: invalid address")
139 }
140 if amount <= 0 || amount > house {
141 panic("gnoslinger: the house holds " + strconv.FormatInt(house, 10) + Denom)
142 }
143 house -= amount
144 pay(cur, to, amount)
145 emit("HouseWithdrawn", "to", to.String(), "amount", strconv.FormatInt(amount, 10))
146}
147
148// SetAdmin hands the house's funds over to another admin. Admin only.
149func SetAdmin(cur realm, addr address) {
150 assertNoSend()
151 assertAdmin(cur)
152 if !addr.IsValid() {
153 panic("gnoslinger: invalid address")
154 }
155 admin = addr
156 emit("AdminChanged", "admin", addr.String())
157}
158
159func assertAdmin(cur realm) {
160 if prev := cur.Previous(); !prev.IsUser() || prev.Address() != admin {
161 panic("gnoslinger: admin only")
162 }
163}
164
165// House is the ugnot the house holds for events.
166func House() int64 { return house }
167
168// Admin is who may withdraw the house's funds.
169func Admin() address { return admin }