Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}