Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}