poker.gno
11.60 Kb ยท 510 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 (only if realm has funds)
115 if p.Chips > 0 {
116 b := banker.NewBanker(banker.BankerTypeRealmSend)
117 pkgaddr := runtime.CurrentRealm().Address()
118 realmCoins := b.GetCoins(pkgaddr)
119 hasEnough := false
120 for _, c := range realmCoins {
121 if c.Denom == "ugnot" && c.Amount >= p.Chips {
122 hasEnough = true
123 break
124 }
125 }
126 if hasEnough {
127 sendCoins := chain.Coins{chain.NewCoin("ugnot", p.Chips)}
128 b.SendCoins(pkgaddr, caller, sendCoins)
129 }
130 }
131
132 table.Players = append(table.Players[:i], table.Players[i+1:]...)
133
134 for j := i; j < len(table.Players); j++ {
135 table.Players[j].SeatIndex = j
136 }
137
138 if len(table.Players) < 2 && table.State != poker.StateWaiting {
139 if len(table.Players) == 1 {
140 remaining := table.Players[0]
141 remaining.Chips += table.Pot
142 table.Pot = 0
143 }
144 table.State = poker.StateWaiting
145 table.Community = nil
146 }
147
148 return
149 }
150 }
151
152 panic("player not found at this table")
153}
154
155// ----- Game Flow -----
156
157func StartHand(cur realm, tableID string) {
158 table := getTable(tableID)
159
160 if len(table.Players) < 2 {
161 panic("need at least 2 players to start")
162 }
163 if table.State != poker.StateWaiting {
164 panic("hand already in progress")
165 }
166
167 deck := poker.NewDeck()
168 seed := int64(table.HandCount)*31337 + 12345
169 deck.Shuffle(seed)
170
171 for _, p := range table.Players {
172 p.HasFolded = false
173 p.IsAllIn = false
174 p.CurrentBet = 0
175 cards := deck.Deal(2)
176 p.Hand = [2]poker.Card{cards[0], cards[1]}
177 }
178
179 table.DeckSeed = seed
180 table.Community = make([]poker.Card, 0)
181 table.Pot = 0
182 table.State = poker.StatePreFlop
183 table.HandCount++
184
185 numPlayers := len(table.Players)
186 sbIdx := (table.DealerIdx + 1) % numPlayers
187 bbIdx := (table.DealerIdx + 2) % numPlayers
188
189 sb := table.SmallBlind
190 bb := table.BigBlind
191
192 table.Players[sbIdx].Chips -= sb
193 table.Players[sbIdx].CurrentBet = sb
194 table.Players[bbIdx].Chips -= bb
195 table.Players[bbIdx].CurrentBet = bb
196 table.Pot = sb + bb
197
198 table.CurrentTurn = (bbIdx + 1) % numPlayers
199}
200
201func PlaceBet(cur realm, tableID string, amount int64) {
202 caller := runtime.PreviousRealm().Address()
203 table := getTable(tableID)
204 player := getPlayerAt(table, caller.String())
205
206 assertPlayerTurn(table, player)
207
208 if amount <= 0 || amount > player.Chips {
209 panic("invalid bet amount")
210 }
211 if amount > table.MaxBet {
212 panic("exceeds max bet")
213 }
214
215 player.Chips -= amount
216 player.CurrentBet += amount
217 table.Pot += amount
218
219 advanceTurn(table)
220}
221
222func Call(cur realm, tableID string) {
223 caller := runtime.PreviousRealm().Address()
224 table := getTable(tableID)
225 player := getPlayerAt(table, caller.String())
226
227 assertPlayerTurn(table, player)
228
229 maxBet := getMaxCurrentBet(table)
230 toCall := maxBet - player.CurrentBet
231
232 if toCall <= 0 {
233 panic("nothing to call, use Check instead")
234 }
235
236 if toCall > player.Chips {
237 table.Pot += player.Chips
238 player.CurrentBet += player.Chips
239 player.Chips = 0
240 player.IsAllIn = true
241 } else {
242 player.Chips -= toCall
243 player.CurrentBet += toCall
244 table.Pot += toCall
245 }
246
247 advanceTurn(table)
248}
249
250func Check(cur realm, tableID string) {
251 caller := runtime.PreviousRealm().Address()
252 table := getTable(tableID)
253 player := getPlayerAt(table, caller.String())
254
255 assertPlayerTurn(table, player)
256
257 maxBet := getMaxCurrentBet(table)
258 if player.CurrentBet < maxBet {
259 panic("cannot check, there is an outstanding bet")
260 }
261
262 advanceTurn(table)
263}
264
265func Fold(cur realm, tableID string) {
266 caller := runtime.PreviousRealm().Address()
267 table := getTable(tableID)
268 player := getPlayerAt(table, caller.String())
269
270 assertPlayerTurn(table, player)
271
272 player.HasFolded = true
273
274 activePlayers := countActivePlayers(table)
275 if activePlayers == 1 {
276 for _, p := range table.Players {
277 if !p.HasFolded {
278 awardPot(table, p)
279 return
280 }
281 }
282 }
283
284 advanceTurn(table)
285}
286
287func AllIn(cur realm, tableID string) {
288 caller := runtime.PreviousRealm().Address()
289 table := getTable(tableID)
290 player := getPlayerAt(table, caller.String())
291
292 assertPlayerTurn(table, player)
293
294 amount := player.Chips
295 player.CurrentBet += amount
296 player.Chips = 0
297 player.IsAllIn = true
298 table.Pot += amount
299
300 advanceTurn(table)
301}
302
303// ----- Admin Functions -----
304
305func SetRakePercent(cur realm, percent int64) {
306 caller := runtime.PreviousRealm().Address()
307 if caller != admin {
308 panic("only admin can set rake")
309 }
310 if percent < 0 || percent > 500 {
311 panic("rake must be 0-500 (0-5%)")
312 }
313 rakePercent = percent
314}
315
316func WithdrawRake(cur realm) int64 {
317 caller := runtime.PreviousRealm().Address()
318 if caller != admin {
319 panic("only admin can withdraw rake")
320 }
321 amount := rakePool
322 rakePool = 0
323 return amount
324}
325
326// ----- Query Functions -----
327
328func GetTables() string {
329 var sb strings.Builder
330 sb.WriteString("[")
331 for i, id := range tableList {
332 t := tables[id]
333 if i > 0 {
334 sb.WriteString(",")
335 }
336 sb.WriteString("{")
337 sb.WriteString(`"id":"` + t.ID + `"`)
338 sb.WriteString(`,"name":"` + t.Name + `"`)
339 sb.WriteString(`,"maxPlayers":` + poker.IntToStr(t.MaxPlayers))
340 sb.WriteString(`,"currentPlayers":` + poker.IntToStr(len(t.Players)))
341 sb.WriteString(`,"smallBlind":` + int64ToStr(t.SmallBlind))
342 sb.WriteString(`,"bigBlind":` + int64ToStr(t.BigBlind))
343 sb.WriteString(`,"minBet":` + int64ToStr(t.MinBet))
344 sb.WriteString(`,"maxBet":` + int64ToStr(t.MaxBet))
345 sb.WriteString(`,"minBuyIn":` + int64ToStr(t.MinBuyIn))
346 sb.WriteString(`,"maxBuyIn":` + int64ToStr(t.MaxBuyIn))
347 sb.WriteString(`,"pot":` + int64ToStr(t.Pot))
348 sb.WriteString(`,"state":"` + t.State.String() + `"`)
349 sb.WriteString("}")
350 }
351 sb.WriteString("]")
352 return sb.String()
353}
354
355func GetTableState(tableID string) string {
356 table := getTable(tableID)
357 var sb strings.Builder
358 sb.WriteString("{")
359 sb.WriteString(`"id":"` + table.ID + `"`)
360 sb.WriteString(`,"state":"` + table.State.String() + `"`)
361 sb.WriteString(`,"pot":` + int64ToStr(table.Pot))
362 sb.WriteString(`,"currentTurn":` + poker.IntToStr(table.CurrentTurn))
363 sb.WriteString(`,"dealerIdx":` + poker.IntToStr(table.DealerIdx))
364 sb.WriteString(`,"players":` + poker.IntToStr(len(table.Players)))
365 sb.WriteString("}")
366 return sb.String()
367}
368
369// ----- Internal Helpers -----
370
371func getTable(id string) *poker.Table {
372 table, ok := tables[id]
373 if !ok {
374 panic("table not found: " + id)
375 }
376 return table
377}
378
379func getPlayerAt(table *poker.Table, addr string) *poker.Player {
380 for _, p := range table.Players {
381 if p.Address == addr {
382 return p
383 }
384 }
385 panic("player not at this table")
386}
387
388func assertPlayerTurn(table *poker.Table, player *poker.Player) {
389 if table.State == poker.StateWaiting || table.State == poker.StateShowdown {
390 panic("not a betting round")
391 }
392 if player.HasFolded {
393 panic("you have folded")
394 }
395 if player.SeatIndex != table.CurrentTurn {
396 panic("not your turn")
397 }
398}
399
400func getMaxCurrentBet(table *poker.Table) int64 {
401 maxBet := int64(0)
402 for _, p := range table.Players {
403 if p.CurrentBet > maxBet {
404 maxBet = p.CurrentBet
405 }
406 }
407 return maxBet
408}
409
410func countActivePlayers(table *poker.Table) int {
411 count := 0
412 for _, p := range table.Players {
413 if !p.HasFolded {
414 count++
415 }
416 }
417 return count
418}
419
420func advanceTurn(table *poker.Table) {
421 numPlayers := len(table.Players)
422 next := table.CurrentTurn
423 for i := 0; i < numPlayers; i++ {
424 next = (next + 1) % numPlayers
425 p := table.Players[next]
426 if !p.HasFolded && !p.IsAllIn {
427 table.CurrentTurn = next
428 return
429 }
430 }
431 advanceRound(table)
432}
433
434func advanceRound(table *poker.Table) {
435 for _, p := range table.Players {
436 p.CurrentBet = 0
437 }
438
439 deck := poker.NewDeck()
440 deck.Shuffle(table.DeckSeed + int64(table.State)*7919)
441 deck.Deal(len(table.Players) * 2)
442
443 switch table.State {
444 case poker.StatePreFlop:
445 cards := deck.Deal(3 + len(table.Community))
446 table.Community = cards[len(cards)-3:]
447 table.State = poker.StateFlop
448 case poker.StateFlop:
449 deck.Deal(len(table.Community))
450 card := deck.DealOne()
451 table.Community = append(table.Community, card)
452 table.State = poker.StateTurn
453 case poker.StateTurn:
454 deck.Deal(len(table.Community))
455 card := deck.DealOne()
456 table.Community = append(table.Community, card)
457 table.State = poker.StateRiver
458 case poker.StateRiver:
459 table.State = poker.StateShowdown
460 resolveShowdown(table)
461 return
462 }
463
464 table.CurrentTurn = (table.DealerIdx + 1) % len(table.Players)
465 for i := 0; i < len(table.Players); i++ {
466 idx := (table.CurrentTurn + i) % len(table.Players)
467 if !table.Players[idx].HasFolded && !table.Players[idx].IsAllIn {
468 table.CurrentTurn = idx
469 return
470 }
471 }
472}
473
474func resolveShowdown(table *poker.Table) {
475 var bestResult poker.HandResult
476 var winner *poker.Player
477
478 for _, p := range table.Players {
479 if p.HasFolded {
480 continue
481 }
482 result := poker.EvaluateHand(p.Hand, table.Community)
483 if winner == nil || poker.CompareHandResults(result, bestResult) > 0 {
484 bestResult = result
485 winner = p
486 }
487 }
488
489 if winner != nil {
490 awardPot(table, winner)
491 }
492}
493
494func awardPot(table *poker.Table, winner *poker.Player) {
495 rake := (table.Pot * rakePercent) / 10000
496 rakePool += rake
497
498 winnings := table.Pot - rake
499 winner.Chips += winnings
500 table.Pot = 0
501 table.RakeCollected += rake
502
503 table.State = poker.StateWaiting
504 table.DealerIdx = (table.DealerIdx + 1) % len(table.Players)
505 table.Community = nil
506}
507
508func int64ToStr(n int64) string {
509 return poker.IntToStr(int(n))
510}