mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is semantics-preserving, and build/vet/test are green either side. Split out from the code-review fixes that follow so those stay reviewable instead of hiding inside a wall of realignment.
384 lines
9.1 KiB
Go
384 lines
9.1 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// postBlinds posts SB and BB, handling heads-up edge case.
|
|
func (g *HoldemGame) postBlinds() (sbIdx, bbIdx int) {
|
|
inHand := g.inHandPlayers()
|
|
n := len(inHand)
|
|
|
|
if n == 2 {
|
|
// Heads-up: dealer posts SB, other posts BB.
|
|
sbIdx = g.DealerIdx
|
|
bbIdx = g.nextActiveIdx(g.DealerIdx)
|
|
} else {
|
|
sbIdx = g.nextActiveIdx(g.DealerIdx)
|
|
bbIdx = g.nextActiveIdx(sbIdx)
|
|
}
|
|
|
|
// Post small blind.
|
|
sb := g.Players[sbIdx]
|
|
sbAmount := g.SmallBlind
|
|
if sbAmount > sb.Stack {
|
|
sbAmount = sb.Stack
|
|
}
|
|
sb.Stack -= sbAmount
|
|
sb.Bet = sbAmount
|
|
sb.TotalBet = sbAmount
|
|
if sb.Stack == 0 {
|
|
sb.State = PlayerAllIn
|
|
}
|
|
|
|
// Post big blind.
|
|
bb := g.Players[bbIdx]
|
|
bbAmount := g.BigBlind
|
|
if bbAmount > bb.Stack {
|
|
bbAmount = bb.Stack
|
|
}
|
|
bb.Stack -= bbAmount
|
|
bb.Bet = bbAmount
|
|
bb.TotalBet = bbAmount
|
|
if bb.Stack == 0 {
|
|
bb.State = PlayerAllIn
|
|
}
|
|
|
|
g.CurrentBet = g.BigBlind
|
|
g.MinRaise = g.BigBlind
|
|
g.LastAggressorIdx = bbIdx // BB has option
|
|
|
|
return sbIdx, bbIdx
|
|
}
|
|
|
|
// firstToActPreflop returns the seat index of the first player to act preflop.
|
|
func (g *HoldemGame) firstToActPreflop(bbIdx int) int {
|
|
inHand := len(g.inHandPlayers())
|
|
if inHand == 2 {
|
|
// Heads-up: dealer/SB acts first preflop.
|
|
return g.DealerIdx
|
|
}
|
|
// UTG = next active after BB.
|
|
return g.nextCanActIdx(bbIdx)
|
|
}
|
|
|
|
// firstToActPostflop returns the first player to act on post-flop streets.
|
|
func (g *HoldemGame) firstToActPostflop() int {
|
|
// First active player after dealer.
|
|
return g.nextCanActIdx(g.DealerIdx)
|
|
}
|
|
|
|
// ActionResult describes what happened after an action.
|
|
type ActionResult struct {
|
|
Announcement string
|
|
HandOver bool // only 1 player remains
|
|
StreetOver bool // street betting is complete
|
|
AllAllIn bool // all remaining players are all-in
|
|
}
|
|
|
|
// doFold processes a fold action.
|
|
func (g *HoldemGame) doFold(seatIdx int) ActionResult {
|
|
p := g.Players[seatIdx]
|
|
p.State = PlayerFolded
|
|
p.HasActed = true
|
|
g.StreetHistory += "f"
|
|
|
|
ann := renderActionAnnouncement(p.DisplayName, "fold", 0)
|
|
|
|
if g.activeCount() == 1 {
|
|
return ActionResult{Announcement: ann, HandOver: true}
|
|
}
|
|
|
|
return ActionResult{
|
|
Announcement: ann,
|
|
StreetOver: g.isStreetComplete(g.nextCanActIdx(seatIdx)),
|
|
AllAllIn: g.canActCount() == 0,
|
|
}
|
|
}
|
|
|
|
// doCheck processes a check action. Returns error string if invalid.
|
|
func (g *HoldemGame) doCheck(seatIdx int) (ActionResult, string) {
|
|
p := g.Players[seatIdx]
|
|
if p.Bet < g.CurrentBet {
|
|
return ActionResult{}, "You must call, raise, or fold — there's a bet to you."
|
|
}
|
|
|
|
p.HasActed = true
|
|
g.StreetHistory += "c"
|
|
ann := renderActionAnnouncement(p.DisplayName, "check", 0)
|
|
return ActionResult{
|
|
Announcement: ann,
|
|
StreetOver: g.isStreetComplete(g.nextCanActIdx(seatIdx)),
|
|
}, ""
|
|
}
|
|
|
|
// doCall processes a call action. Returns error string if nothing to call.
|
|
func (g *HoldemGame) doCall(seatIdx int) (ActionResult, string) {
|
|
p := g.Players[seatIdx]
|
|
toCall := g.CurrentBet - p.Bet
|
|
if toCall <= 0 {
|
|
return ActionResult{}, "Nothing to call. Use `!holdem check` instead."
|
|
}
|
|
if toCall > p.Stack {
|
|
toCall = p.Stack
|
|
}
|
|
|
|
p.Stack -= toCall
|
|
p.Bet += toCall
|
|
p.TotalBet += toCall
|
|
p.HasActed = true
|
|
|
|
action := "call"
|
|
if p.Stack == 0 {
|
|
p.State = PlayerAllIn
|
|
action = "allin"
|
|
g.StreetHistory += "a"
|
|
} else {
|
|
g.StreetHistory += "c"
|
|
}
|
|
|
|
ann := renderActionAnnouncement(p.DisplayName, action, toCall)
|
|
|
|
if g.activeCount() == 1 {
|
|
return ActionResult{Announcement: ann, HandOver: true}, ""
|
|
}
|
|
|
|
return ActionResult{
|
|
Announcement: ann,
|
|
StreetOver: g.isStreetComplete(g.nextCanActIdx(seatIdx)),
|
|
AllAllIn: g.canActCount() == 0,
|
|
}, ""
|
|
}
|
|
|
|
// doRaise processes a raise action. raiseTo is the total bet amount.
|
|
func (g *HoldemGame) doRaise(seatIdx int, raiseTo int64) (ActionResult, string) {
|
|
p := g.Players[seatIdx]
|
|
|
|
minRaiseTo := g.CurrentBet + g.MinRaise
|
|
maxRaiseTo := p.Bet + p.Stack
|
|
|
|
if raiseTo < minRaiseTo && raiseTo < maxRaiseTo {
|
|
return ActionResult{}, fmt.Sprintf("Minimum raise is to €%d.", minRaiseTo)
|
|
}
|
|
|
|
if raiseTo > maxRaiseTo {
|
|
return ActionResult{}, fmt.Sprintf("You can raise to at most €%d (your stack).", maxRaiseTo)
|
|
}
|
|
|
|
raiseAmount := raiseTo - p.Bet
|
|
actualRaise := raiseTo - g.CurrentBet
|
|
|
|
p.Stack -= raiseAmount
|
|
p.Bet = raiseTo
|
|
p.TotalBet += raiseAmount
|
|
p.HasActed = true
|
|
|
|
if actualRaise > 0 {
|
|
g.MinRaise = actualRaise
|
|
}
|
|
g.CurrentBet = raiseTo
|
|
g.LastAggressorIdx = seatIdx
|
|
|
|
action := "raise"
|
|
if p.Stack == 0 {
|
|
p.State = PlayerAllIn
|
|
action = "allin"
|
|
g.StreetHistory += "a"
|
|
} else {
|
|
// Approximate: >=75% of pot is a pot-size raise ('R'), otherwise half-pot ('r').
|
|
totalPot := g.Pot
|
|
for _, pp := range g.Players {
|
|
totalPot += pp.Bet
|
|
}
|
|
if totalPot > 0 && float64(actualRaise) >= float64(totalPot)*0.75 {
|
|
g.StreetHistory += "R"
|
|
} else {
|
|
g.StreetHistory += "r"
|
|
}
|
|
}
|
|
|
|
ann := renderActionAnnouncement(p.DisplayName, action, raiseTo)
|
|
|
|
return ActionResult{
|
|
Announcement: ann,
|
|
AllAllIn: g.canActCount() == 0,
|
|
}, ""
|
|
}
|
|
|
|
// doAllIn processes an all-in action.
|
|
func (g *HoldemGame) doAllIn(seatIdx int) ActionResult {
|
|
p := g.Players[seatIdx]
|
|
allInAmount := p.Stack
|
|
totalBet := p.Bet + allInAmount
|
|
|
|
p.Stack = 0
|
|
p.Bet = totalBet
|
|
p.TotalBet += allInAmount
|
|
p.State = PlayerAllIn
|
|
p.HasActed = true
|
|
|
|
if totalBet > g.CurrentBet {
|
|
actualRaise := totalBet - g.CurrentBet
|
|
// Only reopen action if the raise meets the minimum.
|
|
// A short all-in (under-raise) does NOT reopen betting.
|
|
if actualRaise >= g.MinRaise {
|
|
g.MinRaise = actualRaise
|
|
g.LastAggressorIdx = seatIdx
|
|
}
|
|
g.CurrentBet = totalBet
|
|
}
|
|
|
|
g.StreetHistory += "a"
|
|
ann := renderActionAnnouncement(p.DisplayName, "allin", totalBet)
|
|
|
|
if g.activeCount() == 1 {
|
|
return ActionResult{Announcement: ann, HandOver: true}
|
|
}
|
|
|
|
return ActionResult{
|
|
Announcement: ann,
|
|
StreetOver: g.isStreetComplete(g.nextCanActIdx(seatIdx)),
|
|
AllAllIn: g.canActCount() == 0,
|
|
}
|
|
}
|
|
|
|
// isStreetComplete checks if the betting round is done.
|
|
func (g *HoldemGame) isStreetComplete(nextIdx int) bool {
|
|
// All active players have matched the current bet and action has returned to the last aggressor.
|
|
if g.canActCount() == 0 {
|
|
return true
|
|
}
|
|
|
|
// Check if all Active players have matched the bet AND have acted at least once.
|
|
// The acted check is critical for preflop: BB posts a blind but hasn't voluntarily
|
|
// acted yet, so the street can't end before BB gets their option.
|
|
for _, p := range g.Players {
|
|
if p.State == PlayerActive && p.Bet != g.CurrentBet {
|
|
return false
|
|
}
|
|
if p.State == PlayerActive && !p.HasActed {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If the last aggressor is all-in (can't act), the street is done when
|
|
// all active players have matched the bet (already checked above).
|
|
if g.Players[g.LastAggressorIdx].State == PlayerAllIn {
|
|
return true
|
|
}
|
|
|
|
// Action must have gone around to the last aggressor.
|
|
return nextIdx == g.LastAggressorIdx
|
|
}
|
|
|
|
// buildSidePots creates side pots when all-ins are present.
|
|
func (g *HoldemGame) buildSidePots() {
|
|
// Collect all bets.
|
|
g.collectPot()
|
|
|
|
type betEntry struct {
|
|
uid id.UserID
|
|
bet int64
|
|
}
|
|
|
|
var entries []betEntry
|
|
for _, p := range g.Players {
|
|
if p.State != PlayerFolded && p.State != PlayerSatOut {
|
|
entries = append(entries, betEntry{p.UserID, p.TotalBet})
|
|
}
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
return
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].bet < entries[j].bet
|
|
})
|
|
|
|
var sidePots []SidePot
|
|
prevLevel := int64(0)
|
|
|
|
for _, e := range entries {
|
|
if e.bet <= prevLevel {
|
|
continue
|
|
}
|
|
|
|
level := e.bet
|
|
potSlice := int64(0)
|
|
|
|
// All players who bet >= level contribute (level - prevLevel) each.
|
|
var eligible []id.UserID
|
|
for _, p := range g.Players {
|
|
if p.State == PlayerFolded || p.State == PlayerSatOut {
|
|
// Folded players still contributed up to their TotalBet.
|
|
contrib := p.TotalBet - prevLevel
|
|
if contrib > level-prevLevel {
|
|
contrib = level - prevLevel
|
|
}
|
|
if contrib > 0 {
|
|
potSlice += contrib
|
|
}
|
|
continue
|
|
}
|
|
|
|
contrib := p.TotalBet - prevLevel
|
|
if contrib > level-prevLevel {
|
|
contrib = level - prevLevel
|
|
}
|
|
if contrib > 0 {
|
|
potSlice += contrib
|
|
}
|
|
if p.TotalBet >= level {
|
|
eligible = append(eligible, p.UserID)
|
|
}
|
|
}
|
|
|
|
if potSlice > 0 {
|
|
sidePots = append(sidePots, SidePot{Amount: potSlice, Eligible: eligible})
|
|
}
|
|
|
|
prevLevel = level
|
|
}
|
|
|
|
if len(sidePots) > 0 {
|
|
g.SidePots = sidePots
|
|
g.Pot = 0
|
|
}
|
|
}
|
|
|
|
// returnUncalledBet returns any unmatched portion of a bet to the player.
|
|
func (g *HoldemGame) returnUncalledBet() (name string, amount int64) {
|
|
// Find the highest and second-highest bets among non-folded players.
|
|
var highest, secondHighest int64
|
|
var highestIdx int
|
|
|
|
for i, p := range g.Players {
|
|
if p.State == PlayerFolded || p.State == PlayerSatOut {
|
|
continue
|
|
}
|
|
if p.TotalBet > highest {
|
|
secondHighest = highest
|
|
highest = p.TotalBet
|
|
highestIdx = i
|
|
} else if p.TotalBet > secondHighest {
|
|
secondHighest = p.TotalBet
|
|
}
|
|
}
|
|
|
|
excess := highest - secondHighest
|
|
if excess > 0 && secondHighest > 0 {
|
|
p := g.Players[highestIdx]
|
|
p.Stack += excess
|
|
p.TotalBet -= excess
|
|
// Don't adjust per-street Bet — it may be smaller than the hand-total
|
|
// excess, and it's irrelevant since the hand is ending.
|
|
return p.DisplayName, excess
|
|
}
|
|
|
|
return "", 0
|
|
}
|