poker.gno
11.36 Kb ยท 500 lines
1package poker
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime"
7 "strings"
8
9 "gno.land/p/demo/poker"
10)
11
12// ----- State -----
13var (
14 tables map[string]*poker.Table
15 tableList []string
16 admin address
17 rakePercent int64
18 rakePool int64
19 tableCount int
20)
21
22func init() {
23 tables = make(map[string]*poker.Table)
24 admin = runtime.PreviousRealm().Address()
25 rakePercent = 300 // 3% default
26}
27
28// ----- Lobby Functions -----
29
30func CreateTable(cur realm, name string, smallBlind int64, bigBlind int64, minBet int64, maxBet int64, maxPlayers int, minBuyIn int64) string {
31 caller := runtime.PreviousRealm().Address()
32
33 if maxPlayers != 4 && maxPlayers != 6 {
34 panic("maxPlayers must be 4 or 6")
35 }
36 if smallBlind <= 0 || bigBlind <= 0 || smallBlind > bigBlind {
37 panic("invalid blind structure")
38 }
39 if minBet <= 0 || maxBet <= 0 || minBet > maxBet {
40 panic("invalid bet limits")
41 }
42 if minBuyIn < bigBlind*10 {
43 panic("minBuyIn must be at least 10x big blind")
44 }
45
46 tableCount++
47 id := "table-" + poker.IntToStr(tableCount)
48
49 table := &poker.Table{
50 ID: id,
51 Name: name,
52 Creator: caller.String(),
53 Players: make([]*poker.Player, 0),
54 MaxPlayers: maxPlayers,
55 SmallBlind: smallBlind,
56 BigBlind: bigBlind,
57 MinBet: minBet,
58 MaxBet: maxBet,
59 MinBuyIn: minBuyIn,
60 MaxBuyIn: minBuyIn * 10,
61 State: poker.StateWaiting,
62 }
63
64 tables[id] = table
65 tableList = append(tableList, id)
66
67 return id
68}
69
70// JoinTable joins an existing table. buyInAmount is in ugnot.
71// The caller must send at least buyInAmount ugnot via --send flag.
72func JoinTable(cur realm, tableID string, buyInAmount int64) {
73 caller := runtime.PreviousRealm().Address()
74 table := getTable(tableID)
75
76 if len(table.Players) >= table.MaxPlayers {
77 panic("table is full")
78 }
79
80 for _, p := range table.Players {
81 if p.Address == caller.String() {
82 panic("already at this table")
83 }
84 }
85
86 if buyInAmount < table.MinBuyIn {
87 panic("insufficient buy-in amount")
88 }
89 if buyInAmount > table.MaxBuyIn {
90 panic("exceeds max buy-in")
91 }
92
93 player := &poker.Player{
94 Address: caller.String(),
95 Chips: buyInAmount,
96 IsActive: true,
97 SeatIndex: len(table.Players),
98 }
99
100 table.Players = append(table.Players, player)
101}
102
103// LeaveTable sends remaining chips back to player wallet
104func LeaveTable(cur realm, tableID string) {
105 caller := runtime.PreviousRealm().Address()
106 table := getTable(tableID)
107
108 for i, p := range table.Players {
109 if p.Address == caller.String() {
110 if table.State != poker.StateWaiting && !p.HasFolded {
111 p.HasFolded = true
112 }
113
114 // Send remaining chips back to player
115 if p.Chips > 0 {
116 b := banker.NewBanker(banker.BankerTypeRealmSend)
117 sendCoins := chain.Coins{chain.NewCoin("ugnot", p.Chips)}
118 pkgaddr := runtime.CurrentRealm().Address()
119 b.SendCoins(pkgaddr, caller, sendCoins)
120 }
121
122 table.Players = append(table.Players[:i], table.Players[i+1:]...)
123
124 for j := i; j < len(table.Players); j++ {
125 table.Players[j].SeatIndex = j
126 }
127
128 if len(table.Players) < 2 && table.State != poker.StateWaiting {
129 if len(table.Players) == 1 {
130 remaining := table.Players[0]
131 remaining.Chips += table.Pot
132 table.Pot = 0
133 }
134 table.State = poker.StateWaiting
135 table.Community = nil
136 }
137
138 return
139 }
140 }
141
142 panic("player not found at this table")
143}
144
145// ----- Game Flow -----
146
147func StartHand(cur realm, tableID string) {
148 table := getTable(tableID)
149
150 if len(table.Players) < 2 {
151 panic("need at least 2 players to start")
152 }
153 if table.State != poker.StateWaiting {
154 panic("hand already in progress")
155 }
156
157 deck := poker.NewDeck()
158 seed := int64(table.HandCount)*31337 + 12345
159 deck.Shuffle(seed)
160
161 for _, p := range table.Players {
162 p.HasFolded = false
163 p.IsAllIn = false
164 p.CurrentBet = 0
165 cards := deck.Deal(2)
166 p.Hand = [2]poker.Card{cards[0], cards[1]}
167 }
168
169 table.DeckSeed = seed
170 table.Community = make([]poker.Card, 0)
171 table.Pot = 0
172 table.State = poker.StatePreFlop
173 table.HandCount++
174
175 numPlayers := len(table.Players)
176 sbIdx := (table.DealerIdx + 1) % numPlayers
177 bbIdx := (table.DealerIdx + 2) % numPlayers
178
179 sb := table.SmallBlind
180 bb := table.BigBlind
181
182 table.Players[sbIdx].Chips -= sb
183 table.Players[sbIdx].CurrentBet = sb
184 table.Players[bbIdx].Chips -= bb
185 table.Players[bbIdx].CurrentBet = bb
186 table.Pot = sb + bb
187
188 table.CurrentTurn = (bbIdx + 1) % numPlayers
189}
190
191func PlaceBet(cur realm, tableID string, amount int64) {
192 caller := runtime.PreviousRealm().Address()
193 table := getTable(tableID)
194 player := getPlayerAt(table, caller.String())
195
196 assertPlayerTurn(table, player)
197
198 if amount <= 0 || amount > player.Chips {
199 panic("invalid bet amount")
200 }
201 if amount > table.MaxBet {
202 panic("exceeds max bet")
203 }
204
205 player.Chips -= amount
206 player.CurrentBet += amount
207 table.Pot += amount
208
209 advanceTurn(table)
210}
211
212func Call(cur realm, tableID string) {
213 caller := runtime.PreviousRealm().Address()
214 table := getTable(tableID)
215 player := getPlayerAt(table, caller.String())
216
217 assertPlayerTurn(table, player)
218
219 maxBet := getMaxCurrentBet(table)
220 toCall := maxBet - player.CurrentBet
221
222 if toCall <= 0 {
223 panic("nothing to call, use Check instead")
224 }
225
226 if toCall > player.Chips {
227 table.Pot += player.Chips
228 player.CurrentBet += player.Chips
229 player.Chips = 0
230 player.IsAllIn = true
231 } else {
232 player.Chips -= toCall
233 player.CurrentBet += toCall
234 table.Pot += toCall
235 }
236
237 advanceTurn(table)
238}
239
240func Check(cur realm, tableID string) {
241 caller := runtime.PreviousRealm().Address()
242 table := getTable(tableID)
243 player := getPlayerAt(table, caller.String())
244
245 assertPlayerTurn(table, player)
246
247 maxBet := getMaxCurrentBet(table)
248 if player.CurrentBet < maxBet {
249 panic("cannot check, there is an outstanding bet")
250 }
251
252 advanceTurn(table)
253}
254
255func Fold(cur realm, tableID string) {
256 caller := runtime.PreviousRealm().Address()
257 table := getTable(tableID)
258 player := getPlayerAt(table, caller.String())
259
260 assertPlayerTurn(table, player)
261
262 player.HasFolded = true
263
264 activePlayers := countActivePlayers(table)
265 if activePlayers == 1 {
266 for _, p := range table.Players {
267 if !p.HasFolded {
268 awardPot(table, p)
269 return
270 }
271 }
272 }
273
274 advanceTurn(table)
275}
276
277func AllIn(cur realm, tableID string) {
278 caller := runtime.PreviousRealm().Address()
279 table := getTable(tableID)
280 player := getPlayerAt(table, caller.String())
281
282 assertPlayerTurn(table, player)
283
284 amount := player.Chips
285 player.CurrentBet += amount
286 player.Chips = 0
287 player.IsAllIn = true
288 table.Pot += amount
289
290 advanceTurn(table)
291}
292
293// ----- Admin Functions -----
294
295func SetRakePercent(cur realm, percent int64) {
296 caller := runtime.PreviousRealm().Address()
297 if caller != admin {
298 panic("only admin can set rake")
299 }
300 if percent < 0 || percent > 500 {
301 panic("rake must be 0-500 (0-5%)")
302 }
303 rakePercent = percent
304}
305
306func WithdrawRake(cur realm) int64 {
307 caller := runtime.PreviousRealm().Address()
308 if caller != admin {
309 panic("only admin can withdraw rake")
310 }
311 amount := rakePool
312 rakePool = 0
313 return amount
314}
315
316// ----- Query Functions -----
317
318func GetTables() string {
319 var sb strings.Builder
320 sb.WriteString("[")
321 for i, id := range tableList {
322 t := tables[id]
323 if i > 0 {
324 sb.WriteString(",")
325 }
326 sb.WriteString("{")
327 sb.WriteString(`"id":"` + t.ID + `"`)
328 sb.WriteString(`,"name":"` + t.Name + `"`)
329 sb.WriteString(`,"maxPlayers":` + poker.IntToStr(t.MaxPlayers))
330 sb.WriteString(`,"currentPlayers":` + poker.IntToStr(len(t.Players)))
331 sb.WriteString(`,"smallBlind":` + int64ToStr(t.SmallBlind))
332 sb.WriteString(`,"bigBlind":` + int64ToStr(t.BigBlind))
333 sb.WriteString(`,"minBet":` + int64ToStr(t.MinBet))
334 sb.WriteString(`,"maxBet":` + int64ToStr(t.MaxBet))
335 sb.WriteString(`,"minBuyIn":` + int64ToStr(t.MinBuyIn))
336 sb.WriteString(`,"maxBuyIn":` + int64ToStr(t.MaxBuyIn))
337 sb.WriteString(`,"pot":` + int64ToStr(t.Pot))
338 sb.WriteString(`,"state":"` + t.State.String() + `"`)
339 sb.WriteString("}")
340 }
341 sb.WriteString("]")
342 return sb.String()
343}
344
345func GetTableState(tableID string) string {
346 table := getTable(tableID)
347 var sb strings.Builder
348 sb.WriteString("{")
349 sb.WriteString(`"id":"` + table.ID + `"`)
350 sb.WriteString(`,"state":"` + table.State.String() + `"`)
351 sb.WriteString(`,"pot":` + int64ToStr(table.Pot))
352 sb.WriteString(`,"currentTurn":` + poker.IntToStr(table.CurrentTurn))
353 sb.WriteString(`,"dealerIdx":` + poker.IntToStr(table.DealerIdx))
354 sb.WriteString(`,"players":` + poker.IntToStr(len(table.Players)))
355 sb.WriteString("}")
356 return sb.String()
357}
358
359// ----- Internal Helpers -----
360
361func getTable(id string) *poker.Table {
362 table, ok := tables[id]
363 if !ok {
364 panic("table not found: " + id)
365 }
366 return table
367}
368
369func getPlayerAt(table *poker.Table, addr string) *poker.Player {
370 for _, p := range table.Players {
371 if p.Address == addr {
372 return p
373 }
374 }
375 panic("player not at this table")
376}
377
378func assertPlayerTurn(table *poker.Table, player *poker.Player) {
379 if table.State == poker.StateWaiting || table.State == poker.StateShowdown {
380 panic("not a betting round")
381 }
382 if player.HasFolded {
383 panic("you have folded")
384 }
385 if player.SeatIndex != table.CurrentTurn {
386 panic("not your turn")
387 }
388}
389
390func getMaxCurrentBet(table *poker.Table) int64 {
391 maxBet := int64(0)
392 for _, p := range table.Players {
393 if p.CurrentBet > maxBet {
394 maxBet = p.CurrentBet
395 }
396 }
397 return maxBet
398}
399
400func countActivePlayers(table *poker.Table) int {
401 count := 0
402 for _, p := range table.Players {
403 if !p.HasFolded {
404 count++
405 }
406 }
407 return count
408}
409
410func advanceTurn(table *poker.Table) {
411 numPlayers := len(table.Players)
412 next := table.CurrentTurn
413 for i := 0; i < numPlayers; i++ {
414 next = (next + 1) % numPlayers
415 p := table.Players[next]
416 if !p.HasFolded && !p.IsAllIn {
417 table.CurrentTurn = next
418 return
419 }
420 }
421 advanceRound(table)
422}
423
424func advanceRound(table *poker.Table) {
425 for _, p := range table.Players {
426 p.CurrentBet = 0
427 }
428
429 deck := poker.NewDeck()
430 deck.Shuffle(table.DeckSeed + int64(table.State)*7919)
431 deck.Deal(len(table.Players) * 2)
432
433 switch table.State {
434 case poker.StatePreFlop:
435 cards := deck.Deal(3 + len(table.Community))
436 table.Community = cards[len(cards)-3:]
437 table.State = poker.StateFlop
438 case poker.StateFlop:
439 deck.Deal(len(table.Community))
440 card := deck.DealOne()
441 table.Community = append(table.Community, card)
442 table.State = poker.StateTurn
443 case poker.StateTurn:
444 deck.Deal(len(table.Community))
445 card := deck.DealOne()
446 table.Community = append(table.Community, card)
447 table.State = poker.StateRiver
448 case poker.StateRiver:
449 table.State = poker.StateShowdown
450 resolveShowdown(table)
451 return
452 }
453
454 table.CurrentTurn = (table.DealerIdx + 1) % len(table.Players)
455 for i := 0; i < len(table.Players); i++ {
456 idx := (table.CurrentTurn + i) % len(table.Players)
457 if !table.Players[idx].HasFolded && !table.Players[idx].IsAllIn {
458 table.CurrentTurn = idx
459 return
460 }
461 }
462}
463
464func resolveShowdown(table *poker.Table) {
465 var bestResult poker.HandResult
466 var winner *poker.Player
467
468 for _, p := range table.Players {
469 if p.HasFolded {
470 continue
471 }
472 result := poker.EvaluateHand(p.Hand, table.Community)
473 if winner == nil || poker.CompareHandResults(result, bestResult) > 0 {
474 bestResult = result
475 winner = p
476 }
477 }
478
479 if winner != nil {
480 awardPot(table, winner)
481 }
482}
483
484func awardPot(table *poker.Table, winner *poker.Player) {
485 rake := (table.Pot * rakePercent) / 10000
486 rakePool += rake
487
488 winnings := table.Pot - rake
489 winner.Chips += winnings
490 table.Pot = 0
491 table.RakeCollected += rake
492
493 table.State = poker.StateWaiting
494 table.DealerIdx = (table.DealerIdx + 1) % len(table.Players)
495 table.Community = nil
496}
497
498func int64ToStr(n int64) string {
499 return poker.IntToStr(int(n))
500}