package plugin import ( "math/rand/v2" "time" "github.com/chehsunliu/poker" "maunium.net/go/mautrix/id" ) // Street represents the current phase of a Hold'em hand. type Street int const ( StreetPreFlop Street = iota StreetFlop StreetTurn StreetRiver StreetShowdown ) func (s Street) String() string { switch s { case StreetPreFlop: return "Pre-Flop" case StreetFlop: return "Flop" case StreetTurn: return "Turn" case StreetRiver: return "River" case StreetShowdown: return "Showdown" default: return "Unknown" } } // PlayerState tracks a player's status within a hand. type PlayerState int const ( PlayerActive PlayerState = iota PlayerFolded PlayerAllIn PlayerSatOut ) // SidePot represents a pot with specific eligible players. type SidePot struct { Amount int64 Eligible []id.UserID } // HoldemPlayer represents a seated player. type HoldemPlayer struct { UserID id.UserID DisplayName string Stack int64 OpeningStack int64 Hole [2]poker.Card Bet int64 // committed this street TotalBet int64 // committed this hand State PlayerState HasActed bool // whether the player has voluntarily acted this street TipsEnabled bool SittingOut bool WantsLeave bool IsNPC bool DMRoomID id.RoomID } // HoldemGame holds all state for one table. type HoldemGame struct { RoomID id.RoomID Players []*HoldemPlayer Community []poker.Card Deck []poker.Card DeckPos int // position in the deck Pot int64 SidePots []SidePot Street Street DealerIdx int ActionIdx int CurrentBet int64 MinRaise int64 SmallBlind int64 BigBlind int64 LastAggressorIdx int WaitingForPlayers bool HandInProgress bool StreetHistory string // action chars for current street (f/c/r/R/a) for CFR policy lookup actionTimer *time.Timer warningTimer *time.Timer idleTimer *time.Timer idleWarningTimer *time.Timer } // newShuffledDeck creates a shuffled 52-card deck. func newShuffledDeck() []poker.Card { cards := allCards() rand.Shuffle(len(cards), func(i, j int) { cards[i], cards[j] = cards[j], cards[i] }) return cards } // drawCard draws the next card from the deck. func (g *HoldemGame) drawCard() poker.Card { c := g.Deck[g.DeckPos] g.DeckPos++ return c } // burnCard discards the top card (standard casino burn). func (g *HoldemGame) burnCard() { g.DeckPos++ } // activePlayers returns players who are Active or AllIn. func (g *HoldemGame) activePlayers() []*HoldemPlayer { var result []*HoldemPlayer for _, p := range g.Players { if p.State == PlayerActive || p.State == PlayerAllIn { result = append(result, p) } } return result } // activeCount returns the number of Active or AllIn players. func (g *HoldemGame) activeCount() int { n := 0 for _, p := range g.Players { if p.State == PlayerActive || p.State == PlayerAllIn { n++ } } return n } // canActCount returns the number of players who can still act (Active with stack > 0). func (g *HoldemGame) canActCount() int { n := 0 for _, p := range g.Players { if p.State == PlayerActive { n++ } } return n } // nextActiveIdx returns the next seat index after idx with Active or AllIn state. func (g *HoldemGame) nextActiveIdx(idx int) int { n := len(g.Players) for i := 1; i < n; i++ { next := (idx + i) % n p := g.Players[next] if p.State == PlayerActive || p.State == PlayerAllIn { return next } } return idx } // nextCanActIdx returns the next seat with Active state (can take actions). func (g *HoldemGame) nextCanActIdx(idx int) int { n := len(g.Players) for i := 1; i < n; i++ { next := (idx + i) % n if g.Players[next].State == PlayerActive { return next } } return idx } // inHandPlayers returns players participating in the current hand (not SatOut). func (g *HoldemGame) inHandPlayers() []*HoldemPlayer { var result []*HoldemPlayer for _, p := range g.Players { if p.State != PlayerSatOut { result = append(result, p) } } return result } // playerByUserID finds a player by their Matrix user ID. func (g *HoldemGame) playerByUserID(uid id.UserID) *HoldemPlayer { for _, p := range g.Players { if p.UserID == uid { return p } } return nil } // playerIdx returns the seat index for a user ID, or -1. func (g *HoldemGame) playerIdx(uid id.UserID) int { for i, p := range g.Players { if p.UserID == uid { return i } } return -1 } // resetStreetBets clears per-street bet tracking. func (g *HoldemGame) resetStreetBets() { for _, p := range g.Players { p.Bet = 0 p.HasActed = false } g.CurrentBet = 0 g.MinRaise = g.BigBlind g.StreetHistory = "" } // collectPot moves all bets into the pot. func (g *HoldemGame) collectPot() { for _, p := range g.Players { g.Pot += p.Bet p.Bet = 0 } } // positionLabel returns BTN/SB/BB/UTG/MP/CO for a seat index. func (g *HoldemGame) positionLabel(seatIdx int) string { n := len(g.inHandPlayers()) if n <= 1 { return "" } if seatIdx == g.DealerIdx { return "BTN" } headsUp := n == 2 if headsUp { // In heads-up, dealer is SB; other is BB. return "BB" } sbIdx := g.nextActiveIdx(g.DealerIdx) bbIdx := g.nextActiveIdx(sbIdx) if seatIdx == sbIdx { return "SB" } if seatIdx == bbIdx { return "BB" } utgIdx := g.nextActiveIdx(bbIdx) if seatIdx == utgIdx { return "UTG" } // Rough labeling for remaining seats. // Count distance from UTG. dist := 0 cur := utgIdx for i := 0; i < n; i++ { cur = g.nextActiveIdx(cur) dist++ if cur == seatIdx { break } } remaining := n - 4 // seats after UTG before BTN if remaining <= 0 { return "MP" } if dist >= remaining { return "CO" } return "MP" }