757 lines
18 KiB
Go
757 lines
18 KiB
Go
package poker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Phase represents the current state of a poker hand.
|
|
type Phase string
|
|
|
|
const (
|
|
PhaseWaiting Phase = "waiting" // waiting for enough players
|
|
PhaseDealing Phase = "dealing" // cards being dealt (brief transition)
|
|
PhasePreflop Phase = "preflop"
|
|
PhaseFlop Phase = "flop"
|
|
PhaseTurn Phase = "turn"
|
|
PhaseRiver Phase = "river"
|
|
PhaseShowdown Phase = "showdown"
|
|
)
|
|
|
|
// Config holds immutable table configuration.
|
|
type Config struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
SeatCount int `json:"seatCount"`
|
|
SmallBlind int64 `json:"smallBlind"`
|
|
BigBlind int64 `json:"bigBlind"`
|
|
MinBuyin int64 `json:"minBuyin"`
|
|
MaxBuyin int64 `json:"maxBuyin"`
|
|
TurnTimer int `json:"turnTimer"` // seconds
|
|
}
|
|
|
|
// Seat holds per-seat state.
|
|
type Seat struct {
|
|
Username string `json:"username,omitempty"`
|
|
Stack int64 `json:"stack"`
|
|
Bet int64 `json:"bet"` // current street bet
|
|
Folded bool `json:"folded"`
|
|
SitOut bool `json:"sitOut"`
|
|
Revealed bool `json:"revealed"` // hole cards shown to all
|
|
}
|
|
|
|
// HandState holds the mutable state for a single hand.
|
|
type HandState struct {
|
|
Phase Phase `json:"phase"`
|
|
Community []string `json:"community"`
|
|
Pot int64 `json:"pot"`
|
|
ActionOn int `json:"actionOn"` // seat index
|
|
ActionDeadline int64 `json:"actionDeadline"` // unix timestamp
|
|
DealerSeat int `json:"dealerSeat"`
|
|
LastRaise int64 `json:"lastRaise"`
|
|
MinRaise int64 `json:"minRaise"`
|
|
}
|
|
|
|
// Table is the central struct — holds config, seats, and hand state.
|
|
type Table struct {
|
|
mu sync.RWMutex
|
|
Config Config
|
|
Seats []*Seat
|
|
Hand HandState
|
|
|
|
deck []string
|
|
hands map[string][]string // username -> hole cards, never sent to other players
|
|
folded map[string]bool
|
|
acted map[string]bool
|
|
|
|
cancelTurn context.CancelFunc // cancels the current turn timer
|
|
|
|
// Broadcast is set by the manager — called whenever state changes
|
|
// that all connected clients should know about.
|
|
Broadcast func(msg interface{})
|
|
|
|
// SendPrivate sends a message to a single username only.
|
|
SendPrivate func(username string, msg interface{})
|
|
}
|
|
|
|
// NewTable creates a Table from a Config.
|
|
func NewTable(cfg Config) *Table {
|
|
seats := make([]*Seat, cfg.SeatCount)
|
|
for i := range seats {
|
|
seats[i] = &Seat{}
|
|
}
|
|
return &Table{
|
|
Config: cfg,
|
|
Seats: seats,
|
|
Hand: HandState{Phase: PhaseWaiting, Community: []string{}},
|
|
hands: make(map[string][]string),
|
|
folded: make(map[string]bool),
|
|
acted: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
// ── Seat management ──────────────────────────────────────────────
|
|
|
|
// Sit places a player in a seat and deducts the buy-in from their stack.
|
|
// The caller is responsible for deducting from the player's wallet first.
|
|
func (t *Table) Sit(username string, seatIndex int, buyin int64) error {
|
|
t.mu.Lock()
|
|
|
|
if seatIndex < 0 || seatIndex >= len(t.Seats) {
|
|
t.mu.Unlock()
|
|
return errors.New("invalid seat index")
|
|
}
|
|
if t.Seats[seatIndex].Username != "" {
|
|
t.mu.Unlock()
|
|
return errors.New("seat taken")
|
|
}
|
|
for _, s := range t.Seats {
|
|
if s.Username == username {
|
|
t.mu.Unlock()
|
|
return errors.New("already seated")
|
|
}
|
|
}
|
|
if buyin < t.Config.MinBuyin || buyin > t.Config.MaxBuyin {
|
|
t.mu.Unlock()
|
|
return fmt.Errorf("buy-in must be between %d and %d", t.Config.MinBuyin, t.Config.MaxBuyin)
|
|
}
|
|
|
|
t.Seats[seatIndex].Username = username
|
|
t.Seats[seatIndex].Stack = buyin
|
|
t.Seats[seatIndex].Folded = false
|
|
t.Seats[seatIndex].SitOut = false
|
|
t.mu.Unlock()
|
|
|
|
t.broadcastState()
|
|
t.maybeStartHand()
|
|
return nil
|
|
}
|
|
|
|
// Stand removes a player from their seat and returns their remaining stack.
|
|
// Cannot stand mid-hand unless folded.
|
|
func (t *Table) Stand(username string) (int64, error) {
|
|
t.mu.Lock()
|
|
|
|
seat := t.findSeat(username)
|
|
if seat == nil {
|
|
t.mu.Unlock()
|
|
return 0, errors.New("not seated")
|
|
}
|
|
// Allow standing if folded OR if marked as disconnected (SitOut mid-hand)
|
|
if t.Hand.Phase != PhaseWaiting && !seat.Folded && !seat.SitOut {
|
|
t.mu.Unlock()
|
|
return 0, errors.New("cannot stand mid-hand while active")
|
|
}
|
|
|
|
stack := seat.Stack
|
|
*seat = Seat{}
|
|
t.mu.Unlock()
|
|
|
|
t.broadcastState()
|
|
return stack, nil
|
|
}
|
|
|
|
func (t *Table) Disconnect(username string) (int64, bool) {
|
|
t.mu.Lock()
|
|
seat := t.findSeat(username)
|
|
if seat == nil {
|
|
t.mu.Unlock()
|
|
return 0, false
|
|
}
|
|
|
|
if t.Hand.Phase == PhaseWaiting {
|
|
stack := seat.Stack
|
|
*seat = Seat{}
|
|
t.mu.Unlock()
|
|
t.broadcastState()
|
|
return stack, true // safe to refund immediately
|
|
}
|
|
|
|
// Mid-hand — mark sitout and let the hand clean up
|
|
seat.SitOut = true
|
|
if t.Seats[t.Hand.ActionOn].Username == username {
|
|
if t.cancelTurn != nil {
|
|
t.cancelTurn()
|
|
}
|
|
}
|
|
t.mu.Unlock()
|
|
t.broadcastState()
|
|
return 0, false // refund will happen via player:disconnect:stand after hand ends
|
|
}
|
|
|
|
// TopUp adds chips to a seated player's stack (between hands only).
|
|
func (t *Table) TopUp(username string, amount int64) error {
|
|
t.mu.Lock()
|
|
|
|
if t.Hand.Phase != PhaseWaiting {
|
|
t.mu.Unlock()
|
|
return errors.New("can only top up between hands")
|
|
}
|
|
seat := t.findSeat(username)
|
|
if seat == nil {
|
|
t.mu.Unlock()
|
|
return errors.New("not seated")
|
|
}
|
|
if seat.Stack+amount > t.Config.MaxBuyin {
|
|
t.mu.Unlock()
|
|
return fmt.Errorf("would exceed max buy-in of %d", t.Config.MaxBuyin)
|
|
}
|
|
seat.Stack += amount
|
|
t.mu.Unlock()
|
|
|
|
t.broadcastState()
|
|
return nil
|
|
}
|
|
|
|
// ── Hand flow ────────────────────────────────────────────────────
|
|
|
|
func (t *Table) activeSeatCount() int {
|
|
n := 0
|
|
for _, s := range t.Seats {
|
|
if s.Username != "" && !s.SitOut && s.Stack > 0 {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (t *Table) maybeStartHand() {
|
|
if t.Hand.Phase == PhaseWaiting && t.activeSeatCount() >= 2 {
|
|
go t.startHand()
|
|
}
|
|
}
|
|
|
|
func (t *Table) startHand() {
|
|
t.mu.Lock()
|
|
|
|
// Reset per-hand state
|
|
t.deck = NewDeck()
|
|
t.hands = make(map[string][]string)
|
|
t.folded = make(map[string]bool)
|
|
t.acted = make(map[string]bool)
|
|
|
|
for _, s := range t.Seats {
|
|
s.Bet = 0
|
|
s.Folded = false
|
|
s.Revealed = false
|
|
}
|
|
|
|
// Advance dealer button
|
|
t.Hand.DealerSeat = t.nextActiveSeat(t.Hand.DealerSeat)
|
|
t.Hand.Community = []string{}
|
|
t.Hand.Pot = 0
|
|
t.Hand.Phase = PhaseDealing
|
|
|
|
// Deal hole cards
|
|
active := t.activeSeats()
|
|
for _, idx := range active {
|
|
var cards []string
|
|
cards, t.deck = Deal(t.deck, 2)
|
|
t.hands[t.Seats[idx].Username] = cards
|
|
}
|
|
|
|
t.mu.Unlock()
|
|
|
|
// Send private hole cards
|
|
for username, cards := range t.hands {
|
|
if t.SendPrivate != nil {
|
|
t.SendPrivate(username, map[string]interface{}{
|
|
"type": "hand:dealt",
|
|
"cards": cards,
|
|
})
|
|
}
|
|
}
|
|
|
|
t.mu.Lock()
|
|
|
|
// Post blinds
|
|
sbSeat := t.nextActiveSeat(t.Hand.DealerSeat)
|
|
bbSeat := t.nextActiveSeat(sbSeat)
|
|
t.postBlind(sbSeat, t.Config.SmallBlind)
|
|
t.postBlind(bbSeat, t.Config.BigBlind)
|
|
|
|
t.Hand.LastRaise = t.Config.BigBlind
|
|
t.Hand.MinRaise = t.Config.BigBlind * 2
|
|
t.Hand.Phase = PhasePreflop
|
|
|
|
// Action starts left of big blind
|
|
t.Hand.ActionOn = t.nextActiveSeat(bbSeat)
|
|
t.mu.Unlock()
|
|
|
|
t.broadcastState()
|
|
t.startTurnTimer()
|
|
}
|
|
|
|
func (t *Table) postBlind(seatIdx int, amount int64) {
|
|
seat := t.Seats[seatIdx]
|
|
if seat.Stack < amount {
|
|
amount = seat.Stack // all-in blind
|
|
}
|
|
seat.Stack -= amount
|
|
seat.Bet += amount
|
|
t.Hand.Pot += amount
|
|
}
|
|
|
|
// ── Player actions ───────────────────────────────────────────────
|
|
|
|
type ActionType string
|
|
|
|
const (
|
|
ActionFold ActionType = "fold"
|
|
ActionCheck ActionType = "check"
|
|
ActionCall ActionType = "call"
|
|
ActionRaise ActionType = "raise"
|
|
)
|
|
|
|
// Action processes a player's action. Returns an error if invalid.
|
|
func (t *Table) Action(username string, action ActionType, amount int64) error {
|
|
t.mu.Lock()
|
|
|
|
if t.Hand.Phase == PhaseWaiting || t.Hand.Phase == PhaseDealing || t.Hand.Phase == PhaseShowdown {
|
|
t.mu.Unlock()
|
|
return errors.New("no action in current phase")
|
|
}
|
|
|
|
seatIdx := t.findSeatIndex(username)
|
|
if seatIdx < 0 {
|
|
t.mu.Unlock()
|
|
return errors.New("not seated")
|
|
}
|
|
if seatIdx != t.Hand.ActionOn {
|
|
t.mu.Unlock()
|
|
return errors.New("not your turn")
|
|
}
|
|
|
|
seat := t.Seats[seatIdx]
|
|
currentBet := t.maxBet()
|
|
|
|
switch action {
|
|
case ActionFold:
|
|
seat.Folded = true
|
|
t.folded[username] = true
|
|
|
|
case ActionCheck:
|
|
if seat.Bet < currentBet {
|
|
t.mu.Unlock()
|
|
return errors.New("cannot check — must call or raise")
|
|
}
|
|
|
|
case ActionCall:
|
|
toCall := currentBet - seat.Bet
|
|
if toCall <= 0 {
|
|
t.mu.Unlock()
|
|
return errors.New("nothing to call — check instead")
|
|
}
|
|
if toCall > seat.Stack {
|
|
toCall = seat.Stack // all-in
|
|
}
|
|
seat.Stack -= toCall
|
|
seat.Bet += toCall
|
|
t.Hand.Pot += toCall
|
|
|
|
case ActionRaise:
|
|
if amount < t.Hand.MinRaise {
|
|
t.mu.Unlock()
|
|
return fmt.Errorf("minimum raise is %d", t.Hand.MinRaise)
|
|
}
|
|
toCall := currentBet - seat.Bet
|
|
total := toCall + amount
|
|
if total > seat.Stack {
|
|
total = seat.Stack // all-in
|
|
}
|
|
seat.Stack -= total
|
|
seat.Bet += total
|
|
t.Hand.Pot += total
|
|
t.Hand.LastRaise = amount
|
|
t.Hand.MinRaise = currentBet + amount
|
|
t.acted = make(map[string]bool) // raise reopens action
|
|
|
|
default:
|
|
t.mu.Unlock()
|
|
return fmt.Errorf("unknown action: %s", action)
|
|
}
|
|
|
|
t.acted[username] = true
|
|
|
|
// Cancel the current turn timer
|
|
if t.cancelTurn != nil {
|
|
t.cancelTurn()
|
|
t.cancelTurn = nil
|
|
}
|
|
|
|
t.mu.Unlock()
|
|
t.broadcastAction(username, action, amount)
|
|
t.advance()
|
|
return nil
|
|
}
|
|
|
|
// Reveal marks a player's hole cards as public.
|
|
func (t *Table) Reveal(username string) {
|
|
t.mu.Lock()
|
|
seat := t.findSeat(username)
|
|
if seat == nil {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
seat.Revealed = true
|
|
cards := t.hands[username]
|
|
t.mu.Unlock()
|
|
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(map[string]interface{}{
|
|
"type": "hand:reveal",
|
|
"username": username,
|
|
"cards": cards,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Street advancement ───────────────────────────────────────────
|
|
|
|
func (t *Table) advance() {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
// Check if only one player remains
|
|
if t.countActivePlayers() == 1 {
|
|
t.awardPot()
|
|
return
|
|
}
|
|
|
|
// Check if betting is complete for this street
|
|
if !t.bettingComplete() {
|
|
t.Hand.ActionOn = t.nextActiveSeat(t.Hand.ActionOn)
|
|
t.mu.Unlock()
|
|
t.broadcastState()
|
|
t.startTurnTimer()
|
|
t.mu.Lock()
|
|
return
|
|
}
|
|
|
|
// Advance to next street
|
|
switch t.Hand.Phase {
|
|
case PhasePreflop:
|
|
var cards []string
|
|
cards, t.deck = Deal(t.deck, 3)
|
|
t.Hand.Community = cards
|
|
t.Hand.Phase = PhaseFlop
|
|
case PhaseFlop:
|
|
var cards []string
|
|
cards, t.deck = Deal(t.deck, 1)
|
|
t.Hand.Community = append(t.Hand.Community, cards...)
|
|
t.Hand.Phase = PhaseTurn
|
|
case PhaseTurn:
|
|
var cards []string
|
|
cards, t.deck = Deal(t.deck, 1)
|
|
t.Hand.Community = append(t.Hand.Community, cards...)
|
|
t.Hand.Phase = PhaseRiver
|
|
case PhaseRiver:
|
|
t.Hand.Phase = PhaseShowdown
|
|
t.mu.Unlock()
|
|
t.doShowdown()
|
|
t.mu.Lock()
|
|
return
|
|
}
|
|
|
|
// Reset bets for new street
|
|
for _, s := range t.Seats {
|
|
s.Bet = 0
|
|
}
|
|
t.acted = make(map[string]bool)
|
|
t.Hand.LastRaise = 0
|
|
t.Hand.MinRaise = t.Config.BigBlind
|
|
|
|
// Action starts left of dealer post-flop
|
|
t.Hand.ActionOn = t.nextActiveSeat(t.Hand.DealerSeat)
|
|
t.mu.Unlock()
|
|
t.broadcastState()
|
|
t.startTurnTimer()
|
|
t.mu.Lock()
|
|
}
|
|
|
|
func (t *Table) doShowdown() {
|
|
remaining := make(map[string][]string)
|
|
for username, cards := range t.hands {
|
|
if !t.folded[username] {
|
|
remaining[username] = cards
|
|
}
|
|
}
|
|
|
|
winners := Winners(remaining, t.Hand.Community)
|
|
|
|
revealedHands := make(map[string][]string)
|
|
for _, w := range winners {
|
|
revealedHands[w.Username] = remaining[w.Username]
|
|
}
|
|
t.mu.RLock()
|
|
for _, seat := range t.Seats {
|
|
if seat.Revealed && seat.Username != "" {
|
|
revealedHands[seat.Username] = t.hands[seat.Username]
|
|
}
|
|
}
|
|
t.mu.RUnlock()
|
|
|
|
winnerNames := make([]string, len(winners))
|
|
winnerDescs := make(map[string]string)
|
|
for i, w := range winners {
|
|
winnerNames[i] = w.Username
|
|
winnerDescs[w.Username] = w.Desc
|
|
}
|
|
|
|
share := t.Hand.Pot / int64(len(winners))
|
|
remainder := t.Hand.Pot % int64(len(winners))
|
|
t.mu.Lock()
|
|
for i, w := range winners {
|
|
seat := t.findSeat(w.Username)
|
|
if seat != nil {
|
|
extra := int64(0)
|
|
if i == 0 {
|
|
extra = remainder
|
|
}
|
|
seat.Stack += share + extra
|
|
}
|
|
}
|
|
t.Hand.Pot = 0
|
|
t.mu.Unlock()
|
|
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(map[string]interface{}{
|
|
"type": "hand:end",
|
|
"winners": winnerNames,
|
|
"descs": winnerDescs,
|
|
"hands": revealedHands,
|
|
})
|
|
}
|
|
|
|
time.Sleep(5 * time.Second)
|
|
t.mu.Lock()
|
|
t.Hand.Phase = PhaseWaiting
|
|
t.cleanupSeats()
|
|
t.mu.Unlock()
|
|
t.broadcastState()
|
|
t.mu.Lock()
|
|
t.maybeStartHand()
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *Table) awardPot() {
|
|
for _, s := range t.Seats {
|
|
if s.Username != "" && !s.Folded {
|
|
s.Stack += t.Hand.Pot
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(map[string]interface{}{
|
|
"type": "hand:end",
|
|
"winners": []string{s.Username},
|
|
"descs": map[string]string{},
|
|
"hands": map[string][]string{},
|
|
})
|
|
}
|
|
break
|
|
}
|
|
}
|
|
t.Hand.Pot = 0
|
|
time.Sleep(2 * time.Second)
|
|
t.mu.Lock()
|
|
t.Hand.Phase = PhaseWaiting
|
|
t.cleanupSeats()
|
|
t.mu.Unlock()
|
|
t.broadcastState()
|
|
t.maybeStartHand()
|
|
}
|
|
|
|
// ── Turn timer ───────────────────────────────────────────────────
|
|
|
|
func (t *Table) startTurnTimer() {
|
|
t.mu.Lock()
|
|
if t.cancelTurn != nil {
|
|
t.cancelTurn()
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(),
|
|
time.Duration(t.Config.TurnTimer)*time.Second)
|
|
t.cancelTurn = cancel
|
|
username := t.Seats[t.Hand.ActionOn].Username
|
|
t.Hand.ActionDeadline = time.Now().Add(
|
|
time.Duration(t.Config.TurnTimer) * time.Second).Unix()
|
|
t.mu.Unlock()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
log.Printf("[poker] turn timer expired for %s at table %s", username, t.Config.ID)
|
|
t.Action(username, ActionFold, 0)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
|
|
func (t *Table) findSeat(username string) *Seat {
|
|
for _, s := range t.Seats {
|
|
if s.Username == username {
|
|
return s
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *Table) findSeatIndex(username string) int {
|
|
for i, s := range t.Seats {
|
|
if s.Username == username {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (t *Table) activeSeats() []int {
|
|
var idxs []int
|
|
for i, s := range t.Seats {
|
|
if s.Username != "" && !s.SitOut && s.Stack > 0 {
|
|
idxs = append(idxs, i)
|
|
}
|
|
}
|
|
return idxs
|
|
}
|
|
|
|
func (t *Table) nextActiveSeat(from int) int {
|
|
n := len(t.Seats)
|
|
for i := 1; i <= n; i++ {
|
|
idx := (from + i) % n
|
|
s := t.Seats[idx]
|
|
if s.Username != "" && !s.SitOut && !s.Folded && s.Stack > 0 {
|
|
return idx
|
|
}
|
|
}
|
|
return from
|
|
}
|
|
|
|
func (t *Table) countActivePlayers() int {
|
|
n := 0
|
|
for _, s := range t.Seats {
|
|
if s.Username != "" && !s.Folded {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (t *Table) maxBet() int64 {
|
|
var max int64
|
|
for _, s := range t.Seats {
|
|
if s.Bet > max {
|
|
max = s.Bet
|
|
}
|
|
}
|
|
return max
|
|
}
|
|
|
|
func (t *Table) bettingComplete() bool {
|
|
currentBet := t.maxBet()
|
|
for _, s := range t.Seats {
|
|
if s.Username == "" || s.Folded || s.SitOut {
|
|
continue
|
|
}
|
|
if s.Stack == 0 {
|
|
continue // all-in
|
|
}
|
|
if !t.acted[s.Username] {
|
|
return false
|
|
}
|
|
if s.Bet < currentBet {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (t *Table) cleanupSeats() {
|
|
for _, s := range t.Seats {
|
|
if s.Username == "" {
|
|
continue
|
|
}
|
|
if s.Stack == 0 {
|
|
log.Printf("[poker] %s busted", s.Username)
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(map[string]interface{}{
|
|
"type": "player:bust",
|
|
"username": s.Username,
|
|
"tableId": t.Config.ID,
|
|
})
|
|
}
|
|
*s = Seat{}
|
|
} else if s.SitOut {
|
|
log.Printf("[poker] standing disconnected player %s with %d chips", s.Username, s.Stack)
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(map[string]interface{}{
|
|
"type": "player:disconnect:stand",
|
|
"username": s.Username,
|
|
"stack": s.Stack,
|
|
"tableId": t.Config.ID,
|
|
})
|
|
}
|
|
*s = Seat{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── State broadcast ──────────────────────────────────────────────
|
|
|
|
// PublicState returns the table state safe to send to all clients.
|
|
// Hole cards are never included.
|
|
func (t *Table) PublicState() map[string]interface{} {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
seats := make([]map[string]interface{}, len(t.Seats))
|
|
for i, s := range t.Seats {
|
|
seats[i] = map[string]interface{}{
|
|
"username": s.Username,
|
|
"stack": s.Stack,
|
|
"bet": s.Bet,
|
|
"folded": s.Folded,
|
|
"sitOut": s.SitOut,
|
|
"revealed": s.Revealed,
|
|
}
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"type": "table:state",
|
|
"tableId": t.Config.ID,
|
|
"phase": t.Hand.Phase,
|
|
"community": t.Hand.Community,
|
|
"pot": t.Hand.Pot,
|
|
"actionOn": t.Hand.ActionOn,
|
|
"actionDeadline": t.Hand.ActionDeadline,
|
|
"dealerSeat": t.Hand.DealerSeat,
|
|
"seats": seats,
|
|
"config": t.Config,
|
|
}
|
|
}
|
|
|
|
func (t *Table) broadcastState() {
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(t.PublicState())
|
|
}
|
|
}
|
|
|
|
func (t *Table) broadcastAction(username string, action ActionType, amount int64) {
|
|
if t.Broadcast != nil {
|
|
t.Broadcast(map[string]interface{}{
|
|
"type": "player:action",
|
|
"tableId": t.Config.ID,
|
|
"username": username,
|
|
"action": action,
|
|
"amount": amount,
|
|
})
|
|
}
|
|
}
|
|
|
|
// MarshalConfig serialises the table config to JSON for Redis storage.
|
|
func (c Config) Marshal() ([]byte, error) {
|
|
return json.Marshal(c)
|
|
}
|