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>`