dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

poker.go (76354B)


      1 package poker
      2 
      3 import (
      4 	"bytes"
      5 	"dkforest/pkg/database"
      6 	"dkforest/pkg/pubsub"
      7 	"dkforest/pkg/utils"
      8 	"dkforest/pkg/utils/rwmtx"
      9 	hutils "dkforest/pkg/web/handlers/utils"
     10 	"errors"
     11 	"fmt"
     12 	"github.com/chehsunliu/poker"
     13 	"github.com/sirupsen/logrus"
     14 	"html/template"
     15 	"math"
     16 	"math/rand"
     17 	"sort"
     18 	"strconv"
     19 	"strings"
     20 	"sync"
     21 	"sync/atomic"
     22 	"time"
     23 )
     24 
     25 const NbPlayers = 6
     26 const MaxUserCountdown = 60
     27 const MinTimeAfterGame = 10
     28 const BackfacingDeg = "-180deg"
     29 const BurnStackX = 400
     30 const BurnStackY = 30
     31 const DealX = 155
     32 const DealY = 130
     33 const DealSpacing = 55
     34 const DealerStackX = 250
     35 const DealerStackY = 30
     36 const NbCardsPerPlayer = 2
     37 const animationTime = 1000 * time.Millisecond
     38 const RakeBackPct = 0.30
     39 
     40 type Poker struct {
     41 	sync.Mutex
     42 	games map[RoomID]*Game
     43 }
     44 
     45 func newPoker() *Poker {
     46 	p := &Poker{}
     47 	p.games = make(map[RoomID]*Game)
     48 	return p
     49 }
     50 
     51 func (p *Poker) GetGame(roomID RoomID) *Game {
     52 	p.Lock()
     53 	defer p.Unlock()
     54 	g, found := PokerInstance.games[roomID]
     55 	if !found {
     56 		return nil
     57 	}
     58 	return g
     59 }
     60 
     61 func (p *Poker) GetOrCreateGame(db *database.DkfDB, roomID RoomID, pokerTableID int64,
     62 	pokerTableMinBet database.PokerChip, pokerTableIsTest bool) *Game {
     63 	p.Lock()
     64 	defer p.Unlock()
     65 	g, found := p.games[roomID]
     66 	if !found {
     67 		g = p.createGame(db, roomID, pokerTableID, pokerTableMinBet, pokerTableIsTest)
     68 	}
     69 	return g
     70 }
     71 
     72 func (p *Poker) CreateGame(db *database.DkfDB, roomID RoomID, pokerTableID int64,
     73 	pokerTableMinBet database.PokerChip, pokerTableIsTest bool) *Game {
     74 	p.Lock()
     75 	defer p.Unlock()
     76 	return p.createGame(db, roomID, pokerTableID, pokerTableMinBet, pokerTableIsTest)
     77 }
     78 
     79 func (p *Poker) createGame(db *database.DkfDB, roomID RoomID, pokerTableID int64,
     80 	pokerTableMinBet database.PokerChip, pokerTableIsTest bool) *Game {
     81 	g := p.newGame(db, roomID, pokerTableID, pokerTableMinBet, pokerTableIsTest)
     82 	p.games[roomID] = g
     83 	return g
     84 }
     85 
     86 func (p *Poker) newGame(db *database.DkfDB, roomID RoomID, pokerTableID int64,
     87 	pokerTableMinBet database.PokerChip, pokerTableIsTest bool) *Game {
     88 	g := &Game{
     89 		db:               db,
     90 		roomID:           roomID,
     91 		pokerTableID:     pokerTableID,
     92 		tableType:        TableTypeRake,
     93 		pokerTableMinBet: pokerTableMinBet,
     94 		pokerTableIsTest: pokerTableIsTest,
     95 		playersEventCh:   make(chan playerEvent),
     96 		Players:          rwmtx.New(make(seatedPlayers, NbPlayers)),
     97 		seatsAnimation:   make([]bool, NbPlayers),
     98 		dealerSeatIdx:    atomic.Int32{},
     99 	}
    100 	g.dealerSeatIdx.Store(-1)
    101 	return g
    102 }
    103 
    104 type playerEvent struct {
    105 	UserID database.UserID
    106 	Call   bool
    107 	Check  bool
    108 	Fold   bool
    109 	AllIn  bool
    110 	Unsit  bool
    111 	Raise  bool
    112 	Bet    database.PokerChip
    113 }
    114 
    115 func (e playerEvent) getAction() PlayerAction {
    116 	action := NoAction
    117 	if e.Fold {
    118 		action = FoldAction
    119 	} else if e.Call {
    120 		action = CallAction
    121 	} else if e.Check {
    122 		action = CheckAction
    123 	} else if e.Bet > 0 {
    124 		action = BetAction
    125 	} else if e.Raise {
    126 		action = RaiseAction
    127 	} else if e.AllIn {
    128 		action = AllInAction
    129 	}
    130 	return action
    131 }
    132 
    133 var PokerInstance = newPoker()
    134 
    135 type ongoingGame struct {
    136 	logEvents       rwmtx.RWMtxSlice[LogEvent]
    137 	events          rwmtx.RWMtxSlice[PokerEvent]
    138 	waitTurnEvent   rwmtx.RWMtx[PokerWaitTurnEvent]
    139 	autoActionEvent rwmtx.RWMtx[AutoActionEvent]
    140 	mainPot         rwmtx.RWMtx[database.PokerChip]
    141 	minBet          rwmtx.RWMtx[database.PokerChip]
    142 	minRaise        rwmtx.RWMtx[database.PokerChip]
    143 	playerToPlay    rwmtx.RWMtx[database.UserID]
    144 	hasBet          rwmtx.RWMtx[bool]
    145 	players         pokerPlayers
    146 	createdAt       time.Time
    147 	communityCards  []string
    148 	deck            []string
    149 }
    150 
    151 type pokerPlayers []*PokerPlayer
    152 
    153 type seatedPlayers []*seatedPlayer
    154 
    155 func (p pokerPlayers) get(userID database.UserID) *PokerPlayer {
    156 	for _, player := range p {
    157 		if player != nil && player.userID == userID {
    158 			return player
    159 		}
    160 	}
    161 	return nil
    162 }
    163 
    164 func (p seatedPlayers) get(userID database.UserID) (out *seatedPlayer) {
    165 	for _, player := range p {
    166 		if player != nil && player.userID == userID {
    167 			return player
    168 		}
    169 	}
    170 	return
    171 }
    172 
    173 func (p seatedPlayers) resetStatuses() {
    174 	for _, player := range p {
    175 		player.status.Set("")
    176 	}
    177 }
    178 
    179 func (p seatedPlayers) toPokerPlayers() pokerPlayers {
    180 	players := make([]*PokerPlayer, 0)
    181 	for _, player := range p {
    182 		players = append(players, &PokerPlayer{seatedPlayer: player})
    183 	}
    184 	return players
    185 }
    186 
    187 func (g *Game) getEligibles() (out seatedPlayers) {
    188 	eligiblePlayers := make(seatedPlayers, 0)
    189 	g.Players.RWith(func(gPlayers seatedPlayers) {
    190 		for _, p := range gPlayers {
    191 			if p.isEligible(g.pokerTableMinBet) {
    192 				eligiblePlayers = append(eligiblePlayers, p)
    193 			}
    194 		}
    195 	})
    196 	return eligiblePlayers
    197 }
    198 
    199 type seatedPlayer struct {
    200 	seatIdx         int
    201 	userID          database.UserID
    202 	username        database.Username
    203 	cash            rwmtx.RWMtxUInt64[database.PokerChip]
    204 	status          rwmtx.RWMtx[string]
    205 	hasChecked      bool
    206 	lastActionTS    time.Time
    207 	pokerReferredBy *database.UserID
    208 }
    209 
    210 func (p *seatedPlayer) getCash() (out database.PokerChip) {
    211 	return p.cash.Get()
    212 }
    213 
    214 func (p *seatedPlayer) getStatus() (out string) {
    215 	return p.status.Get()
    216 }
    217 
    218 // Return either or not a player is eligible to play a game
    219 func (p *seatedPlayer) isEligible(pokerTableMinBet database.PokerChip) bool {
    220 	return p != nil && p.getCash() >= pokerTableMinBet
    221 }
    222 
    223 type PokerPlayer struct {
    224 	*seatedPlayer
    225 	bet                  rwmtx.RWMtxUInt64[database.PokerChip]
    226 	cards                rwmtx.RWMtxSlice[playerCard]
    227 	folded               atomic.Bool
    228 	unsit                atomic.Bool
    229 	gameBet              database.PokerChip
    230 	allInMaxGain         database.PokerChip
    231 	rakePaid             float64
    232 	countChancesToAction int
    233 }
    234 
    235 func (p *PokerPlayer) maxGain(mainPot database.PokerChip) database.PokerChip {
    236 	m := utils.MinInt(p.allInMaxGain, mainPot)
    237 	return utils.Ternary(p.isAllIn(), m, mainPot)
    238 }
    239 
    240 func (g *Game) IsBet() (out bool) {
    241 	if g.ongoing != nil {
    242 		return !g.ongoing.hasBet.Get()
    243 	}
    244 	return
    245 }
    246 
    247 func (g *Game) IsYourTurn(player *PokerPlayer) (out bool) {
    248 	if g.ongoing != nil {
    249 		return player.userID == g.ongoing.playerToPlay.Get()
    250 	}
    251 	return
    252 }
    253 
    254 func (g *Game) CanCheck(player *PokerPlayer) (out bool) {
    255 	if g.ongoing != nil {
    256 		return player.bet.Get() == g.ongoing.minBet.Get()
    257 	}
    258 	return
    259 }
    260 
    261 func (g *Game) CanFold(player *PokerPlayer) (out bool) {
    262 	if g.ongoing != nil {
    263 		return player.bet.Get() < g.ongoing.minBet.Get()
    264 	}
    265 	return
    266 }
    267 
    268 func (g *Game) MinBet() (out database.PokerChip) {
    269 	if g.ongoing != nil {
    270 		return g.ongoing.minBet.Get()
    271 	}
    272 	return
    273 }
    274 
    275 func (p *Game) MinRaise() (out database.PokerChip) {
    276 	if p.ongoing != nil {
    277 		return p.ongoing.minRaise.Get()
    278 	}
    279 	return
    280 }
    281 
    282 func (p *PokerPlayer) GetBet() (out database.PokerChip) {
    283 	return p.bet.Get()
    284 }
    285 
    286 func (p *PokerPlayer) canBet() bool {
    287 	return !p.folded.Load() && !p.isAllIn()
    288 }
    289 
    290 func (p *PokerPlayer) isAllIn() bool {
    291 	return p.getCash() == 0
    292 }
    293 
    294 func (p *PokerPlayer) refundPartialBet(db *database.DkfDB, pokerTableID int64, diff database.PokerChip) {
    295 	_ = db.PokerTableAccountRefundPartialBet(p.userID, pokerTableID, diff)
    296 	p.tmp(-diff)
    297 }
    298 
    299 func (p *PokerPlayer) doBet(db *database.DkfDB, pokerTableID int64, bet database.PokerChip) {
    300 	_ = db.PokerTableAccountBet(p.userID, pokerTableID, bet)
    301 	p.tmp(bet)
    302 }
    303 
    304 func (p *PokerPlayer) tmp(diff database.PokerChip) {
    305 	p.gameBet += diff
    306 	p.bet.Incr(diff)
    307 	p.cash.Incr(-diff)
    308 }
    309 
    310 func (p *PokerPlayer) gain(db *database.DkfDB, pokerTableID int64, gain database.PokerChip) {
    311 	_ = db.PokerTableAccountGain(p.userID, pokerTableID, gain)
    312 	p.cash.Incr(gain)
    313 	p.bet.Set(0)
    314 }
    315 
    316 // Reset player's bet to 0 and return the value it had before the reset
    317 func (p *PokerPlayer) resetBet() (old database.PokerChip) {
    318 	// Do not track in database
    319 	// DB keeps track of what was bet during the whole (1 hand) game
    320 	return p.bet.Replace(0)
    321 }
    322 
    323 func (p *PokerPlayer) refundBet(db *database.DkfDB, pokerTableID int64) {
    324 	p.gain(db, pokerTableID, p.GetBet())
    325 }
    326 
    327 func (p *PokerPlayer) doBetAndNotif(g *Game, bet database.PokerChip) {
    328 	p.doBet(g.db, g.pokerTableID, bet)
    329 	PubSub.Pub(g.roomID.Topic(), PlayerBetEvent{PlayerSeatIdx: p.seatIdx, Player: p.username, Bet: bet, TotalBet: p.GetBet(), Cash: p.getCash()})
    330 	pubCashBonus(g, p.seatIdx, bet, false)
    331 }
    332 
    333 func pubCashBonus(g *Game, seatIdx int, amount database.PokerChip, isGain bool) {
    334 	g.seatsAnimation[seatIdx] = !g.seatsAnimation[seatIdx]
    335 	PubSub.Pub(g.roomID.Topic(), CashBonusEvent{PlayerSeatIdx: seatIdx, Amount: amount, Animation: g.seatsAnimation[seatIdx], IsGain: isGain})
    336 }
    337 
    338 type playerCard struct {
    339 	idx  int
    340 	zIdx int
    341 	name string
    342 }
    343 
    344 type Game struct {
    345 	Players          rwmtx.RWMtx[seatedPlayers]
    346 	seatsAnimation   []bool
    347 	ongoing          *ongoingGame
    348 	db               *database.DkfDB
    349 	roomID           RoomID
    350 	pokerTableID     int64
    351 	tableType        int
    352 	pokerTableMinBet database.PokerChip
    353 	pokerTableIsTest bool
    354 	playersEventCh   chan playerEvent
    355 	dealerSeatIdx    atomic.Int32
    356 	isGameStarted    atomic.Bool
    357 }
    358 
    359 type gameResult struct {
    360 	handScore int32
    361 	players   []*PokerPlayer
    362 }
    363 
    364 func (g *Game) GetLogs() (out []LogEvent) {
    365 	if g.ongoing != nil {
    366 		out = g.ongoing.logEvents.Clone()
    367 	}
    368 	return
    369 }
    370 
    371 func (g *Game) Check(userID database.UserID) {
    372 	g.sendPlayerEvent(playerEvent{UserID: userID, Check: true})
    373 }
    374 
    375 func (g *Game) AllIn(userID database.UserID) {
    376 	g.sendPlayerEvent(playerEvent{UserID: userID, AllIn: true})
    377 }
    378 
    379 func (g *Game) Raise(userID database.UserID) {
    380 	g.sendPlayerEvent(playerEvent{UserID: userID, Raise: true})
    381 }
    382 
    383 func (g *Game) Bet(userID database.UserID, bet database.PokerChip) {
    384 	g.sendPlayerEvent(playerEvent{UserID: userID, Bet: bet})
    385 }
    386 
    387 func (g *Game) Call(userID database.UserID) {
    388 	g.sendPlayerEvent(playerEvent{UserID: userID, Call: true})
    389 }
    390 
    391 func (g *Game) Fold(userID database.UserID) {
    392 	g.sendPlayerEvent(playerEvent{UserID: userID, Fold: true})
    393 }
    394 
    395 func (g *Game) sendUnsitPlayerEvent(userID database.UserID) {
    396 	g.sendPlayerEvent(playerEvent{UserID: userID, Unsit: true})
    397 }
    398 
    399 func (g *Game) sendPlayerEvent(evt playerEvent) {
    400 	select {
    401 	case g.playersEventCh <- evt:
    402 	default:
    403 	}
    404 }
    405 
    406 func (g *ongoingGame) isHeadsUpGame() bool {
    407 	return len(g.players) == 2 // https://en.wikipedia.org/wiki/Heads-up_poker
    408 }
    409 
    410 func (g *ongoingGame) computeWinners() (winner []gameResult) {
    411 	return computeWinners(g.players, g.communityCards)
    412 }
    413 
    414 func computeWinners(players []*PokerPlayer, communityCards []string) (winner []gameResult) {
    415 	countAlive := 0
    416 	var lastAlive *PokerPlayer
    417 	for _, p := range players {
    418 		if !p.folded.Load() {
    419 			countAlive++
    420 			lastAlive = p
    421 		}
    422 	}
    423 	if countAlive == 0 {
    424 		return []gameResult{}
    425 	} else if countAlive == 1 {
    426 		return []gameResult{{-1, []*PokerPlayer{lastAlive}}}
    427 	}
    428 
    429 	m := make(map[int32][]*PokerPlayer)
    430 	for _, p := range players {
    431 		if p.folded.Load() {
    432 			continue
    433 		}
    434 
    435 		var playerCard1, playerCard2 string
    436 		p.cards.RWith(func(pCards []playerCard) {
    437 			playerCard1 = pCards[0].name
    438 			playerCard2 = pCards[1].name
    439 		})
    440 
    441 		if len(communityCards) != 5 {
    442 			return []gameResult{}
    443 		}
    444 		hand := []poker.Card{
    445 			poker.NewCard(cardToPokerCard(communityCards[0])),
    446 			poker.NewCard(cardToPokerCard(communityCards[1])),
    447 			poker.NewCard(cardToPokerCard(communityCards[2])),
    448 			poker.NewCard(cardToPokerCard(communityCards[3])),
    449 			poker.NewCard(cardToPokerCard(communityCards[4])),
    450 			poker.NewCard(cardToPokerCard(playerCard1)),
    451 			poker.NewCard(cardToPokerCard(playerCard2)),
    452 		}
    453 		handEvaluation := poker.Evaluate(hand)
    454 		if _, ok := m[handEvaluation]; !ok {
    455 			m[handEvaluation] = make([]*PokerPlayer, 0)
    456 		}
    457 		m[handEvaluation] = append(m[handEvaluation], p)
    458 	}
    459 
    460 	arr := make([]gameResult, 0)
    461 	for k, v := range m {
    462 		arr = append(arr, gameResult{handScore: k, players: v})
    463 	}
    464 	sortGameResults(arr)
    465 
    466 	return arr
    467 }
    468 
    469 // Sort players by cash remaining (to have all-ins first), then by GameBet.
    470 func sortGameResults(arr []gameResult) {
    471 	for idx := range arr {
    472 		sort.Slice(arr[idx].players, func(i, j int) bool {
    473 			if arr[idx].players[i].getCash() == arr[idx].players[j].getCash() {
    474 				return arr[idx].players[i].gameBet < arr[idx].players[j].gameBet
    475 			}
    476 			return arr[idx].players[i].getCash() < arr[idx].players[j].getCash()
    477 		})
    478 	}
    479 	sort.Slice(arr, func(i, j int) bool { return arr[i].handScore < arr[j].handScore })
    480 }
    481 
    482 func (g *ongoingGame) getDeckStr() string {
    483 	return strings.Join(g.deck, "")
    484 }
    485 
    486 func (g *ongoingGame) GetDeckHash() string {
    487 	return utils.MD5([]byte(g.getDeckStr()))
    488 }
    489 
    490 // Get the player index in ongoingGame.Players from a seat index (index in Game.Players)
    491 // [nil p1 nil nil p2 nil] -> Game.Players
    492 // [p1 p2]                 -> ongoingGame.Players
    493 func (g *ongoingGame) getPlayerBySeatIdx(seatIdx int) (*PokerPlayer, int) {
    494 	for idx, p := range g.players {
    495 		if p.seatIdx == seatIdx {
    496 			return p, idx
    497 		}
    498 	}
    499 	return nil, -1
    500 }
    501 
    502 func (g *ongoingGame) countCanBetPlayers() (nbCanBet int) {
    503 	for _, p := range g.players {
    504 		if p.canBet() {
    505 			nbCanBet++
    506 		}
    507 	}
    508 	return
    509 }
    510 
    511 func (g *ongoingGame) countAlivePlayers() (playerAlive int) {
    512 	return countAlivePlayers(g.players)
    513 }
    514 
    515 func countAlivePlayers(players []*PokerPlayer) (playerAlive int) {
    516 	for _, p := range players {
    517 		if !p.folded.Load() {
    518 			playerAlive++
    519 		}
    520 	}
    521 	return
    522 }
    523 
    524 // IsSeatedUnsafe returns either or not a userID is seated at the table.
    525 // WARN: The caller of this function needs to ensure that g.Players has been Lock/RLock
    526 func (g *Game) IsSeatedUnsafe(userID database.UserID) bool {
    527 	return isSeated(*g.Players.Val(), userID)
    528 }
    529 
    530 func (g *Game) IsSeated(userID database.UserID) bool {
    531 	return isSeated(g.Players.Get(), userID)
    532 }
    533 
    534 func isSeated(players seatedPlayers, userID database.UserID) bool {
    535 	return players.get(userID) != nil
    536 }
    537 
    538 func isRoundSettled(players []*PokerPlayer) bool {
    539 	arr := make([]*PokerPlayer, len(players))
    540 	copy(arr, players)
    541 	sort.Slice(arr, func(i, j int) bool { return arr[i].GetBet() > arr[j].GetBet() })
    542 	b := arr[0].GetBet()
    543 	for _, el := range arr {
    544 		if !el.canBet() {
    545 			continue
    546 		}
    547 		if el.GetBet() != b {
    548 			return false
    549 		}
    550 	}
    551 	return true
    552 }
    553 
    554 func (g *Game) incrDealerIdx() (smallBlindIdx, bigBlindIdx int) {
    555 	ongoing := g.ongoing
    556 	nbPlayers := len(ongoing.players)
    557 	dealerSeatIdx := g.dealerSeatIdx.Load()
    558 	var dealerPlayer *PokerPlayer
    559 	var dealerIdx int
    560 	for {
    561 		dealerSeatIdx = (dealerSeatIdx + 1) % NbPlayers
    562 		if dealerPlayer, dealerIdx = ongoing.getPlayerBySeatIdx(int(dealerSeatIdx)); dealerPlayer != nil {
    563 			break
    564 		}
    565 	}
    566 	g.dealerSeatIdx.Store(dealerSeatIdx)
    567 	startIDx := utils.Ternary(ongoing.isHeadsUpGame(), 0, 1)
    568 	smallBlindIdx = (dealerIdx + startIDx) % nbPlayers
    569 	bigBlindIdx = (dealerIdx + startIDx + 1) % nbPlayers
    570 	return
    571 }
    572 
    573 func (g *Game) Sit(userID database.UserID, username database.Username, pokerReferredBy *database.UserID, pos int) {
    574 	if err := g.Players.WithE(func(gPlayers *seatedPlayers) error {
    575 		pokerTable, err := g.db.GetPokerTableBySlug(g.roomID.String())
    576 		if err != nil {
    577 			return errors.New("failed to get poker table")
    578 		}
    579 		tableAccount, err := g.db.GetPokerTableAccount(userID, pokerTable.ID)
    580 		if err != nil {
    581 			return errors.New("failed to get table account")
    582 		}
    583 		if tableAccount.Amount < pokerTable.MinBet {
    584 			return errors.New(fmt.Sprintf("not enough chips to sit. have: %d, need: %d", tableAccount.Amount, pokerTable.MinBet))
    585 		}
    586 		if isSeated(*gPlayers, userID) {
    587 			return errors.New("player already seated")
    588 		}
    589 		if pos < 0 || pos >= len(*gPlayers) {
    590 			return errors.New("invalid position")
    591 		}
    592 		if (*gPlayers)[pos] != nil {
    593 			return errors.New("seat already taken")
    594 		}
    595 		(*gPlayers)[pos] = &seatedPlayer{
    596 			seatIdx:         pos,
    597 			userID:          userID,
    598 			username:        username,
    599 			cash:            rwmtx.RWMtxUInt64[database.PokerChip]{rwmtx.New(tableAccount.Amount)},
    600 			lastActionTS:    time.Now(),
    601 			pokerReferredBy: pokerReferredBy,
    602 		}
    603 
    604 		PubSub.Pub(g.roomID.Topic(), PokerSeatTakenEvent{})
    605 		g.newLogEvent(fmt.Sprintf("%s sit", username.String()))
    606 
    607 		return nil
    608 	}); err != nil {
    609 		PubSub.Pub(g.roomID.UserTopic(userID), NewErrorMsgEvent(err.Error()))
    610 		return
    611 	}
    612 }
    613 
    614 func (g *Game) UnSit(userID database.UserID) {
    615 	g.Players.With(func(gPlayers *seatedPlayers) {
    616 		if p := gPlayers.get(userID); p != nil {
    617 			g.unSitPlayer(gPlayers, p)
    618 			g.newLogEvent(fmt.Sprintf("%s un-sit", p.username.String()))
    619 		}
    620 	})
    621 }
    622 
    623 func (g *Game) unSitPlayer(gPlayers *seatedPlayers, seatedPlayer *seatedPlayer) {
    624 	ongoing := g.ongoing
    625 	if ongoing != nil {
    626 		if player := ongoing.players.get(seatedPlayer.userID); player != nil {
    627 			g.sendUnsitPlayerEvent(player.userID)
    628 			player.unsit.Store(true)
    629 			player.folded.Store(true)
    630 			player.cards.RWith(func(playerCards []playerCard) {
    631 				for _, card := range playerCards {
    632 					evt := PokerEvent{ID: card.idx, Name: "", ZIdx: card.zIdx, Top: BurnStackY, Left: BurnStackX, Angle: "0deg", Reveal: false}
    633 					PubSub.Pub(g.roomID.Topic(), evt)
    634 					ongoing.events.Append(evt)
    635 				}
    636 			})
    637 		}
    638 	}
    639 	(*gPlayers)[seatedPlayer.seatIdx] = nil
    640 	PubSub.Pub(g.roomID.Topic(), PokerSeatLeftEvent{})
    641 }
    642 
    643 func generateDeck() []string {
    644 	deck := []string{
    645 		"A♠", "2♠", "3♠", "4♠", "5♠", "6♠", "7♠", "8♠", "9♠", "10♠", "J♠", "Q♠", "K♠",
    646 		"A♥", "2♥", "3♥", "4♥", "5♥", "6♥", "7♥", "8♥", "9♥", "10♥", "J♥", "Q♥", "K♥",
    647 		"A♣", "2♣", "3♣", "4♣", "5♣", "6♣", "7♣", "8♣", "9♣", "10♣", "J♣", "Q♣", "K♣",
    648 		"A♦", "2♦", "3♦", "4♦", "5♦", "6♦", "7♦", "8♦", "9♦", "10♦", "J♦", "Q♦", "K♦",
    649 	}
    650 	r := rand.New(utils.NewCryptoRandSource())
    651 	utils.Shuffle1(r, deck)
    652 	return deck
    653 }
    654 
    655 func newOngoing(eligiblePlayers seatedPlayers) *ongoingGame {
    656 	return &ongoingGame{
    657 		deck:          generateDeck(),
    658 		players:       eligiblePlayers.toPokerPlayers(),
    659 		waitTurnEvent: rwmtx.New(PokerWaitTurnEvent{Idx: -1}),
    660 		createdAt:     time.Now(),
    661 	}
    662 }
    663 
    664 func (g *Game) newLogEvent(msg string) {
    665 	ongoing := g.ongoing
    666 	logEvt := LogEvent{Message: msg}
    667 	PubSub.Pub(g.roomID.LogsTopic(), logEvt)
    668 	if ongoing != nil {
    669 		ongoing.logEvents.Append(logEvt)
    670 	}
    671 }
    672 
    673 func showCards(g *Game, seats []Seat) {
    674 	ongoing := g.ongoing
    675 	roomTopic := g.roomID.Topic()
    676 	for _, p := range ongoing.players {
    677 		if !p.folded.Load() {
    678 			var firstCard, secondCard playerCard
    679 			p.cards.RWith(func(pCards []playerCard) {
    680 				firstCard = pCards[0]
    681 				secondCard = pCards[1]
    682 			})
    683 			seatData := seats[p.seatIdx]
    684 			if p.seatIdx == 0 {
    685 				seatData.Left -= 30
    686 			} else if p.seatIdx == 1 {
    687 				seatData.Left -= 31
    688 			} else if p.seatIdx == 2 {
    689 				seatData.Top -= 8
    690 			}
    691 			evt1 := PokerEvent{ID: firstCard.idx, Name: firstCard.name, ZIdx: firstCard.zIdx, Top: seatData.Top, Left: seatData.Left, Reveal: true}
    692 			evt2 := PokerEvent{ID: secondCard.idx, Name: secondCard.name, ZIdx: secondCard.zIdx, Top: seatData.Top, Left: seatData.Left + 53, Reveal: true}
    693 			PubSub.Pub(roomTopic, evt1)
    694 			PubSub.Pub(roomTopic, evt2)
    695 			ongoing.events.Append(evt1, evt2)
    696 		}
    697 	}
    698 }
    699 
    700 func setWaitTurn(g *Game, seatIdx int) {
    701 	evt := PokerWaitTurnEvent{Idx: seatIdx, CreatedAt: time.Now()}
    702 	PubSub.Pub(g.roomID.Topic(), evt)
    703 	g.ongoing.waitTurnEvent.Set(evt)
    704 }
    705 
    706 func setAutoAction(g *Game, roomUserTopic, msg string) {
    707 	evt := AutoActionEvent{Message: msg}
    708 	PubSub.Pub(roomUserTopic, evt)
    709 	g.ongoing.autoActionEvent.Set(evt)
    710 }
    711 
    712 type PlayerAction int
    713 
    714 const (
    715 	NoAction PlayerAction = iota
    716 	FoldAction
    717 	CallAction
    718 	CheckAction
    719 	BetAction
    720 	AllInAction
    721 	RaiseAction
    722 )
    723 
    724 func (a PlayerAction) String() string {
    725 	switch a {
    726 	case NoAction:
    727 		return ""
    728 	case FoldAction:
    729 		return "fold"
    730 	case CallAction:
    731 		return "call"
    732 	case CheckAction:
    733 		return "check"
    734 	case BetAction:
    735 		return "bet"
    736 	case RaiseAction:
    737 		return "raise"
    738 	case AllInAction:
    739 		return "all-in"
    740 	}
    741 	return ""
    742 }
    743 
    744 const (
    745 	doNothing = iota
    746 	breakRoundIsSettledLoop
    747 	continueGetPlayerEventLoop
    748 	breakGetPlayerEventLoop
    749 )
    750 
    751 type autoAction struct {
    752 	action PlayerAction
    753 	evt    playerEvent
    754 }
    755 
    756 func foldPlayer(g *Game, p *PokerPlayer) {
    757 	roomTopic := g.roomID.Topic()
    758 	p.folded.Store(true)
    759 	var firstCard, secondCard playerCard
    760 	p.cards.RWith(func(pCards []playerCard) {
    761 		firstCard = pCards[0]
    762 		secondCard = pCards[1]
    763 	})
    764 	evt1 := PokerEvent{ID: firstCard.idx, Name: "", ZIdx: firstCard.zIdx, Top: BurnStackY, Left: BurnStackX, Angle: "0deg", Reveal: false}
    765 	evt2 := PokerEvent{ID: secondCard.idx, Name: "", ZIdx: secondCard.zIdx, Top: BurnStackY, Left: BurnStackX, Angle: "0deg", Reveal: false}
    766 	PubSub.Pub(roomTopic, evt1)
    767 	PubSub.Pub(roomTopic, evt2)
    768 	g.ongoing.events.Append(evt1, evt2)
    769 }
    770 
    771 func doUnsit(g *Game, p *PokerPlayer, playerAlive *int) int {
    772 	*playerAlive = g.ongoing.countAlivePlayers()
    773 	if *playerAlive == 1 {
    774 		p.countChancesToAction--
    775 		return breakRoundIsSettledLoop
    776 	}
    777 	return continueGetPlayerEventLoop
    778 }
    779 
    780 func doTimeout(g *Game, p *PokerPlayer, playerAlive *int) int {
    781 	pUsername := p.username
    782 	if p.GetBet() < g.ongoing.minBet.Get() {
    783 		foldPlayer(g, p)
    784 		p.status.Set("fold")
    785 		g.newLogEvent(fmt.Sprintf("%s auto fold", pUsername))
    786 
    787 		*playerAlive--
    788 		if *playerAlive == 1 {
    789 			return breakRoundIsSettledLoop
    790 		}
    791 		return doNothing
    792 	}
    793 	p.hasChecked = true
    794 	p.status.Set("check")
    795 	g.newLogEvent(fmt.Sprintf("%s auto check", pUsername))
    796 	return doNothing
    797 }
    798 
    799 func doCheck(g *Game, p *PokerPlayer) int {
    800 	minBet := g.ongoing.minBet.Get()
    801 	if p.GetBet() < minBet {
    802 		msg := fmt.Sprintf("Need to bet %d", minBet-p.GetBet())
    803 		PubSub.Pub(g.roomID.UserTopic(p.userID), NewErrorMsgEvent(msg))
    804 		return continueGetPlayerEventLoop
    805 	}
    806 	p.hasChecked = true
    807 	p.status.Set("check")
    808 	g.newLogEvent(fmt.Sprintf("%s check", p.username))
    809 	return doNothing
    810 }
    811 
    812 func doFold(g *Game, p *PokerPlayer, playerAlive *int) int {
    813 	roomUserTopic := g.roomID.UserTopic(p.userID)
    814 	if p.GetBet() == g.ongoing.minBet.Get() {
    815 		msg := fmt.Sprintf("Cannot fold if there is no bet; check")
    816 		PubSub.Pub(roomUserTopic, NewErrorMsgEvent(msg))
    817 		return doCheck(g, p)
    818 	}
    819 	foldPlayer(g, p)
    820 	p.status.Set("fold")
    821 	g.newLogEvent(fmt.Sprintf("%s fold", p.username))
    822 
    823 	*playerAlive--
    824 	if *playerAlive == 1 {
    825 		PubSub.Pub(roomUserTopic, NewErrorMsgEvent(""))
    826 		return breakRoundIsSettledLoop
    827 	}
    828 	return doNothing
    829 }
    830 
    831 func doCall(g *Game, p *PokerPlayer,
    832 	newlyAllInPlayers *[]*PokerPlayer, lastBetPlayerIdx *int, playerToPlayIdx int) int {
    833 	pUsername := p.username
    834 	bet := utils.MinInt(g.ongoing.minBet.Get()-p.GetBet(), p.getCash())
    835 	if bet == 0 {
    836 		return doCheck(g, p)
    837 	} else if bet == p.cash.Get() {
    838 		return doAllIn(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx)
    839 	} else {
    840 		p.status.Set("call")
    841 		p.doBetAndNotif(g, bet)
    842 		logMsg := fmt.Sprintf("%s call (%d)", pUsername, bet)
    843 		g.newLogEvent(logMsg)
    844 	}
    845 	return doNothing
    846 }
    847 
    848 func doAllIn(g *Game, p *PokerPlayer,
    849 	newlyAllInPlayers *[]*PokerPlayer, lastBetPlayerIdx *int, playerToPlayIdx int) int {
    850 	bet := p.getCash()
    851 	minBet := g.ongoing.minBet.Get()
    852 	if (p.GetBet() + bet) > minBet {
    853 		*lastBetPlayerIdx = playerToPlayIdx
    854 		g.ongoing.minRaise.Set(bet)
    855 		PubSub.Pub(g.roomID.Topic(), PokerMinRaiseUpdatedEvent{MinRaise: bet})
    856 	}
    857 	g.ongoing.minBet.Set(utils.MaxInt(p.GetBet()+bet, minBet))
    858 	p.doBetAndNotif(g, bet)
    859 	logMsg := fmt.Sprintf("%s all-in (%d)", p.username, bet)
    860 	if p.isAllIn() {
    861 		*newlyAllInPlayers = append(*newlyAllInPlayers, p)
    862 	}
    863 	p.status.Set("all-in")
    864 	g.newLogEvent(logMsg)
    865 	return doNothing
    866 }
    867 
    868 func doRaise(g *Game, p *PokerPlayer,
    869 	newlyAllInPlayers *[]*PokerPlayer, lastBetPlayerIdx *int, playerToPlayIdx int) int {
    870 	return doBet(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx, g.ongoing.minRaise.Get())
    871 }
    872 
    873 func doBet(g *Game, p *PokerPlayer,
    874 	newlyAllInPlayers *[]*PokerPlayer, lastBetPlayerIdx *int, playerToPlayIdx int, evtBet database.PokerChip) int {
    875 	roomUserTopic := g.roomID.UserTopic(p.userID)
    876 	minBet := g.ongoing.minBet.Get()
    877 	minRaise := g.ongoing.minRaise.Get()
    878 	playerBet := p.bet.Get()          // Player's chips already on the table
    879 	callDelta := minBet - playerBet   // Chips missing to equalize the minBet
    880 	bet := evtBet + callDelta         // Amount of chips player need to put on the table to make the raise
    881 	playerTotalBet := bet + playerBet // Player's total bet during the betting round
    882 	if bet >= p.cash.Get() {
    883 		return doAllIn(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx)
    884 	}
    885 	betLbl := utils.Ternary(g.IsBet(), "bet", "raise")
    886 	// Ensure the player cannot bet below the table minimum bet (amount of the big blind)
    887 	if evtBet < minRaise {
    888 		msg := fmt.Sprintf("%s (%d) is too low. Must %s at least %d", betLbl, evtBet, betLbl, minRaise)
    889 		PubSub.Pub(roomUserTopic, NewErrorMsgEvent(msg))
    890 		return continueGetPlayerEventLoop
    891 	}
    892 	*lastBetPlayerIdx = playerToPlayIdx
    893 	PubSub.Pub(g.roomID.Topic(), PokerMinRaiseUpdatedEvent{MinRaise: evtBet})
    894 	g.ongoing.minRaise.Set(evtBet)
    895 	g.ongoing.minBet.Set(playerTotalBet)
    896 
    897 	p.doBetAndNotif(g, bet)
    898 	g.newLogEvent(fmt.Sprintf("%s %s %d", p.username, betLbl, g.ongoing.minRaise.Get()))
    899 	if p.hasChecked {
    900 		p.status.Set("check-" + betLbl)
    901 		p.hasChecked = false
    902 	} else {
    903 		p.status.Set(betLbl)
    904 	}
    905 	g.ongoing.hasBet.Set(true)
    906 	return doNothing
    907 }
    908 
    909 func handleAutoActionReceived(g *Game, autoCache map[database.UserID]autoAction, evt playerEvent) int {
    910 	roomUserTopic := g.roomID.UserTopic(evt.UserID)
    911 	autoActionVal := autoCache[evt.UserID]
    912 	if evt.Fold && autoActionVal.action == FoldAction ||
    913 		evt.Call && autoActionVal.action == CallAction ||
    914 		evt.Check && autoActionVal.action == CheckAction {
    915 		delete(autoCache, evt.UserID)
    916 		setAutoAction(g, roomUserTopic, "")
    917 		return continueGetPlayerEventLoop
    918 	}
    919 
    920 	action := evt.getAction()
    921 	if action != NoAction {
    922 		autoCache[evt.UserID] = autoAction{action: action, evt: evt}
    923 		msg := "Will auto "
    924 		if action == FoldAction {
    925 			msg += "fold/check"
    926 		} else {
    927 			msg += action.String()
    928 		}
    929 		if evt.Bet > 0 {
    930 			msg += fmt.Sprintf(" %d", evt.Bet.Raw())
    931 		}
    932 		setAutoAction(g, roomUserTopic, msg)
    933 	}
    934 	return continueGetPlayerEventLoop
    935 }
    936 
    937 func applyAutoAction(g *Game, p *PokerPlayer,
    938 	newlyAllInPlayers *[]*PokerPlayer,
    939 	lastBetPlayerIdx, playerAlive *int, playerToPlayIdx int, autoAction autoAction,
    940 	autoCache map[database.UserID]autoAction) (actionResult int) {
    941 
    942 	pUserID := p.userID
    943 	roomUserTopic := g.roomID.UserTopic(pUserID)
    944 	if autoAction.action > NoAction {
    945 		time.Sleep(500 * time.Millisecond)
    946 		actionResult = handlePlayerActionEvent(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerAlive, playerToPlayIdx, autoAction.evt)
    947 	}
    948 	delete(autoCache, pUserID)
    949 	setAutoAction(g, roomUserTopic, "")
    950 	return actionResult
    951 }
    952 
    953 func handlePlayerActionEvent(g *Game, p *PokerPlayer,
    954 	newlyAllInPlayers *[]*PokerPlayer,
    955 	lastBetPlayerIdx, playerAlive *int, playerToPlayIdx int, evt playerEvent) (actionResult int) {
    956 
    957 	p.lastActionTS = time.Now()
    958 	if evt.Fold {
    959 		actionResult = doFold(g, p, playerAlive)
    960 	} else if evt.Check {
    961 		actionResult = doCheck(g, p)
    962 	} else if evt.Call {
    963 		actionResult = doCall(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx)
    964 	} else if evt.AllIn {
    965 		actionResult = doAllIn(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx)
    966 	} else if evt.Raise {
    967 		actionResult = doRaise(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx)
    968 	} else if evt.Bet > 0 {
    969 		actionResult = doBet(g, p, newlyAllInPlayers, lastBetPlayerIdx, playerToPlayIdx, evt.Bet)
    970 	} else {
    971 		actionResult = continueGetPlayerEventLoop
    972 	}
    973 	return actionResult
    974 }
    975 
    976 // Return either or not the game ended because only 1 player left playing (or none)
    977 func execBettingRound(g *Game, skip int, minBet database.PokerChip) bool {
    978 	roomID := g.roomID
    979 	roomTopic := roomID.Topic()
    980 	gPokerTableMinBet := g.pokerTableMinBet
    981 	g.ongoing.minBet.Set(minBet)
    982 	g.ongoing.minRaise.Set(gPokerTableMinBet)
    983 	PubSub.Pub(roomTopic, PokerMinRaiseUpdatedEvent{MinRaise: gPokerTableMinBet})
    984 	db := g.db
    985 	ongoing := g.ongoing
    986 	_, dealerIdx := ongoing.getPlayerBySeatIdx(int(g.dealerSeatIdx.Load()))
    987 	playerToPlayIdx := (dealerIdx + skip) % len(ongoing.players)
    988 	lastBetPlayerIdx := -1
    989 	newlyAllInPlayers := make([]*PokerPlayer, 0)
    990 	autoCache := make(map[database.UserID]autoAction)
    991 
    992 	for _, p := range ongoing.players {
    993 		p.hasChecked = false
    994 		if p.canBet() {
    995 			p.status.Set("")
    996 		}
    997 	}
    998 	PubSub.Pub(roomTopic, RedrawSeatsEvent{})
    999 
   1000 	playerAlive := ongoing.countAlivePlayers()
   1001 
   1002 	// Avoid asking for actions if only 1 player can do so (because others are all-in)
   1003 	nbCanBet := ongoing.countCanBetPlayers()
   1004 	if nbCanBet == 0 || nbCanBet == 1 {
   1005 		goto RoundIsSettled
   1006 	}
   1007 
   1008 	// TODO: implement maximum re-raise
   1009 
   1010 RoundIsSettledLoop:
   1011 	for { // Repeat until the round is settled (all players have equals bet or fold or all-in)
   1012 	AllPlayersLoop:
   1013 		for { // Repeat until all players have played
   1014 			playerToPlayIdx = (playerToPlayIdx + 1) % len(ongoing.players)
   1015 			p := ongoing.players[playerToPlayIdx]
   1016 			g.ongoing.playerToPlay.Set(p.userID)
   1017 			p.countChancesToAction++
   1018 			pUserID := p.userID
   1019 			roomUserTopic := roomID.UserTopic(pUserID)
   1020 
   1021 			if playerToPlayIdx == lastBetPlayerIdx {
   1022 				break AllPlayersLoop
   1023 			}
   1024 			lastBetPlayerIdx = utils.Ternary(lastBetPlayerIdx == -1, playerToPlayIdx, lastBetPlayerIdx)
   1025 			if !p.canBet() {
   1026 				continue AllPlayersLoop
   1027 			}
   1028 
   1029 			minBet = g.ongoing.minBet.Get()
   1030 
   1031 			PubSub.Pub(roomUserTopic, RefreshButtonsEvent{})
   1032 
   1033 			setWaitTurn(g, p.seatIdx)
   1034 			PubSub.Pub(roomUserTopic, PokerYourTurnEvent{})
   1035 
   1036 			// Maximum time allowed for the player to send his action
   1037 			waitCh := time.After(MaxUserCountdown * time.Second)
   1038 		GetPlayerEventLoop:
   1039 			for { // Repeat until we get an event from the player we're interested in
   1040 				var evt playerEvent
   1041 				actionResult := doNothing
   1042 				// Check for pre-selected action
   1043 				if autoActionVal, ok := autoCache[pUserID]; ok {
   1044 					actionResult = applyAutoAction(g, p, &newlyAllInPlayers,
   1045 						&lastBetPlayerIdx, &playerAlive, playerToPlayIdx, autoActionVal, autoCache)
   1046 					goto checkActionResult
   1047 				}
   1048 				select {
   1049 				case evt = <-g.playersEventCh:
   1050 				case <-waitCh: // Waited too long, either auto-check or auto-fold
   1051 					actionResult = doTimeout(g, p, &playerAlive)
   1052 					goto checkActionResult
   1053 				}
   1054 				if evt.Unsit {
   1055 					actionResult = doUnsit(g, p, &playerAlive)
   1056 					goto checkActionResult
   1057 				}
   1058 				if evt.UserID != pUserID {
   1059 					actionResult = handleAutoActionReceived(g, autoCache, evt)
   1060 					goto checkActionResult
   1061 				}
   1062 				actionResult = handlePlayerActionEvent(g, p, &newlyAllInPlayers,
   1063 					&lastBetPlayerIdx, &playerAlive, playerToPlayIdx, evt)
   1064 				goto checkActionResult
   1065 
   1066 			checkActionResult:
   1067 				switch actionResult {
   1068 				case doNothing:
   1069 				case continueGetPlayerEventLoop:
   1070 					continue GetPlayerEventLoop
   1071 				case breakGetPlayerEventLoop:
   1072 					break GetPlayerEventLoop
   1073 				case breakRoundIsSettledLoop:
   1074 					break RoundIsSettledLoop
   1075 				}
   1076 				PubSub.Pub(roomUserTopic, NewErrorMsgEvent(""))
   1077 				PubSub.Pub(roomTopic, RedrawSeatsEvent{})
   1078 				break GetPlayerEventLoop
   1079 			} // End of repeat until we get an event from the player we're interested in
   1080 		} // End of repeat until all players have played
   1081 		// All settle when all players have the same bet amount
   1082 		if isRoundSettled(ongoing.players) {
   1083 			break RoundIsSettledLoop
   1084 		}
   1085 	} // End of repeat until the round is settled (all players have equals bet or fold or all-in)
   1086 
   1087 RoundIsSettled:
   1088 
   1089 	setAutoAction(g, roomTopic, "")
   1090 	PubSub.Pub(roomTopic, NewErrorMsgEvent(""))
   1091 	g.newLogEvent(fmt.Sprintf("--"))
   1092 	setWaitTurn(g, -1)
   1093 
   1094 	time.Sleep(animationTime)
   1095 
   1096 	mainPot := ongoing.mainPot.Get()
   1097 
   1098 	// Calculate what is the max gain all-in players can make
   1099 	computeAllInMaxGain(ongoing, newlyAllInPlayers, mainPot)
   1100 
   1101 	// Always refund the difference between the first-biggest bet and the second-biggest bet.
   1102 	// We refund the "uncalled bet" so that it does not go in the main pot and does not get raked.
   1103 	// Also, if a player goes all-in and a fraction of his bet is not matched, it will be refunded.
   1104 	refundUncalledBet(db, ongoing, g.pokerTableID, roomTopic)
   1105 
   1106 	// Transfer players bets into the main pot
   1107 	mainPot += resetPlayersBet(ongoing)
   1108 
   1109 	PubSub.Pub(roomTopic, PokerMainPotUpdatedEvent{MainPot: mainPot})
   1110 	ongoing.mainPot.Set(mainPot)
   1111 	g.ongoing.hasBet.Set(false)
   1112 
   1113 	return playerAlive <= 1
   1114 }
   1115 
   1116 // Reset all players bets, and return the sum of it
   1117 func resetPlayersBet(ongoing *ongoingGame) (sum database.PokerChip) {
   1118 	for _, p := range ongoing.players {
   1119 		sum += p.resetBet()
   1120 	}
   1121 	return
   1122 }
   1123 
   1124 func refundUncalledBet(db *database.DkfDB, ongoing *ongoingGame, pokerTableID int64, roomTopic string) {
   1125 	lenPlayers := len(ongoing.players)
   1126 	if lenPlayers < 2 {
   1127 		return
   1128 	}
   1129 	newArray := make([]*PokerPlayer, lenPlayers)
   1130 	copy(newArray, ongoing.players)
   1131 	sort.Slice(newArray, func(i, j int) bool { return newArray[i].GetBet() > newArray[j].GetBet() })
   1132 	firstPlayer := newArray[0]
   1133 	secondPlayer := newArray[1]
   1134 	diff := firstPlayer.GetBet() - secondPlayer.GetBet()
   1135 	if diff > 0 {
   1136 		firstPlayer.refundPartialBet(db, pokerTableID, diff)
   1137 		PubSub.Pub(roomTopic, RedrawSeatsEvent{})
   1138 		time.Sleep(animationTime)
   1139 	}
   1140 }
   1141 
   1142 type Seat struct {
   1143 	Top   int
   1144 	Left  int
   1145 	Angle string
   1146 	Top2  int
   1147 	Left2 int
   1148 }
   1149 
   1150 // Positions of the dealer token for each seats
   1151 var dealerTokenPos = [][]int{
   1152 	{142, 714},
   1153 	{261, 732},
   1154 	{384, 607},
   1155 	{369, 379},
   1156 	{367, 190},
   1157 	{363, 123},
   1158 }
   1159 
   1160 func burnCard(g *Game, idx, burnIdx *int) {
   1161 	ongoing := g.ongoing
   1162 	*idx++
   1163 	evt := PokerEvent{
   1164 		ID:   *idx,
   1165 		ID1:  *idx + 1,
   1166 		Name: "",
   1167 		ZIdx: *idx + 53,
   1168 		Top:  BurnStackY + (*burnIdx * 2),
   1169 		Left: BurnStackX + (*burnIdx * 4),
   1170 	}
   1171 	PubSub.Pub(g.roomID.Topic(), evt)
   1172 	ongoing.events.Append(evt)
   1173 	*burnIdx++
   1174 }
   1175 
   1176 func dealCard(g *Game, idx *int, dealCardIdx int) {
   1177 	ongoing := g.ongoing
   1178 	card := ongoing.deck[*idx]
   1179 	*idx++
   1180 	evt := PokerEvent{
   1181 		ID:     *idx,
   1182 		ID1:    *idx + 1,
   1183 		Name:   card,
   1184 		ZIdx:   *idx + 53,
   1185 		Top:    DealY,
   1186 		Left:   DealX + (dealCardIdx * DealSpacing),
   1187 		Reveal: true,
   1188 	}
   1189 	PubSub.Pub(g.roomID.Topic(), evt)
   1190 	ongoing.events.Append(evt)
   1191 	ongoing.communityCards = append(ongoing.communityCards, card)
   1192 }
   1193 
   1194 func dealPlayersCards(g *Game, seats []Seat, idx *int) {
   1195 	roomID := g.roomID
   1196 	ongoing := g.ongoing
   1197 	roomTopic := roomID.Topic()
   1198 	var card string
   1199 	for cardIdx := 1; cardIdx <= NbCardsPerPlayer; cardIdx++ {
   1200 		for _, p := range ongoing.players {
   1201 			pUserID := p.userID
   1202 			if !p.canBet() {
   1203 				continue
   1204 			}
   1205 			if p.unsit.Load() {
   1206 				continue
   1207 			}
   1208 			roomUserTopic := roomID.UserTopic(pUserID)
   1209 			seatData := seats[p.seatIdx]
   1210 			time.Sleep(animationTime)
   1211 			card = ongoing.deck[*idx]
   1212 			*idx++
   1213 			left := seatData.Left
   1214 			top := seatData.Top
   1215 			if cardIdx == 2 {
   1216 				left = seatData.Left2
   1217 				top = seatData.Top2
   1218 			}
   1219 
   1220 			seatData1 := seats[p.seatIdx]
   1221 			if p.seatIdx == 0 {
   1222 				seatData1.Left -= 30
   1223 			} else if p.seatIdx == 1 {
   1224 				seatData1.Left -= 31
   1225 			} else if p.seatIdx == 2 {
   1226 				seatData1.Top -= 8
   1227 			}
   1228 			if cardIdx == 2 {
   1229 				seatData1.Left += 53
   1230 			}
   1231 
   1232 			evt := PokerEvent{ID: *idx, ID1: *idx + 1, Name: "", ZIdx: *idx + 104, Top: top, Left: left, Angle: seatData.Angle}
   1233 			evt1 := PokerEvent{ID: *idx, ID1: *idx + 1, Name: card, ZIdx: *idx + 104, Top: seatData1.Top, Left: seatData1.Left, Reveal: true, UserID: pUserID}
   1234 
   1235 			PubSub.Pub(roomTopic, evt)
   1236 			PubSub.Pub(roomUserTopic, evt1)
   1237 
   1238 			p.cards.Append(playerCard{idx: *idx, zIdx: *idx + 104, name: card})
   1239 
   1240 			ongoing.events.Append(evt, evt1)
   1241 		}
   1242 	}
   1243 }
   1244 
   1245 func computeAllInMaxGain(ongoing *ongoingGame, newlyAllInPlayers []*PokerPlayer, mainPot database.PokerChip) {
   1246 	for _, p := range newlyAllInPlayers {
   1247 		maxGain := mainPot
   1248 		for _, op := range ongoing.players {
   1249 			maxGain += utils.MinInt(op.GetBet(), p.GetBet())
   1250 		}
   1251 		p.allInMaxGain = maxGain
   1252 	}
   1253 }
   1254 
   1255 func dealerThread(g *Game, eligiblePlayers seatedPlayers) {
   1256 	eligiblePlayers.resetStatuses()
   1257 	g.ongoing = newOngoing(eligiblePlayers)
   1258 
   1259 	roomID := g.roomID
   1260 	roomTopic := roomID.Topic()
   1261 	bigBlindBet := g.pokerTableMinBet
   1262 	collectRake := false
   1263 	ongoing := g.ongoing
   1264 	isHeadsUpGame := ongoing.isHeadsUpGame()
   1265 
   1266 	seats := []Seat{
   1267 		{Top: 55, Left: 610, Top2: 55 + 5, Left2: 610 + 5, Angle: "-95deg"},
   1268 		{Top: 175, Left: 620, Top2: 175 + 5, Left2: 620 + 3, Angle: "-80deg"},
   1269 		{Top: 290, Left: 580, Top2: 290 + 5, Left2: 580 + 1, Angle: "-50deg"},
   1270 		{Top: 310, Left: 430, Top2: 310 + 5, Left2: 430 + 1, Angle: "0deg"},
   1271 		{Top: 315, Left: 240, Top2: 315 + 5, Left2: 240 + 1, Angle: "0deg"},
   1272 		{Top: 270, Left: 70, Top2: 270 + 5, Left2: 70 + 1, Angle: "10deg"},
   1273 	}
   1274 
   1275 	idx := 0
   1276 	burnIdx := 0
   1277 
   1278 	sbIdx, bbIdx := g.incrDealerIdx()
   1279 
   1280 	PubSub.Pub(roomTopic, GameStartedEvent{DealerSeatIdx: int(g.dealerSeatIdx.Load())})
   1281 	g.newLogEvent(fmt.Sprintf("-- New game --"))
   1282 
   1283 	applySmallBlindBet(g, bigBlindBet, sbIdx)
   1284 	time.Sleep(animationTime)
   1285 
   1286 	applyBigBlindBet(g, bigBlindBet, bbIdx)
   1287 	time.Sleep(animationTime)
   1288 	g.ongoing.hasBet.Set(true)
   1289 	g.ongoing.minRaise.Set(bigBlindBet)
   1290 
   1291 	// Deal players cards
   1292 	dealPlayersCards(g, seats, &idx)
   1293 
   1294 	PubSub.Pub(roomTopic, RefreshButtonsEvent{})
   1295 
   1296 	// Wait for players to bet/call/check/fold...
   1297 	time.Sleep(animationTime)
   1298 	skip := utils.Ternary(isHeadsUpGame, 1, 2)
   1299 	if execBettingRound(g, skip, bigBlindBet) {
   1300 		goto END
   1301 	}
   1302 
   1303 	// Flop (3 first cards)
   1304 	time.Sleep(animationTime)
   1305 	burnCard(g, &idx, &burnIdx)
   1306 	for i := 1; i <= 3; i++ {
   1307 		time.Sleep(animationTime)
   1308 		dealCard(g, &idx, i)
   1309 	}
   1310 
   1311 	// No flop, no drop
   1312 	if g.tableType == TableTypeRake {
   1313 		collectRake = true
   1314 	}
   1315 
   1316 	skip = utils.Ternary(isHeadsUpGame, 1, 0)
   1317 
   1318 	// Wait for players to bet/call/check/fold...
   1319 	time.Sleep(animationTime)
   1320 	if execBettingRound(g, skip, 0) {
   1321 		goto END
   1322 	}
   1323 
   1324 	// Turn (4th card)
   1325 	time.Sleep(animationTime)
   1326 	burnCard(g, &idx, &burnIdx)
   1327 	time.Sleep(animationTime)
   1328 	dealCard(g, &idx, 4)
   1329 
   1330 	// Wait for players to bet/call/check/fold...
   1331 	time.Sleep(animationTime)
   1332 	if execBettingRound(g, skip, 0) {
   1333 		goto END
   1334 	}
   1335 
   1336 	// River (5th card)
   1337 	time.Sleep(animationTime)
   1338 	burnCard(g, &idx, &burnIdx)
   1339 	time.Sleep(animationTime)
   1340 	dealCard(g, &idx, 5)
   1341 
   1342 	// Wait for players to bet/call/check/fold...
   1343 	time.Sleep(animationTime)
   1344 	if execBettingRound(g, skip, 0) {
   1345 		goto END
   1346 	}
   1347 
   1348 	// Show cards
   1349 	showCards(g, seats)
   1350 	g.newLogEvent(g.ongoing.gameStr())
   1351 
   1352 END:
   1353 
   1354 	winners := ongoing.computeWinners()
   1355 	mainPotOrig := ongoing.mainPot.Get()
   1356 	mainPot, rake := computeRake(g.ongoing.players, bigBlindBet, mainPotOrig, collectRake)
   1357 	playersGain := processPot(winners, mainPot)
   1358 	winnersStr, winnerHand := applyGains(g, playersGain, mainPotOrig, rake)
   1359 
   1360 	ongoing.mainPot.Set(0)
   1361 
   1362 	PubSub.Pub(roomTopic, GameIsDoneEvent{Winner: winnersStr, WinnerHand: winnerHand})
   1363 	g.newLogEvent(fmt.Sprintf("-- Game ended --"))
   1364 
   1365 	// Wait a minimum of X seconds before allowing a new game
   1366 	time.Sleep(MinTimeAfterGame * time.Second)
   1367 
   1368 	// Auto unsit inactive players
   1369 	autoUnsitInactivePlayers(g)
   1370 
   1371 	PubSub.Pub(roomTopic, GameIsOverEvent{})
   1372 	g.isGameStarted.Store(false)
   1373 }
   1374 
   1375 func (g *ongoingGame) gameStr() string {
   1376 	out := fmt.Sprintf("%s", g.communityCards)
   1377 	for _, p := range g.players {
   1378 		if !p.folded.Load() {
   1379 			out += fmt.Sprintf(" | @%s", p.username)
   1380 			p.cards.RWith(func(pCards []playerCard) {
   1381 				out += " " + pCards[0].name
   1382 				out += " " + pCards[1].name
   1383 			})
   1384 		}
   1385 	}
   1386 	return out
   1387 }
   1388 
   1389 func computeRake(players []*PokerPlayer, pokerTableMinBet, mainPotIn database.PokerChip, collectRake bool) (mainPotOut, rake database.PokerChip) {
   1390 	if !collectRake {
   1391 		return mainPotIn, 0
   1392 	}
   1393 	rake = calculateRake(mainPotIn, pokerTableMinBet, len(players))
   1394 	for _, p := range players {
   1395 		pctOfPot := float64(p.gameBet) / float64(mainPotIn)
   1396 		p.rakePaid = pctOfPot * float64(rake)
   1397 	}
   1398 	mainPotOut = mainPotIn - rake
   1399 	return mainPotOut, rake
   1400 }
   1401 
   1402 func applySmallBlindBet(g *Game, bigBlindBet database.PokerChip, sbIdx int) {
   1403 	applyBlindBet(g, sbIdx, bigBlindBet/2, "small blind")
   1404 }
   1405 
   1406 func applyBigBlindBet(g *Game, bigBlindBet database.PokerChip, bbIdx int) {
   1407 	applyBlindBet(g, bbIdx, bigBlindBet, "big blind")
   1408 }
   1409 
   1410 func applyBlindBet(g *Game, playerIdx int, bet database.PokerChip, name string) {
   1411 	p := g.ongoing.players[playerIdx]
   1412 	p.doBetAndNotif(g, bet)
   1413 	g.newLogEvent(fmt.Sprintf("%s %s %d", p.username, name, bet))
   1414 }
   1415 
   1416 func autoUnsitInactivePlayers(g *Game) {
   1417 	ongoing := g.ongoing
   1418 	pokerTableMinBet := g.pokerTableMinBet
   1419 	g.Players.With(func(gPlayers *seatedPlayers) {
   1420 		for _, p := range *gPlayers {
   1421 			if playerShouldBeBooted(p, ongoing, pokerTableMinBet) {
   1422 				g.unSitPlayer(gPlayers, p)
   1423 				g.newLogEvent(fmt.Sprintf("%s auto un-sit", p.username))
   1424 			}
   1425 		}
   1426 	})
   1427 }
   1428 
   1429 // Returns either or not a seated player should be booted out of the table.
   1430 func playerShouldBeBooted(p *seatedPlayer, ongoing *ongoingGame, pokerTableMinBet database.PokerChip) (playerShallBeBooted bool) {
   1431 	if p == nil {
   1432 		return false
   1433 	}
   1434 	pIsEligible := p.isEligible(pokerTableMinBet)
   1435 	if !pIsEligible {
   1436 		return true
   1437 	}
   1438 	if p.lastActionTS.Before(ongoing.createdAt) {
   1439 		// If the player was playing the game, must be booted if he had the chance to make actions and did not.
   1440 		// If the player was not playing the game, must be booted if he's not eligible to play the next one.
   1441 		op := ongoing.players.get(p.userID)
   1442 		playerShallBeBooted = (op != nil && op.countChancesToAction > 0) ||
   1443 			(op == nil && !pIsEligible)
   1444 	}
   1445 	return playerShallBeBooted
   1446 }
   1447 
   1448 const (
   1449 	TableTypeRake = iota
   1450 	TableType2
   1451 )
   1452 
   1453 // Increase users rake-back and casino rake for paying tables.
   1454 func applyRake(g *Game, tx *database.DkfDB, rake database.PokerChip) {
   1455 	rakeBackMap := make(map[database.UserID]database.PokerChip)
   1456 	for _, p := range g.ongoing.players {
   1457 		if p.pokerReferredBy != nil {
   1458 			rakeBack := database.PokerChip(math.RoundToEven(RakeBackPct * p.rakePaid))
   1459 			rakeBackMap[*p.pokerReferredBy] += rakeBack
   1460 		}
   1461 	}
   1462 	casinoRakeBack := database.PokerChip(0)
   1463 	for userID, totalRakeBack := range rakeBackMap {
   1464 		casinoRakeBack += totalRakeBack
   1465 		rake -= totalRakeBack
   1466 		if err := tx.IncrUserRakeBack(userID, totalRakeBack); err != nil {
   1467 			logrus.Error(err)
   1468 			casinoRakeBack -= totalRakeBack
   1469 			rake += totalRakeBack
   1470 		}
   1471 	}
   1472 	_ = tx.IncrPokerCasinoRake(rake, casinoRakeBack)
   1473 }
   1474 
   1475 func applyGains(g *Game, playersGain []PlayerGain, mainPot, rake database.PokerChip) (winnersStr, winnerHand string) {
   1476 	ongoing := g.ongoing
   1477 	pokerTableID := g.pokerTableID
   1478 	nbPlayersGain := len(playersGain)
   1479 	g.db.With(func(tx *database.DkfDB) {
   1480 		if nbPlayersGain >= 1 {
   1481 			winnerHand = utils.Ternary(nbPlayersGain == 1, playersGain[0].HandStr, "Split pot")
   1482 
   1483 			if g.tableType == TableTypeRake {
   1484 				if !g.pokerTableIsTest {
   1485 					applyRake(g, tx, rake)
   1486 				}
   1487 				g.newLogEvent(fmt.Sprintf("Rake %d (%.2f%%)", rake, (float64(rake)/float64(mainPot))*100))
   1488 			}
   1489 
   1490 			for _, el := range playersGain {
   1491 				g.newLogEvent(fmt.Sprintf("Winner #%d: %s %s -> %d", el.Group, el.Player.username, el.HandStr, el.Gain))
   1492 				winnersStr += el.Player.username.String() + " "
   1493 				el.Player.gain(tx, pokerTableID, el.Gain)
   1494 				pubCashBonus(g, el.Player.seatIdx, el.Gain, true)
   1495 			}
   1496 			for _, op := range ongoing.players {
   1497 				op.gain(tx, pokerTableID, 0)
   1498 			}
   1499 
   1500 		} else if nbPlayersGain == 0 {
   1501 			// No winners, refund bets
   1502 			for _, op := range ongoing.players {
   1503 				op.refundBet(tx, pokerTableID)
   1504 			}
   1505 		}
   1506 	})
   1507 	return
   1508 }
   1509 
   1510 type PlayerGain struct {
   1511 	Player  *PokerPlayer
   1512 	Gain    database.PokerChip
   1513 	Group   int
   1514 	HandStr string
   1515 }
   1516 
   1517 func calculateRake(mainPot, pokerTableMinBet database.PokerChip, nbPlayers int) (rake database.PokerChip) {
   1518 	// https://www.pokerstars.com/poker/room/rake
   1519 	// BB: pct, 2P, 3-4P, 5+P
   1520 	rakeTable := map[database.PokerChip][]float64{
   1521 		3:    {0.035, 178, 178, 178},
   1522 		20:   {0.0415, 297, 297, 595},
   1523 		200:  {0.05, 446, 446, 1190},
   1524 		1000: {0.05, 722, 722, 1589},
   1525 		2000: {0.05, 892, 892, 1785},
   1526 	}
   1527 	maxRake := pokerTableMinBet * 15
   1528 	rakePct := 0.045
   1529 	if val, ok := rakeTable[pokerTableMinBet]; ok {
   1530 		rakePct = val[0]
   1531 		if nbPlayers == 2 {
   1532 			maxRake = database.PokerChip(val[1])
   1533 		} else if nbPlayers == 3 || nbPlayers == 4 {
   1534 			maxRake = database.PokerChip(val[2])
   1535 		} else if nbPlayers >= 5 {
   1536 			maxRake = database.PokerChip(val[3])
   1537 		}
   1538 	}
   1539 	rake = database.PokerChip(math.RoundToEven(rakePct * float64(mainPot)))
   1540 	rake = utils.MinInt(rake, maxRake) // Max rake
   1541 	return rake
   1542 }
   1543 
   1544 func processPot(winners []gameResult, mainPot database.PokerChip) (res []PlayerGain) {
   1545 	if len(winners) == 0 {
   1546 		logrus.Error("winners has len 0")
   1547 		return
   1548 	}
   1549 
   1550 	isOnlyPlayerAlive := len(winners) == 1 && len(winners[0].players) == 1
   1551 	for groupIdx, group := range winners {
   1552 		if mainPot == 0 {
   1553 			break
   1554 		}
   1555 		groupPlayers := group.players
   1556 		groupPlayersLen := len(groupPlayers)
   1557 		handStr := "Only player alive"
   1558 		if !isOnlyPlayerAlive {
   1559 			handStr = poker.RankString(group.handScore)
   1560 		}
   1561 		allInCount := 0
   1562 		calcExpectedSplit := func() database.PokerChip {
   1563 			return mainPot / utils.MaxInt(database.PokerChip(groupPlayersLen-allInCount), 1)
   1564 		}
   1565 		expectedSplit := calcExpectedSplit()
   1566 		for _, p := range groupPlayers {
   1567 			piece := utils.MinInt(p.maxGain(mainPot), expectedSplit)
   1568 			res = append(res, PlayerGain{Player: p, Gain: piece, Group: groupIdx, HandStr: handStr})
   1569 			mainPot -= piece
   1570 			if p.isAllIn() {
   1571 				allInCount++
   1572 				expectedSplit = calcExpectedSplit()
   1573 			}
   1574 		}
   1575 		// If everyone in the group was all-in, we need to evaluate the next group as well
   1576 		if allInCount == groupPlayersLen {
   1577 			continue
   1578 		}
   1579 		break
   1580 	}
   1581 
   1582 	// If any remaining "odd chip(s)" distribute them to players.
   1583 	// TODO: these chips should be given to the stronger hand first
   1584 	idx := 0
   1585 	for mainPot > 0 {
   1586 		res[idx].Gain++
   1587 		mainPot--
   1588 		idx = (idx + 1) % len(res)
   1589 	}
   1590 
   1591 	return
   1592 }
   1593 
   1594 func cardToPokerCard(name string) string {
   1595 	r := strings.NewReplacer("♠", "s", "♥", "h", "♣", "c", "♦", "d", "10", "T")
   1596 	return r.Replace(name)
   1597 }
   1598 
   1599 func (g *Game) OngoingPlayer(userID database.UserID) *PokerPlayer {
   1600 	if g.ongoing != nil {
   1601 		return g.ongoing.players.get(userID)
   1602 	}
   1603 	return nil
   1604 }
   1605 
   1606 func (g *Game) Deal(userID database.UserID) {
   1607 	roomTopic := g.roomID.Topic()
   1608 	roomUserTopic := g.roomID.UserTopic(userID)
   1609 	eligiblePlayers := g.getEligibles()
   1610 	if !g.IsSeated(userID) {
   1611 		PubSub.Pub(roomUserTopic, NewErrorMsgEvent("you need to be seated"))
   1612 		return
   1613 	}
   1614 	if len(eligiblePlayers) < 2 {
   1615 		PubSub.Pub(roomUserTopic, NewErrorMsgEvent("need at least 2 players"))
   1616 		return
   1617 	}
   1618 	if !g.isGameStarted.CompareAndSwap(false, true) {
   1619 		PubSub.Pub(roomUserTopic, NewErrorMsgEvent("game already ongoing"))
   1620 		return
   1621 	}
   1622 
   1623 	PubSub.Pub(roomUserTopic, NewErrorMsgEvent(""))
   1624 	PubSub.Pub(roomTopic, ResetCardsEvent{})
   1625 	time.Sleep(animationTime)
   1626 
   1627 	go dealerThread(g, eligiblePlayers)
   1628 }
   1629 
   1630 func (g *Game) CountSeated() (count int) {
   1631 	g.Players.RWith(func(gPlayers seatedPlayers) {
   1632 		for _, p := range gPlayers {
   1633 			if p != nil {
   1634 				count++
   1635 			}
   1636 		}
   1637 	})
   1638 	return
   1639 }
   1640 
   1641 var PubSub = pubsub.NewPubSub[any]()
   1642 
   1643 func Refund(db *database.DkfDB) {
   1644 	accounts, _ := db.GetPositivePokerTableAccounts()
   1645 	db.With(func(tx *database.DkfDB) {
   1646 		for _, account := range accounts {
   1647 			_ = tx.PokerTableAccountRefundPartialBet(account.UserID, account.PokerTableID, account.AmountBet)
   1648 		}
   1649 	})
   1650 }
   1651 
   1652 type RoomID string
   1653 
   1654 func (r RoomID) String() string    { return string(r) }
   1655 func (r RoomID) Topic() string     { return "room_" + string(r) }
   1656 func (r RoomID) LogsTopic() string { return r.Topic() + "_logs" }
   1657 func (r RoomID) UserTopic(userID database.UserID) string {
   1658 	return r.Topic() + "_" + userID.String()
   1659 }
   1660 
   1661 func isHeartOrDiamond(name string) bool {
   1662 	return strings.Contains(name, "♥") ||
   1663 		strings.Contains(name, "♦")
   1664 }
   1665 
   1666 func colorForCard(name string) string {
   1667 	return utils.Ternary(isHeartOrDiamond(name), "red", "black")
   1668 }
   1669 
   1670 func buildDealerTokenHtml(g *Game) (html string) {
   1671 	html += `<div id="dealerToken"><div class="inner"></div></div>`
   1672 	if g.ongoing != nil {
   1673 		pos := dealerTokenPos[g.dealerSeatIdx.Load()]
   1674 		top := itoa(pos[0])
   1675 		left := itoa(pos[1])
   1676 		html += fmt.Sprintf(`<style>#dealerToken { top: %spx; left: %spx; }</style>`, top, left)
   1677 	}
   1678 	return
   1679 }
   1680 
   1681 func BuildPayloadHtml(g *Game, authUser *database.User, payload any) (html string) {
   1682 	switch evt := payload.(type) {
   1683 	case GameStartedEvent:
   1684 		html += drawGameStartedEvent(evt, authUser)
   1685 	case GameIsDoneEvent:
   1686 		html += drawGameIsDoneHtml(g, evt)
   1687 	case GameIsOverEvent:
   1688 		html += drawGameIsOverHtml(g)
   1689 	case PlayerBetEvent:
   1690 		html += drawPlayerBetEvent(evt)
   1691 		html += drawSeatsStyle(authUser, g)
   1692 	case ErrorMsgEvent:
   1693 		html += drawErrorMsgEvent(evt)
   1694 	case AutoActionEvent:
   1695 		html += drawAutoActionMsgEvent(evt)
   1696 	case PlayerFoldEvent:
   1697 		html += drawPlayerFoldEvent(evt)
   1698 	case ResetCardsEvent:
   1699 		html += drawResetCardsEvent()
   1700 	case CashBonusEvent:
   1701 		html += drawCashBonus(evt)
   1702 	case RedrawSeatsEvent:
   1703 		html += drawSeatsStyle(authUser, g)
   1704 	case PokerSeatTakenEvent:
   1705 		html += drawSeatsStyle(authUser, g)
   1706 	case PokerSeatLeftEvent:
   1707 		html += drawSeatsStyle(authUser, g)
   1708 	case PokerWaitTurnEvent:
   1709 		html += drawCountDownStyle(evt)
   1710 	case PokerYourTurnEvent:
   1711 		html += drawYourTurnHtml(authUser)
   1712 	case PokerEvent:
   1713 		html += getPokerEventHtml(evt, animationTime.String())
   1714 	case PokerMainPotUpdatedEvent:
   1715 		html += drawMainPotHtml(evt)
   1716 	case PokerMinRaiseUpdatedEvent:
   1717 		html += drawMinRaiseHtml(evt)
   1718 	}
   1719 	return
   1720 }
   1721 
   1722 func buildGameDiv(g *Game, authUser *database.User) (html string) {
   1723 	roomID := g.roomID
   1724 	html += `<div id="game">`
   1725 	html += `<div id="table"><div class="inner"></div><div class="cards-outline"></div></div>`
   1726 	html += buildSeatsHtml(g, authUser)
   1727 	html += buildCardsHtml()
   1728 	html += buildActionsDiv(roomID)
   1729 	html += buildDealerTokenHtml(g)
   1730 	html += buildMainPotHtml(g)
   1731 	html += buildMinRaiseHtml(g)
   1732 	html += buildWinnerHtml()
   1733 	html += `</div>`
   1734 	return
   1735 }
   1736 
   1737 func BuildBaseHtml(g *Game, authUser *database.User, chatRoomSlug string) (html string) {
   1738 	ongoing := g.ongoing
   1739 	roomID := g.roomID
   1740 	html += hutils.HtmlCssReset
   1741 	html += pokerCss
   1742 	//html += `<script>document.onclick = function(e) { console.log(e.x, e.y); };</script>` // TODO: dev only
   1743 	//html += buildDevHtml()
   1744 	html += buildGameDiv(g, authUser)
   1745 	html += buildSoundsHtml(authUser)
   1746 	html += buildHelpHtml()
   1747 	html += `<div id="chat-div">`
   1748 	html += `	<iframe id="chat-top-bar" name="iframe1" src="/api/v1/chat/top-bar/` + chatRoomSlug + `" sandbox="allow-forms allow-scripts allow-same-origin allow-top-navigation-by-user-activation"></iframe>`
   1749 	html += `	<iframe id="chat-content" name="iframe2" src="/api/v1/chat/messages/` + chatRoomSlug + `/stream?hrm=1&hactions=1&hide_ts=1"></iframe>`
   1750 	html += `</div>`
   1751 	html += `<iframe src="/poker/` + roomID.String() + `/logs" id="eventLogs"></iframe>`
   1752 
   1753 	if ongoing != nil {
   1754 		html += drawCountDownStyle(ongoing.waitTurnEvent.Get())
   1755 		html += drawAutoActionMsgEvent(ongoing.autoActionEvent.Get())
   1756 		ongoing.events.Each(func(evt PokerEvent) {
   1757 			if evt.UserID == 0 || evt.UserID == authUser.ID {
   1758 				html += getPokerEventHtml(evt, "0s")
   1759 			}
   1760 		})
   1761 	}
   1762 	return
   1763 }
   1764 
   1765 func buildSoundsHtml(authUser *database.User) (html string) {
   1766 	html += `
   1767 <div id="soundsStatus">
   1768 	<a href="/settings/chat" rel="noopener noreferrer" target="_blank">`
   1769 	if authUser.PokerSoundsEnabled {
   1770 		html += `<img src="/public/img/sounds-enabled.png" style="height: 20px;" alt="" title="Sounds enabled" />`
   1771 	} else {
   1772 		html += `<img src="/public/img/no-sound.png" style="height: 20px;" alt="" title="Sounds disabled" />`
   1773 	}
   1774 	html += `</a>
   1775 </div>`
   1776 	return
   1777 }
   1778 
   1779 func buildCardsHtml() (html string) {
   1780 	for i := 52; i >= 1; i-- {
   1781 		idxStr := itoa(i)
   1782 		html += fmt.Sprintf(`<div class="card-holder" id="card%s"><div class="back"><div class="inner"></div></div><div class="card"><div class="inner"></div></div></div>`, idxStr)
   1783 	}
   1784 	return
   1785 }
   1786 
   1787 func buildMainPotHtml(g *Game) string {
   1788 	ongoing := g.ongoing
   1789 	html := `<div id="mainPot"></div>`
   1790 	mainPot := uint64(0)
   1791 	if ongoing != nil {
   1792 		mainPot = uint64(ongoing.mainPot.Get())
   1793 	}
   1794 	html += `<style>#mainPot:before { content: "Pot: ` + itoa1(mainPot) + `"; }</style>`
   1795 	return html
   1796 }
   1797 
   1798 func buildMinRaiseHtml(g *Game) string {
   1799 	ongoing := g.ongoing
   1800 	html := `<div id="minRaise"></div>`
   1801 	minRaise := uint64(0)
   1802 	if ongoing != nil {
   1803 		minRaise = uint64(ongoing.minRaise.Get())
   1804 	}
   1805 	html += `<style>#minRaise:before { content: "Min raise: ` + itoa1(minRaise) + `"; }</style>`
   1806 	return html
   1807 }
   1808 
   1809 func buildActionsDiv(roomID RoomID) (html string) {
   1810 	htmlTmpl := `
   1811 <table id="actionsDiv">
   1812 	<tr>
   1813 		<td>
   1814 			<iframe src="/poker/{{ .RoomID }}/deal" id="dealBtn"></iframe>
   1815 			<iframe src="/poker/{{ .RoomID }}/unsit" id="unSitBtn"></iframe>
   1816 		</td>
   1817 		<td style="vertical-align: top;">
   1818 			<iframe src="/poker/{{ .RoomID }}/bet" id="betBtn"></iframe>
   1819 		</td>
   1820 	</tr>
   1821 	<tr>
   1822 		<td></td>
   1823 		<td><div id="autoAction"></div></td>
   1824 	</tr>
   1825 	<tr>
   1826 		<td colspan="2"><div id="errorMsg"></div></td>
   1827 	</tr>
   1828 </table>`
   1829 	data := map[string]any{
   1830 		"RoomID": roomID.String(),
   1831 	}
   1832 	return simpleTmpl(htmlTmpl, data)
   1833 }
   1834 
   1835 func simpleTmpl(htmlTmpl string, data any) string {
   1836 	var buf bytes.Buffer
   1837 	utils.Must1(utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data))
   1838 	return buf.String()
   1839 }
   1840 
   1841 func buildSeatsHtml(g *Game, authUser *database.User) (html string) {
   1842 	g.Players.RWith(func(gPlayers seatedPlayers) {
   1843 		for i := range gPlayers {
   1844 			html += `<div id="seat` + itoa(i+1) + `Pot" class="seatPot"></div>`
   1845 		}
   1846 		html += `<div>`
   1847 		for i := range gPlayers {
   1848 			idxStr := itoa(i + 1)
   1849 			html += `<div class="seat" id="seat` + idxStr + `">`
   1850 			html += `	<div class="cash-bonus"></div>`
   1851 			html += `	<div class="throne"></div>`
   1852 			html += `   <iframe src="/poker/` + g.roomID.String() + `/sit/` + idxStr + `" class="takeSeat takeSeat` + idxStr + `"></iframe>`
   1853 			html += `	<div class="inner"></div>`
   1854 			html += `	<div id="seat` + idxStr + `_cash" class="cash"></div>`
   1855 			html += `	<div id="seat` + idxStr + `_status" class="status"></div>`
   1856 			html += `	<div id="countdown` + idxStr + `" class="countdown"><div class="progress-container"><div class="progress-bar animate"></div></div></div>`
   1857 			html += `</div>`
   1858 		}
   1859 		html += `</div>`
   1860 	})
   1861 	html += drawSeatsStyle(authUser, g)
   1862 	return html
   1863 }
   1864 
   1865 func drawCashBonus(evt CashBonusEvent) (html string) {
   1866 	color := utils.Ternary(evt.IsGain, "#1ee91e", "orange")
   1867 	dur := utils.Ternary(evt.IsGain, "5s", "2s")
   1868 	fontSize := utils.Ternary(evt.IsGain, "25px", "18px")
   1869 	html += `<style>`
   1870 	html += fmt.Sprintf(`#seat%d .cash-bonus { animation: %s %s cubic-bezier(0.25, 0.1, 0.25, 1) forwards; color: %s; font-size: %s; }`,
   1871 		evt.PlayerSeatIdx+1, utils.Ternary(evt.Animation, "cashBonusAnimation", "cashBonusAnimation1"), dur, color, fontSize)
   1872 	html += fmt.Sprintf(`#seat%d .cash-bonus:before { content: "%s%s"; }`,
   1873 		evt.PlayerSeatIdx+1, utils.Ternary(evt.IsGain, "+", ""), evt.Amount)
   1874 	html += `</style>`
   1875 	return
   1876 }
   1877 
   1878 func drawSeatsStyle(authUser *database.User, g *Game) string {
   1879 	ongoing := g.ongoing
   1880 	html := "<style>"
   1881 	seated := g.IsSeated(authUser.ID)
   1882 	g.Players.RWith(func(players seatedPlayers) {
   1883 		for i, p := range players {
   1884 			idxStr := itoa(i + 1)
   1885 			display := utils.Ternary(p != nil || seated, "none", "block")
   1886 			html += fmt.Sprintf(`.takeSeat%s { display: %s; }`, idxStr, display)
   1887 			if p != nil {
   1888 				pUserID := p.userID
   1889 				pUsername := p.username
   1890 				if pUserID == authUser.ID {
   1891 					html += fmt.Sprintf(`#seat%s { border: 2px solid #0d1b8f; }`, idxStr)
   1892 				}
   1893 				html += fmt.Sprintf(`#seat%s .inner:before { content: "%s"; }`, idxStr, pUsername.String())
   1894 				html += fmt.Sprintf(`#seat%s .throne { display: none; }`, idxStr)
   1895 				html += drawSeatCashLabel(idxStr, itoa2(p.getCash()))
   1896 				html += drawSeatStatusLabel(idxStr, p.getStatus())
   1897 				if ongoing != nil {
   1898 					if op := ongoing.players.get(pUserID); op != nil && op.GetBet() > 0 {
   1899 						html += drawSeatPotLabel(idxStr, itoa2(op.GetBet()))
   1900 					}
   1901 				}
   1902 			} else {
   1903 				html += fmt.Sprintf(`#seat%s { border: 1px solid #333; }`, idxStr)
   1904 				html += fmt.Sprintf(`#seat%s .inner:before { content: ""; }`, idxStr)
   1905 				html += fmt.Sprintf(`#seat%s .throne { display: block; }`, idxStr)
   1906 				html += drawSeatCashLabel(idxStr, "")
   1907 				html += drawSeatStatusLabel(idxStr, "")
   1908 			}
   1909 		}
   1910 	})
   1911 	html += "</style>"
   1912 	return html
   1913 }
   1914 
   1915 func drawSeatPotLabel(seatIdxStr, betStr string) string {
   1916 	return fmt.Sprintf(`#seat%sPot:before { content: "%s"; }`, seatIdxStr, betStr)
   1917 }
   1918 
   1919 func drawSeatCashLabel(seatIdxStr, cashStr string) string {
   1920 	return fmt.Sprintf(`#seat%s_cash:before { content: "%s"; }`, seatIdxStr, cashStr)
   1921 }
   1922 
   1923 func drawSeatStatusLabel(seatIdxStr, statusStr string) string {
   1924 	return fmt.Sprintf(`#seat%s_status:before { content: "%s"; }`, seatIdxStr, statusStr)
   1925 }
   1926 
   1927 func drawAutoActionMsgEvent(evt AutoActionEvent) (html string) {
   1928 	display := utils.Ternary(evt.Message != "", "block", "none")
   1929 	html += fmt.Sprintf(`<style>#autoAction { display: %s; } #autoAction:before { content: "%s"; }</style>`, display, evt.Message)
   1930 	return
   1931 }
   1932 
   1933 func drawErrorMsgEvent(evt ErrorMsgEvent) (html string) {
   1934 	display := utils.Ternary(evt.Message != "", "block", "none")
   1935 	html += fmt.Sprintf(`<style>#errorMsg { display: %s; } #errorMsg:before { content: "%s"; }</style>`, display, evt.Message)
   1936 	return
   1937 }
   1938 
   1939 func drawPlayerBetEvent(evt PlayerBetEvent) (html string) {
   1940 	idxStr := itoa(evt.PlayerSeatIdx + 1)
   1941 	html += `<style>`
   1942 	html += drawSeatPotLabel(idxStr, itoa2(evt.TotalBet))
   1943 	html += drawSeatCashLabel(idxStr, itoa2(evt.Cash))
   1944 	html += `</style>`
   1945 	return
   1946 }
   1947 
   1948 func drawGameStartedEvent(evt GameStartedEvent, authUser *database.User) (html string) {
   1949 	pos := dealerTokenPos[evt.DealerSeatIdx]
   1950 	html += `<style>`
   1951 	html += `#dealerToken { top: ` + itoa(pos[0]) + `px; left: ` + itoa(pos[1]) + `px; }`
   1952 	html += `#dealBtn { visibility: hidden; }`
   1953 	html += `</style>`
   1954 	if authUser.PokerSoundsEnabled {
   1955 		html += `<audio src="/public/mp3/shuffle_cards.mp3" autoplay></audio>`
   1956 	}
   1957 	return
   1958 }
   1959 
   1960 func buildWinnerHtml() string {
   1961 	html := `<div id="winner"></div>`
   1962 	html += `<style>#winner:before { content: ""; }</style>`
   1963 	return html
   1964 }
   1965 
   1966 func drawGameIsDoneHtml(g *Game, evt GameIsDoneEvent) (html string) {
   1967 	html += `<style>`
   1968 	g.Players.RWith(func(gPlayers seatedPlayers) {
   1969 		for i, p := range gPlayers {
   1970 			if p != nil {
   1971 				html += drawSeatCashLabel(itoa(i+1), itoa2(p.getCash()))
   1972 			}
   1973 		}
   1974 	})
   1975 	html += `#winner:before { content: "Winner: ` + evt.Winner + ` (` + evt.WinnerHand + `)"; }`
   1976 	html += "</style>"
   1977 	return
   1978 }
   1979 
   1980 func drawGameIsOverHtml(g *Game) (html string) {
   1981 	html += `<style>`
   1982 	html += `#dealBtn { visibility: visible; }`
   1983 	html += "</style>"
   1984 	return
   1985 }
   1986 
   1987 func drawResetCardsEvent() (html string) {
   1988 	html += `<style>`
   1989 	for i := 1; i <= 52; i++ {
   1990 		idxStr := itoa(i)
   1991 		transition := fmt.Sprintf(` transition: %s ease-in-out; transform: translateX(%spx) translateY(%spx) rotateY(%s);`,
   1992 			animationTime.String(), itoa(DealerStackX), itoa(DealerStackY), BackfacingDeg)
   1993 		html += `#card` + idxStr + ` { z-index: ` + itoa(53-i) + `; ` + transition + ` }
   1994 				#card` + idxStr + ` .card .inner:before { content: ""; }`
   1995 	}
   1996 	html += `
   1997 				#winner:before { content: ""; }
   1998 				#mainPot:before { content: "Pot: 0"; }
   1999 			</style>`
   2000 	return
   2001 }
   2002 
   2003 func drawPlayerFoldEvent(evt PlayerFoldEvent) (html string) {
   2004 	idx1Str := itoa(evt.Card1Idx)
   2005 	idx2Str := itoa(evt.Card2Idx)
   2006 	transition := fmt.Sprintf(`transition: %s ease-in-out; transform: translateX(%spx) translateY(%spx) rotateY(%s);`,
   2007 		animationTime.String(), itoa(BurnStackX), itoa(BurnStackY), BackfacingDeg)
   2008 	html = fmt.Sprintf(`<style>#card%s, #card%s { %s }</style>`, idx1Str, idx2Str, transition)
   2009 	return
   2010 }
   2011 
   2012 func drawYourTurnHtml(authUser *database.User) (html string) {
   2013 	if authUser.PokerSoundsEnabled {
   2014 		html += `<audio src="/public/mp3/sound7.mp3" autoplay></audio>`
   2015 	}
   2016 	return
   2017 }
   2018 
   2019 func drawCountDownStyle(evt PokerWaitTurnEvent) string {
   2020 	html := "<style>"
   2021 	html += hideCountdowns()
   2022 	html += resetSeatsBackgroundColor()
   2023 	remainingSecs := int((MaxUserCountdown*time.Second - time.Since(evt.CreatedAt)).Milliseconds())
   2024 	if evt.Idx >= 0 && evt.Idx <= 5 {
   2025 		idxStr := itoa(evt.Idx + 1)
   2026 		html += fmt.Sprintf(`#seat%s { background-color: rgba(200, 45, 45, 0.7); }`, idxStr)
   2027 		html += fmt.Sprintf(`#countdown%s { display: block; }`, idxStr)
   2028 		html += fmt.Sprintf(`#countdown%s .animate { --duration: %s; animation: progressBarAnimation calc(var(--duration) * 1ms) linear forwards; }`, idxStr, itoa(remainingSecs))
   2029 	}
   2030 	html += "</style>"
   2031 	return html
   2032 }
   2033 
   2034 func createCssIDList(idFmt string) (out string) {
   2035 	cssIDList := make([]string, 0)
   2036 	for i := 1; i <= NbPlayers; i++ {
   2037 		cssIDList = append(cssIDList, fmt.Sprintf(idFmt, itoa(i)))
   2038 	}
   2039 	return strings.Join(cssIDList, ", ")
   2040 }
   2041 
   2042 func hideCountdowns() (out string) {
   2043 	out += createCssIDList("#countdown%s") + ` { display: none; }`
   2044 	return
   2045 }
   2046 
   2047 func resetSeatsBackgroundColor() (out string) {
   2048 	return createCssIDList("#seat%s") + ` { background-color: rgba(45, 45, 45, 0.4); }`
   2049 }
   2050 
   2051 func resetSeatsPot() (out string) {
   2052 	return createCssIDList("#seat%sPot:before") + ` { content: ""; }`
   2053 }
   2054 
   2055 func drawMainPotHtml(evt PokerMainPotUpdatedEvent) (html string) {
   2056 	html += `<style>`
   2057 	html += resetSeatsPot()
   2058 	html += `#mainPot:before { content: "Pot: ` + itoa2(evt.MainPot) + `"; }`
   2059 	html += `</style>`
   2060 	return
   2061 }
   2062 
   2063 func drawMinRaiseHtml(evt PokerMinRaiseUpdatedEvent) (html string) {
   2064 	html += `<style>`
   2065 	html += `#minRaise:before { content: "Min raise: ` + itoa2(evt.MinRaise) + `"; }`
   2066 	html += `</style>`
   2067 	return
   2068 }
   2069 
   2070 func getPokerEventHtml(payload PokerEvent, animationTime string) string {
   2071 	transform := `transform: translate(` + itoa(payload.Left) + `px, ` + itoa(payload.Top) + `px)`
   2072 	transform += utils.Ternary(payload.Angle != "", ` rotateZ(`+payload.Angle+`)`, ``)
   2073 	transform += utils.Ternary(!payload.Reveal, ` rotateY(`+BackfacingDeg+`)`, ``)
   2074 	transform += ";"
   2075 	pokerEvtHtml := `<style>`
   2076 	if payload.ID1 != 0 {
   2077 		pokerEvtHtml += `#card` + itoa(payload.ID1) + ` { z-index: ` + itoa(payload.ZIdx+1) + `; }`
   2078 	}
   2079 	pokerEvtHtml += `
   2080 #card` + itoa(payload.ID) + ` { z-index: ` + itoa(payload.ZIdx) + `; transition: ` + animationTime + ` ease-in-out; ` + transform + ` }
   2081 #card` + itoa(payload.ID) + ` .card .inner:before { content: "` + payload.Name + `"; color: ` + colorForCard(payload.Name) + `; }
   2082 </style>`
   2083 	return pokerEvtHtml
   2084 }
   2085 
   2086 func buildDevHtml() (html string) {
   2087 	return `<div class="dev_seat1_card1"></div>
   2088 <div class="dev_seat2_card1"></div>
   2089 <div class="dev_seat3_card1"></div>
   2090 <div class="dev_seat4_card1"></div>
   2091 <div class="dev_seat5_card1"></div>
   2092 <div class="dev_seat6_card1"></div>
   2093 <div class="dev_community_card1"></div>
   2094 <div class="dev_community_card2"></div>
   2095 <div class="dev_community_card3"></div>
   2096 <div class="dev_community_card4"></div>
   2097 <div class="dev_community_card5"></div>
   2098 `
   2099 }
   2100 
   2101 func buildHelpHtml() (html string) {
   2102 	html += `
   2103 <style>
   2104 .heart::after { content: '♥'; display: block; }
   2105 .diamond::after { content: '♦'; display: block; }
   2106 .spade::after { content: '♠'; display: block; }
   2107 .club::after { content: '♣'; display: block; }
   2108 .help { position: absolute; z-index: 999999; left: 50px; top: 12px; }
   2109 .help-content { display: none; }
   2110 .help:hover .help-content { display: block; }
   2111 .disabled::before {
   2112   content: '';
   2113   position: absolute;
   2114   top: 0;
   2115   left: 0;
   2116   width: 100%;
   2117   height: 100%;
   2118   background-color: rgba(0, 0, 0, 0.4); /* Adjust the transparency as needed */
   2119   pointer-events: none; /* Allow clicking through the overlay */
   2120 }
   2121 .title {
   2122 	font-family: Arial,Helvetica,sans-serif;
   2123 	font-weight: bolder;
   2124 }
   2125 </style>
   2126 <div class="help">
   2127 	Help
   2128 	<div class="help-content">
   2129 		<div style="position: absolute; top: 10px; left: 10px; padding: 10px; z-index: 1000; background-color: #ccc; display: flex; width: 365px; border: 1px solid black; border-radius: 5px;">
   2130 			<div style="margin-right: 20px">
   2131 				<div class="title">1- Royal Flush</div>
   2132 				<div>
   2133 					<div class="mini-card red">A<span class="heart"></span></div>
   2134 					<div class="mini-card red">K<span class="heart"></span></div>
   2135 					<div class="mini-card red">Q<span class="heart"></span></div>
   2136 					<div class="mini-card red">J<span class="heart"></span></div>
   2137 					<div class="mini-card red">10<span class="heart"></span></div>
   2138 				</div>
   2139 				<div class="title">2- Straight Flush</div>
   2140 				<div>
   2141 					<div class="mini-card red">10<span class="heart"></div>
   2142 					<div class="mini-card red">9<span class="heart"></div>
   2143 					<div class="mini-card red">8<span class="heart"></div>
   2144 					<div class="mini-card red">7<span class="heart"></div>
   2145 					<div class="mini-card red">6<span class="heart"></div>
   2146 				</div>
   2147 				<div class="title">3- Four of a kind</div>
   2148 				<div>
   2149 					<div class="mini-card red">A<span class="heart"></div>
   2150 					<div class="mini-card">A<span class="club"></div>
   2151 					<div class="mini-card red">A<span class="diamond"></div>
   2152 					<div class="mini-card">A<span class="spade"></div>
   2153 					<div class="mini-card red disabled">K<span class="heart"></div>
   2154 				</div>
   2155 				<div class="title">4- Full house</div>
   2156 				<div>
   2157 					<div class="mini-card red">A<span class="heart"></div>
   2158 					<div class="mini-card">A<span class="club"></div>
   2159 					<div class="mini-card red">A<span class="diamond"></div>
   2160 					<div class="mini-card">K<span class="spade"></div>
   2161 					<div class="mini-card red">K<span class="heart"></div>
   2162 				</div>
   2163 				<div class="title">5- Flush</div>
   2164 				<div>
   2165 					<div class="mini-card">K<span class="club"></div>
   2166 					<div class="mini-card">10<span class="club"></div>
   2167 					<div class="mini-card">8<span class="club"></div>
   2168 					<div class="mini-card">7<span class="club"></div>
   2169 					<div class="mini-card">5<span class="club"></div>
   2170 				</div>
   2171 			</div>
   2172 		
   2173 			<div>
   2174 				<div class="title">6- Straight</div>
   2175 				<div>
   2176 					<div class="mini-card red">10<span class="heart"></div>
   2177 					<div class="mini-card">9<span class="club"></div>
   2178 					<div class="mini-card red">8<span class="diamond"></div>
   2179 					<div class="mini-card">7<span class="spade"></div>
   2180 					<div class="mini-card red">6<span class="heart"></div>
   2181 				</div>
   2182 				<div class="title">7- Three of a kind</div>
   2183 				<div>
   2184 					<div class="mini-card red">A<span class="heart"></div>
   2185 					<div class="mini-card red">A<span class="diamond"></div>
   2186 					<div class="mini-card">A<span class="club"></div>
   2187 					<div class="mini-card disabled">K<span class="spade"></div>
   2188 					<div class="mini-card red disabled">Q<span class="heart"></div>
   2189 				</div>
   2190 				<div class="title">8- Two pair</div>
   2191 				<div>
   2192 					<div class="mini-card red">A<span class="heart"></div>
   2193 					<div class="mini-card">A<span class="club"></div>
   2194 					<div class="mini-card red">K<span class="diamond"></div>
   2195 					<div class="mini-card">K<span class="spade"></div>
   2196 					<div class="mini-card red disabled">7<span class="heart"></div>
   2197 				</div>
   2198 				<div class="title">9- Pair</div>
   2199 				<div>
   2200 					<div class="mini-card red">A<span class="heart"></div>
   2201 					<div class="mini-card">A<span class="club"></div>
   2202 					<div class="mini-card red disabled">K<span class="diamond"></div>
   2203 					<div class="mini-card disabled">J<span class="spade"></div>
   2204 					<div class="mini-card red disabled">7<span class="heart"></div>
   2205 				</div>
   2206 				<div class="title">10- High card</div>
   2207 				<div>
   2208 					<div class="mini-card red">A<span class="heart"></div>
   2209 					<div class="mini-card disabled">K<span class="club"></div>
   2210 					<div class="mini-card red disabled">Q<span class="diamond"></div>
   2211 					<div class="mini-card disabled">9<span class="spade"></div>
   2212 					<div class="mini-card red disabled">7<span class="heart"></div>
   2213 				</div>
   2214 			</div>
   2215 		</div>
   2216 	</div>
   2217 </div>`
   2218 	return
   2219 }
   2220 
   2221 func itoa(i int) string {
   2222 	return strconv.Itoa(i)
   2223 }
   2224 
   2225 func itoa1(i uint64) string {
   2226 	return fmt.Sprintf("%d", i)
   2227 }
   2228 
   2229 func itoa2(i database.PokerChip) string {
   2230 	return fmt.Sprintf("%d", i)
   2231 }
   2232 
   2233 var pokerCss = `<style>
   2234 html, body { height: 100%; width: 100%; }
   2235 body {
   2236 	background:linear-gradient(135deg, #449144 33%,#008a00 95%);
   2237 }
   2238 .card-holder{
   2239 	position: absolute;
   2240 	top: 0;
   2241 	left: 0;
   2242 	transform: translateX(` + itoa(DealerStackX) + `px) translateY(` + itoa(DealerStackY) + `px) rotateY(` + BackfacingDeg + `);
   2243 	transform-style: preserve-3d;
   2244 	backface-visibility: hidden;
   2245 	width:50px;
   2246 	height:70px;
   2247 	display:inline-block;
   2248 	box-shadow:1px 2px 2px rgba(0,0,0,.8);
   2249 	margin:2px;
   2250 }
   2251 .mini-card {
   2252 	width: 29px;
   2253 	height: 38px;
   2254 	padding-top: 2px;
   2255 	display: inline-grid;
   2256 	justify-content: center;
   2257 	justify-items: center;
   2258 	font-size: 20px;
   2259 	font-weight: bolder;
   2260 	background-color:#fcfcfc;
   2261 	border-radius:2%;
   2262 	border:1px solid black;
   2263 }
   2264 .red { color: #cc0000; }
   2265 .disabled { position: relative; background-color: #bbb; }
   2266 .card {
   2267 	box-shadow: inset 2px 2px 0 #fff, inset -2px -2px 0 #fff;
   2268 	transform-style: preserve-3d;
   2269 	position:absolute;
   2270 	top:0;
   2271 	left:0;
   2272 	bottom:0;
   2273 	right:0;
   2274 	backface-visibility: hidden;
   2275 	background-color:#fcfcfc;
   2276 	border-radius:2%;
   2277 	display:block;
   2278 	width:100%;
   2279 	height:100%;
   2280 	border:1px solid black;
   2281 }
   2282 .card .inner {
   2283 	padding: 5px;
   2284 	font-size: 25px;
   2285 	display: flex;
   2286 	justify-content: center;
   2287 }
   2288 .back{
   2289 	position:absolute;
   2290 	top:0;
   2291 	left:0;
   2292 	bottom:0;
   2293 	right:0;
   2294 	width:100%;
   2295 	height:100%;
   2296 	backface-visibility: hidden;
   2297 	transform: rotateY(` + BackfacingDeg + `);
   2298 	background: linear-gradient(135deg, #5D7B93 0%, #6D7E8C 50%, #4C6474 51%, #5D7B93 100%);
   2299 	border-radius:2%;
   2300 	box-shadow: inset 3px 3px 0 #fff, inset -3px -3px 0 #fff;
   2301 	display:block;
   2302 	border:1px solid black;
   2303 }
   2304 .back .inner {
   2305 	background-image: url(/public/img/trees.gif);
   2306     width: 90%;
   2307     height: 80%;
   2308     background-size: contain;
   2309     position: absolute;
   2310     opacity: 0.4;
   2311     margin-top: 8px;
   2312     margin-left: 6px;
   2313     background-repeat: no-repeat;
   2314 }
   2315 .takeSeat {
   2316 	width: 65px;
   2317 	height: 40px;
   2318 	display: flex;
   2319 	margin-left: auto;
   2320 	margin-right: auto;
   2321 	margin-top: 4px;
   2322 	position: absolute;
   2323 	left: 10px;
   2324 }
   2325 .seat {
   2326 	border: 1px solid #333;
   2327 	border-radius: 4px;
   2328 	background-color: rgba(45, 45, 45, 0.4);
   2329 	padding: 1px 2px;
   2330 	min-width: 80px;
   2331 	min-height: 48px;
   2332 	color: #ddd;
   2333 }
   2334 .seat .inner { display: flex; justify-content: center; }
   2335 .seat .cash { display: flex; justify-content: center; }
   2336 .seat .status { display: flex; justify-content: center; }
   2337 .seat .throne {
   2338 	background-image: url(/public/img/throne.png);
   2339     background-size: contain;
   2340     background-repeat: no-repeat;
   2341     background-position: center;
   2342     position: absolute;
   2343     width: 100%;
   2344     height: 90%;
   2345     opacity: 0.3;
   2346 }
   2347 
   2348 .dev_seat1_card1 { top: 55px; left: 610px; transform: rotateZ(-95deg); width:50px; height:70px; background-color: white; position: absolute; }
   2349 .dev_seat1_card2 {}
   2350 .dev_seat2_card1 { top: 175px; left: 620px; transform: rotateZ(-80deg); width:50px; height:70px; background-color: white; position: absolute; }
   2351 .dev_seat2_card2 {}
   2352 .dev_seat3_card1 { top: 290px; left: 580px; transform: rotateZ(-50deg); width:50px; height:70px; background-color: white; position: absolute; }
   2353 .dev_seat3_card2 {}
   2354 .dev_seat4_card1 { top: 310px; left: 430px; transform: rotateZ(0deg); width:50px; height:70px; background-color: white; position: absolute; }
   2355 .dev_seat4_card2 {}
   2356 .dev_seat5_card1 { top: 315px; left: 240px; transform: rotateZ(0deg); width:50px; height:70px; background-color: white; position: absolute; }
   2357 .dev_seat5_card2 {}
   2358 .dev_seat6_card1 { top: 270px; left: 70px; transform: rotateZ(10deg); width:50px; height:70px; background-color: white; position: absolute; }
   2359 .dev_seat6_card2 {}
   2360 .dev_community_card1 {top: ` + itoa(DealY) + `px; left: calc(` + itoa(DealX) + `px + 1 * ` + itoa(DealSpacing) + `px); width:50px; height:70px; background-color: white; position: absolute; }
   2361 .dev_community_card2 {top: ` + itoa(DealY) + `px; left: calc(` + itoa(DealX) + `px + 2 * ` + itoa(DealSpacing) + `px); width:50px; height:70px; background-color: white; position: absolute; }
   2362 .dev_community_card3 {top: ` + itoa(DealY) + `px; left: calc(` + itoa(DealX) + `px + 3 * ` + itoa(DealSpacing) + `px); width:50px; height:70px; background-color: white; position: absolute; }
   2363 .dev_community_card4 {top: ` + itoa(DealY) + `px; left: calc(` + itoa(DealX) + `px + 4 * ` + itoa(DealSpacing) + `px); width:50px; height:70px; background-color: white; position: absolute; }
   2364 .dev_community_card5 {top: ` + itoa(DealY) + `px; left: calc(` + itoa(DealX) + `px + 5 * ` + itoa(DealSpacing) + `px); width:50px; height:70px; background-color: white; position: absolute; }
   2365 
   2366 #seat1 { position: absolute; top: 80px; left: 690px; }
   2367 #seat2 { position: absolute; top: 200px; left: 700px; }
   2368 #seat3 { position: absolute; top: 360px; left: 640px; }
   2369 #seat4 { position: absolute; top: 400px; left: 410px; }
   2370 #seat5 { position: absolute; top: 400px; left: 220px; }
   2371 #seat6 { position: absolute; top: 360px; left: 30px; }
   2372 #seat1_cash { }
   2373 #seat2_cash { }
   2374 #seat3_cash { }
   2375 #seat4_cash { }
   2376 #seat5_cash { }
   2377 #seat6_cash { }
   2378 .seatPot {
   2379 	font-size: 20px;
   2380 	font-family: Arial,Helvetica,sans-serif;
   2381 }
   2382 #seat1Pot { top: 88px; left: 528px; width: 50px; position: absolute; text-align: right; }
   2383 #seat2Pot { top: 190px; left: 530px; width: 50px; position: absolute; text-align: right; }
   2384 #seat3Pot { top: 280px; left: 525px; width: 50px; position: absolute; text-align: right; }
   2385 #seat4Pot { top: 290px; left: 430px; position: absolute; }
   2386 #seat5Pot { top: 290px; left: 240px; position: absolute; }
   2387 #seat6Pot { top: 245px; left: 86px; position: absolute; }
   2388 .takeSeat1 { }
   2389 .takeSeat2 { }
   2390 .takeSeat3 { }
   2391 .takeSeat4 { }
   2392 .takeSeat5 { }
   2393 .takeSeat6 { }
   2394 #actionsDiv { position: absolute; top: 470px; left: 100px; }
   2395 #dealBtn { width: 80px; height: 30px; display: inline-block; vertical-align: top; }
   2396 #unSitBtn { width: 80px; height: 30px; display: inline-block; vertical-align: top; }
   2397 #checkBtn { width: 60px; height: 30px; display: inline-block; vertical-align: top; }
   2398 #foldBtn { width: 50px; height: 30px; display: inline-block; vertical-align: top; }
   2399 #callBtn { width: 50px; height: 30px; display: inline-block; vertical-align: top; }
   2400 #betBtn { width: 400px; height: 45px; display: inline-block; vertical-align: top; }
   2401 .countdown { display: none; position: absolute; left: 0px; right: 2px; bottom: -9px; }
   2402 #mainPot { position: absolute; top: 220px; left: 215px; font-size: 20px; font-family: Arial,Helvetica,sans-serif; }
   2403 #minRaise { position: absolute; top: 220px; left: 365px; font-size: 18px; font-family: Arial,Helvetica,sans-serif; }
   2404 #winner { position: absolute; top: 265px; left: 250px; }
   2405 #errorMsg {
   2406 	margin-top: 10px;
   2407 	color: darkred;
   2408 	font-size: 20px;
   2409 	font-family: Arial,Helvetica,sans-serif;
   2410 	background-color: #ffadad;
   2411 	border: 1px solid #6e1616;
   2412 	padding: 2px 3px;
   2413 	border-radius: 3px;
   2414 	display: none;
   2415 }
   2416 #autoAction {
   2417 	color: #072a85;
   2418 	font-size: 20px;
   2419 	font-family: Arial,Helvetica,sans-serif;
   2420 	background-color: #bcd8ff;
   2421 	border: 1px solid #072a85;
   2422 	display: none;
   2423 	padding: 2px 3px;
   2424 	border-radius: 3px;
   2425 }
   2426 #chat-div { position: absolute; bottom: 0px; left: 0; right: 243px; min-width: 557px; height: 250px; z-index: 200; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); }
   2427 #chat-top-bar { height: 57px; width: 100%; background-color: #222; }
   2428 #chat-content { height: 193px; width: 100%; background-color: #222; }
   2429 #eventLogs { position: absolute; bottom: 0px; right: 0px; width: 243px; height: 250px; background-color: #444; z-index: 200; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); }
   2430 #dealerToken { top: 142px; left: 714px; width: 20px; height: 20px; background-color: #ccc; border: 1px solid #333; border-radius: 11px; position: absolute; }
   2431 #dealerToken .inner { padding: 2px 4px; }
   2432 #dealerToken .inner:before { content: "D"; }
   2433 #soundsStatus {
   2434 	position: absolute; top: 10px; left: 10px;
   2435 }
   2436 #game {
   2437 	position: absolute;
   2438 	left: 0px;
   2439 	top: 0px;
   2440 	width: 760px;
   2441 	height: 400px;
   2442 }
   2443 #table {
   2444 	position: absolute; top: 20px; left: 20px; width: 750px; height: 400px; border-radius: 300px;
   2445 	background: radial-gradient(#449144, #008a00);
   2446 	box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
   2447 	border: 5px solid #2c692c;
   2448 }
   2449 #table .inner {
   2450 	background-image: url(/public/img/trees.gif);
   2451     width: 90%;
   2452     height: 80%;
   2453     display: block;
   2454     position: absolute;
   2455     background-size: contain;
   2456     opacity: 0.07;
   2457     background-repeat: no-repeat;
   2458     background-position: right;
   2459     margin-top: 45px;
   2460 }
   2461 #table .cards-outline {
   2462     position: absolute;
   2463     width: 280px;
   2464     height: 80px;
   2465     border: 3px solid rgba(128, 217, 133, 0.7);
   2466     border-radius: 8px;
   2467     left: 180px;
   2468     top: 100px;
   2469 }
   2470 
   2471 @keyframes cashBonusAnimation {
   2472     0% {
   2473         opacity: 1;
   2474         transform: translateY(0);
   2475     }
   2476     66.66% {
   2477         opacity: 1;
   2478         transform: translateY(0);
   2479 		transform: translateY(-15px);
   2480     }
   2481     100% {
   2482         opacity: 0;
   2483         transform: translateY(-30px); /* Adjust the distance it moves up */
   2484     }
   2485 }
   2486 
   2487 @keyframes cashBonusAnimation1 {
   2488     0% {
   2489         opacity: 1;
   2490         transform: translateY(0);
   2491     }
   2492     66.66% {
   2493         opacity: 1;
   2494         transform: translateY(0);
   2495 		transform: translateY(-15px);
   2496     }
   2497     100% {
   2498         opacity: 0;
   2499         transform: translateY(-30px); /* Adjust the distance it moves up */
   2500     }
   2501 }
   2502 
   2503 .cash-bonus {
   2504 	z-index: 108;
   2505     position: absolute;
   2506 	background-color: rgba(0, 0, 0, 0.99);
   2507 	padding: 1px 5px;
   2508 	border-radius: 5px;
   2509 	opacity: 0;
   2510 	font-family: Arial,Helvetica,sans-serif;
   2511 }
   2512 
   2513 /* Styles for the progress bar container */
   2514 .progress-container {
   2515   width: 100%;
   2516   height: 6px;
   2517   background-color: #f0f0f0;
   2518   overflow: hidden;
   2519   box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
   2520   border: 1px solid rgba(0, 0, 0, 0.9);
   2521 }
   2522 .progress-bar {
   2523   height: 100%;
   2524   width: 100%;
   2525   background-color: #4caf50;
   2526 }
   2527 @keyframes progressBarAnimation {
   2528   from { width: 100%; transform: translateZ(0); }
   2529   to   { width: 0;    transform: translateZ(0); }
   2530 }
   2531 
   2532 </style>`