dkforest

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

commit d02757a0fdb7667e5d84eb732a6426e68dcfb879
parent aa4c75e5b6a34f2a6009a0646144a5b4ec5cf514
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Fri, 22 Dec 2023 23:57:48 -0500

new poker

Diffstat:
Mpkg/web/handlers/interceptors/slashInterceptor.go | 13+++++++++++++
Mpkg/web/handlers/poker.go | 157+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpkg/web/handlers/poker/events.go | 2++
Mpkg/web/handlers/poker/poker.go | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mpkg/web/middlewares/middlewares.go | 7+------
Mpkg/web/web.go | 6------
6 files changed, 307 insertions(+), 180 deletions(-)

diff --git a/pkg/web/handlers/interceptors/slashInterceptor.go b/pkg/web/handlers/interceptors/slashInterceptor.go @@ -96,6 +96,7 @@ var userCmdsMap = map[string]CmdHandler{ "/check": handleCheckCmd, "/call": handleCallCmd, "/fold": handleFoldCmd, + "/raise": handleRaiseCmd, "/allin": handleAllInCmd, "/bet": handleBetCmd, "/deal": handleDealCmd, @@ -1955,6 +1956,18 @@ func handleFoldCmd(c *command.Command) (handled bool) { return false } +func handleRaiseCmd(c *command.Command) (handled bool) { + if c.Message == "/raise" { + roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-")) + if g := poker.PokerInstance.GetGame(roomID); g != nil { + g.Raise(c.AuthUser.ID) + } + c.Err = command.ErrRedirect + return true + } + return false +} + func handleBetCmd(c *command.Command) (handled bool) { if m := betRgx.FindStringSubmatch(c.Message); len(m) == 2 { roomID := poker.RoomID(strings.ReplaceAll(c.Room.Name, "_", "-")) diff --git a/pkg/web/handlers/poker.go b/pkg/web/handlers/poker.go @@ -437,95 +437,108 @@ Loop: return nil } -func PokerCheckHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := poker.RoomID(c.Param("roomID")) - g := poker.PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - g.Check(authUser.ID) - } - return c.HTML(http.StatusOK, hutils.HtmlCssReset+`<form method="post"><button>Check</button></form>`) -} - -func itoa2(i database.PokerChip) string { - return fmt.Sprintf("%d", i) -} - func PokerBetHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) roomID := poker.RoomID(c.Param("roomID")) + authUser := c.Get("authUser").(*database.User) + send := func(s string) { _, _ = c.Response().Write([]byte(s)) } g := poker.PokerInstance.GetGame(roomID) if g == nil { - return c.NoContent(http.StatusNotFound) + return c.Redirect(http.StatusFound, "/") } - bet := database.PokerChip(100) + if c.Request().Method == http.MethodPost { - bet = database.PokerChip(utils.DoParseUint64(c.Request().PostFormValue("betValue"))) - betBtn := c.Request().PostFormValue("bet") - if betBtn == "betAllIn" { + submitBtn := c.Request().PostFormValue("submitBtn") + if submitBtn == "check" { + g.Check(authUser.ID) + return c.Redirect(http.StatusFound, c.Request().Referer()) + } else if submitBtn == "call" { + g.Call(authUser.ID) + return c.Redirect(http.StatusFound, c.Request().Referer()) + } else if submitBtn == "fold" { + g.Fold(authUser.ID) + return c.Redirect(http.StatusFound, c.Request().Referer()) + } else if submitBtn == "allIn" { g.AllIn(authUser.ID) + return c.Redirect(http.StatusFound, c.Request().Referer()) } else { - if strings.HasPrefix(betBtn, "bet_") { - bet = database.PokerChip(utils.DoParseUint64(strings.TrimPrefix(betBtn, "bet_"))) - } - if bet > 0 { - g.Bet(authUser.ID, bet) + raiseBtn := c.Request().PostFormValue("raise") + if raiseBtn == "raise" { + g.Raise(authUser.ID) + return c.Redirect(http.StatusFound, c.Request().Referer()) + } else if raiseBtn == "raiseValue" { + raiseValue := database.PokerChip(utils.DoParseUint64(c.Request().PostFormValue("raiseValue"))) + g.Bet(authUser.ID, raiseValue) + return c.Redirect(http.StatusFound, c.Request().Referer()) } } } - shortcutsTable := map[database.PokerChip][]int{ - 3: {10, 20, 50}, - 20: {50, 100, 200}, - 200: {400, 800, 1500}, - } - v, found := shortcutsTable[g.PokerTableMinBet] - if !found { - v = shortcutsTable[20] + roomUserTopic := roomID.UserTopic(authUser.ID) + sub := poker.PokerPubSub.Subscribe([]string{roomUserTopic}) + defer sub.Close() + quit := hutils.CloseSignalChan(c) + hutils.SetStreamingHeaders(c) + + // Keep track of users streams, so we can limit how many are open at one time per user + if err := usersStreamsManager.Inst.Add(authUser.ID, roomUserTopic); err != nil { + return nil } - shortcuts := fmt.Sprintf(`<button type="submit" name="bet" value="bet_%d">%d</button> - <button type="submit" name="bet" value="bet_%d">%d</button> - <button type="submit" name="bet" value="bet_%d">%d</button>`, - v[0], v[0], - v[1], v[1], - v[2], v[2]) + defer usersStreamsManager.Inst.Remove(authUser.ID, roomUserTopic) - html := hutils.HtmlCssReset + ` -<form method="post"> - <input type="number" name="betValue" value="` + itoa2(bet) + `" style="width: 90px;" /><button type="submit" name="bet" value="bet">Bet</button><br /> - ` + shortcuts + ` - <button type="submit" name="bet" value="betAllIn">All-in</button> -</form>` - return c.HTML(http.StatusOK, html) -} + send(hutils.HtmlCssReset) -func PokerCallHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := poker.RoomID(c.Param("roomID")) - g := poker.PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - g.Call(authUser.ID) + if player := g.OngoingPlayer(authUser.ID); player != nil { + canCheck := g.CanCheck(player) + canFold := g.CanFold(player) + playerBet := player.GetBet() + betBtnLbl := utils.Ternary(g.IsBet(), "Bet", "Raise") + minBet := g.MinBet() + minRaise := g.MinRaise() + send(`<form method="post">`) + send(` <div style="display: inline-block; margin-right: 20px;">`) + send(fmt.Sprintf(`<input type="number" name="raiseValue" value="%s" min="%s" style="width: 90px;" /><button type="submit" name="raise" value="raiseValue">%s</button><br />`, minRaise, minRaise, betBtnLbl)) + send(` </div>`) + send(` <div style="display: inline-block; vertical-align: top;">`) + if canCheck { + send(` <button name="submitBtn" value="check">Check</button>`) + } + if minBet-playerBet > 0 { + send(` <button name="submitBtn" value="call">Call</button>`) + } + if canFold { + send(` <button name="submitBtn" value="fold">Fold</button>`) + } + send(` <button name="submitBtn" value="allIn">All-in</button>`) + send(` </div>`) + send(`</form>`) + send(fmt.Sprintf(`<div style="margin-top: 4px;">Min raise: %d</div>`, minRaise)) } - return c.HTML(http.StatusOK, hutils.HtmlCssReset+`<form method="post"><button>Call</button></form>`) -} + c.Response().Flush() -func PokerFoldHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := poker.RoomID(c.Param("roomID")) - g := poker.PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - g.Fold(authUser.ID) +Loop: + for { + select { + case <-quit: + break Loop + default: + } + + _, payload, err := sub.ReceiveTimeout2(1*time.Second, quit) + if err != nil { + if errors.Is(err, pubsub.ErrCancelled) { + break Loop + } + continue + } + + switch payload.(type) { + case poker.RefreshButtonsEvent: + send(fmt.Sprintf(`<meta http-equiv="refresh" content="0" />`)) + c.Response().Flush() + return nil + } } - return c.HTML(http.StatusOK, hutils.HtmlCssReset+`<form method="post"><button>Fold</button></form>`) + return nil } func PokerDealHandler(c echo.Context) error { @@ -558,7 +571,7 @@ func PokerUnSitHandler(c echo.Context) error { } func PokerSitHandler(c echo.Context) error { - html := hutils.HtmlCssReset + `<form method="post"><button>Sit</button></form>` + html := hutils.HtmlCssReset + `<form method="post"><button style="height: 35px; width: 66px;">Sit</button></form>` authUser := c.Get("authUser").(*database.User) pos := utils.Clamp(utils.DoParseInt(c.Param("pos")), 1, poker.NbPlayers) - 1 roomID := poker.RoomID(c.Param("roomID")) diff --git a/pkg/web/handlers/poker/events.go b/pkg/web/handlers/poker/events.go @@ -46,6 +46,8 @@ type AutoActionEvent struct { Message string } +type RefreshButtonsEvent struct{} + type ErrorMsgEvent struct { Message string } diff --git a/pkg/web/handlers/poker/poker.go b/pkg/web/handlers/poker/poker.go @@ -24,7 +24,7 @@ import ( const NbPlayers = 6 const MaxUserCountdown = 60 -const MinTimeAfterGame = 10 +const MinTimeAfterGame = 1 const BackfacingDeg = "-180deg" const BurnStackX = 400 const BurnStackY = 30 @@ -34,7 +34,7 @@ const DealSpacing = 55 const DealerStackX = 250 const DealerStackY = 30 const NbCardsPerPlayer = 2 -const animationTime = 1000 * time.Millisecond +const animationTime = 100 * time.Millisecond type Poker struct { sync.Mutex @@ -106,6 +106,7 @@ type playerEvent struct { Fold bool AllIn bool Unsit bool + Raise bool Bet database.PokerChip } @@ -119,6 +120,8 @@ func (e playerEvent) getAction() PlayerAction { action = CheckAction } else if e.Bet > 0 { action = BetAction + } else if e.Raise { + action = RaiseAction } else if e.AllIn { action = AllInAction } @@ -132,6 +135,9 @@ type ongoingGame struct { events rwmtx.RWMtxSlice[PokerEvent] waitTurnEvent rwmtx.RWMtx[PokerWaitTurnEvent] autoActionEvent rwmtx.RWMtx[AutoActionEvent] + MinBet rwmtx.RWMtx[database.PokerChip] + MinRaise rwmtx.RWMtx[database.PokerChip] + hasBet rwmtx.RWMtx[bool] players pokerPlayers createdAt time.Time mainPot atomic.Uint64 @@ -161,6 +167,12 @@ func (p seatedPlayers) get(userID database.UserID) (out *seatedPlayer) { return } +func (p seatedPlayers) resetStatuses() { + for _, player := range p { + player.status.Set("") + } +} + func (p seatedPlayers) toPokerPlayers() pokerPlayers { players := make([]*pokerPlayer, 0) for _, player := range p { @@ -186,6 +198,8 @@ type seatedPlayer struct { userID database.UserID username database.Username cash rwmtx.RWMtxUInt64[database.PokerChip] + status rwmtx.RWMtx[string] + hasChecked bool lastActionTS time.Time } @@ -193,6 +207,10 @@ func (p *seatedPlayer) getCash() (out database.PokerChip) { return p.cash.Get() } +func (p *seatedPlayer) getStatus() (out string) { + return p.status.Get() +} + // Return either or not a player is eligible to play a game func (p *seatedPlayer) isEligible(pokerTableMinBet database.PokerChip) bool { return p != nil && p.getCash() >= pokerTableMinBet @@ -209,7 +227,42 @@ type pokerPlayer struct { countChancesToAction int } -func (p *pokerPlayer) getBet() (out database.PokerChip) { +func (g *PokerGame) IsBet() (out bool) { + if g.ongoing != nil { + return !g.ongoing.hasBet.Get() + } + return +} + +func (g *PokerGame) CanCheck(player *pokerPlayer) (out bool) { + if g.ongoing != nil { + return player.bet.Get() == g.ongoing.MinBet.Get() + } + return +} + +func (g *PokerGame) CanFold(player *pokerPlayer) (out bool) { + if g.ongoing != nil { + return player.bet.Get() < g.ongoing.MinBet.Get() + } + return +} + +func (g *PokerGame) MinBet() (out database.PokerChip) { + if g.ongoing != nil { + return g.ongoing.MinBet.Get() + } + return +} + +func (p *PokerGame) MinRaise() (out database.PokerChip) { + if p.ongoing != nil { + return p.ongoing.MinRaise.Get() + } + return +} + +func (p *pokerPlayer) GetBet() (out database.PokerChip) { return p.bet.Get() } @@ -251,12 +304,12 @@ func (p *pokerPlayer) resetBet() (old database.PokerChip) { } func (p *pokerPlayer) refundBet(db *database.DkfDB, pokerTableID int64) { - p.gain(db, pokerTableID, p.getBet()) + p.gain(db, pokerTableID, p.GetBet()) } func (p *pokerPlayer) doBetAndNotif(db *database.DkfDB, pokerTableID int64, bet database.PokerChip, roomTopic string) { p.doBet(db, pokerTableID, bet) - PokerPubSub.Pub(roomTopic, PlayerBetEvent{PlayerSeatIdx: p.seatIdx, Player: p.username, Bet: bet, TotalBet: p.getBet(), Cash: p.getCash()}) + PokerPubSub.Pub(roomTopic, PlayerBetEvent{PlayerSeatIdx: p.seatIdx, Player: p.username, Bet: bet, TotalBet: p.GetBet(), Cash: p.getCash()}) } type playerCard struct { @@ -298,6 +351,10 @@ func (g *PokerGame) AllIn(userID database.UserID) { g.sendPlayerEvent(playerEvent{UserID: userID, AllIn: true}) } +func (g *PokerGame) Raise(userID database.UserID) { + g.sendPlayerEvent(playerEvent{UserID: userID, Raise: true}) +} + func (g *PokerGame) Bet(userID database.UserID, bet database.PokerChip) { g.sendPlayerEvent(playerEvent{UserID: userID, Bet: bet}) } @@ -463,7 +520,7 @@ func isRoundSettled(players []*pokerPlayer) bool { } arr := make([]Tmp, 0) for _, p := range players { - arr = append(arr, Tmp{Bet: p.getBet(), AllIn: p.isAllIn(), Folded: p.folded.Load()}) + arr = append(arr, Tmp{Bet: p.GetBet(), AllIn: p.isAllIn(), Folded: p.folded.Load()}) } sort.Slice(arr, func(i, j int) bool { return arr[i].Bet > arr[j].Bet }) b := arr[0].Bet @@ -644,6 +701,7 @@ const ( CheckAction BetAction AllInAction + RaiseAction ) func (a PlayerAction) String() string { @@ -658,6 +716,8 @@ func (a PlayerAction) String() string { return "check" case BetAction: return "bet" + case RaiseAction: + return "raise" case AllInAction: return "all-in" } @@ -700,10 +760,11 @@ func doUnsit(g *PokerGame, p *pokerPlayer, playerAlive *int) int { return continueGetPlayerEventLoop } -func doTimeout(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, playerAlive *int) int { +func doTimeout(g *PokerGame, p *pokerPlayer, playerAlive *int) int { pUsername := p.username - if p.getBet() < *minBet { + if p.GetBet() < g.ongoing.MinBet.Get() { foldPlayer(g, p) + p.status.Set("fold") g.newLogEvent(fmt.Sprintf("%s auto fold", pUsername)) *playerAlive-- @@ -712,18 +773,33 @@ func doTimeout(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, playerA } return breakGetPlayerEventLoop } + p.hasChecked = true + p.status.Set("check") g.newLogEvent(fmt.Sprintf("%s auto check", pUsername)) return breakGetPlayerEventLoop } -func doFold(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, playerAlive *int) int { +func doCheck(g *PokerGame, p *pokerPlayer) int { + if p.GetBet() < g.ongoing.MinBet.Get() { + msg := fmt.Sprintf("Need to bet %d", g.ongoing.MinBet.Get()-p.GetBet()) + PokerPubSub.Pub(g.roomID.UserTopic(p.userID), ErrorMsgEvent{Message: msg}) + return continueGetPlayerEventLoop + } + p.hasChecked = true + p.status.Set("check") + g.newLogEvent(fmt.Sprintf("%s check", p.username)) + return doNothing +} + +func doFold(g *PokerGame, p *pokerPlayer, playerAlive *int) int { roomUserTopic := g.roomID.UserTopic(p.userID) - if p.getBet() == *minBet { + if p.GetBet() == g.ongoing.MinBet.Get() { msg := fmt.Sprintf("Cannot fold if there is no bet; check") PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) - return doCheck(g, p, minBet) + return doCheck(g, p) } foldPlayer(g, p) + p.status.Set("fold") g.newLogEvent(fmt.Sprintf("%s fold", p.username)) *playerAlive-- @@ -734,84 +810,87 @@ func doFold(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, playerAliv return doNothing } -func doCheck(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip) int { - if p.getBet() < *minBet { - msg := fmt.Sprintf("Need to bet %d", *minBet-p.getBet()) - PokerPubSub.Pub(g.roomID.UserTopic(p.userID), ErrorMsgEvent{Message: msg}) - return continueGetPlayerEventLoop - } - g.newLogEvent(fmt.Sprintf("%s check", p.username)) - return doNothing -} - -func doCall(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, - newlyAllInPlayers *[]*pokerPlayer) int { +func doCall(g *PokerGame, p *pokerPlayer, + newlyAllInPlayers *[]*pokerPlayer, lastBetPlayerIdx, lastRaisePlayerIdx *int, playerToPlayIdx int) int { pUsername := p.username - bet := utils.MinInt(*minBet-p.getBet(), p.getCash()) + bet := utils.MinInt(g.ongoing.MinBet.Get()-p.GetBet(), p.getCash()) if bet == 0 { - g.newLogEvent(fmt.Sprintf("%s check", pUsername)) + return doCheck(g, p) + } else if bet == p.cash.Get() { + return doAllIn(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx) } else { + p.status.Set("call") p.doBetAndNotif(g.db, g.pokerTableID, bet, g.roomID.Topic()) logMsg := fmt.Sprintf("%s call (%d)", pUsername, bet) - if p.isAllIn() { - logMsg += " (all-in)" - *newlyAllInPlayers = append(*newlyAllInPlayers, p) - } g.newLogEvent(logMsg) } return doNothing } -func doAllIn(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, - newlyAllInPlayers *[]*pokerPlayer, lastRaisePlayerIdx *int, playerToPlayIdx int) int { +func doAllIn(g *PokerGame, p *pokerPlayer, + newlyAllInPlayers *[]*pokerPlayer, lastBetPlayerIdx, lastRaisePlayerIdx *int, playerToPlayIdx int) int { bet := p.getCash() - if (p.getBet() + bet) > *minBet { + minBet := g.ongoing.MinBet.Get() + if (p.GetBet() + bet) > minBet { + *lastBetPlayerIdx = playerToPlayIdx *lastRaisePlayerIdx = playerToPlayIdx + g.ongoing.MinRaise.Set(bet) } - *minBet = utils.MaxInt(p.getBet()+bet, *minBet) + g.ongoing.MinBet.Set(utils.MaxInt(p.GetBet()+bet, minBet)) p.doBetAndNotif(g.db, g.pokerTableID, bet, g.roomID.Topic()) logMsg := fmt.Sprintf("%s all-in (%d)", p.username, bet) if p.isAllIn() { *newlyAllInPlayers = append(*newlyAllInPlayers, p) } + p.status.Set("all-in") g.newLogEvent(logMsg) return doNothing } -func doBet(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, - newlyAllInPlayers *[]*pokerPlayer, lastRaisePlayerIdx *int, playerToPlayIdx int, evt playerEvent) int { +func doRaise(g *PokerGame, p *pokerPlayer, + newlyAllInPlayers *[]*pokerPlayer, lastBetPlayerIdx, lastRaisePlayerIdx *int, playerToPlayIdx int, evt playerEvent) int { + evt.Bet = g.ongoing.MinRaise.Get() + return doBet(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx, evt) +} + +func doBet(g *PokerGame, p *pokerPlayer, + newlyAllInPlayers *[]*pokerPlayer, lastBetPlayerIdx, lastRaisePlayerIdx *int, playerToPlayIdx int, evt playerEvent) int { roomTopic := g.roomID.Topic() roomUserTopic := g.roomID.UserTopic(p.userID) - pokerTableMinBet := g.PokerTableMinBet - bet := evt.Bet - playerBet := p.getBet() + minBet := g.ongoing.MinBet.Get() + minRaise := g.ongoing.MinRaise.Get() + playerBet := p.bet.Get() + bet := evt.Bet + (minBet - playerBet) + if bet >= p.cash.Get() { + return doAllIn(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx) + } + playerTotalBet := playerBet + bet + betLbl := utils.Ternary(g.IsBet(), "bet", "raise") // Ensure the player cannot bet below the table minimum bet (amount of the big blind) - if playerBet+bet != *minBet && bet < pokerTableMinBet { - msg := fmt.Sprintf("Bet (%d) is too low. Must bet at least %d", bet, pokerTableMinBet) + if bet < minRaise { + msg := fmt.Sprintf("%s (%d) is too low. Must %s at least %d", betLbl, bet, betLbl, minRaise) PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) return continueGetPlayerEventLoop } - if (playerBet + bet) < *minBet { - msg := fmt.Sprintf("Bet (%d) is too low. Must bet at least %d", bet, *minBet-playerBet) + if playerTotalBet < minBet+minRaise { + msg := fmt.Sprintf("%s (%d) is too low. Must %s at least %d", betLbl, evt.Bet, betLbl, (minBet+minRaise)-playerBet) PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) return continueGetPlayerEventLoop } - if bet > p.getCash() { - msg := fmt.Sprintf("Bet (%d) is too high. Must bet at most %d", bet, p.getCash()) - PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) - return continueGetPlayerEventLoop - } - if (playerBet + bet) > *minBet { - *lastRaisePlayerIdx = playerToPlayIdx - } - *minBet = utils.MaxInt(playerBet+bet, *minBet) + *lastBetPlayerIdx = playerToPlayIdx + *lastRaisePlayerIdx = playerToPlayIdx + g.ongoing.MinRaise.Set(evt.Bet) + g.ongoing.MinBet.Set(playerTotalBet) + p.doBetAndNotif(g.db, g.pokerTableID, bet, roomTopic) - logMsg := fmt.Sprintf("%s bet %d", p.username, bet) - if p.isAllIn() { - logMsg += " (all-in)" - *newlyAllInPlayers = append(*newlyAllInPlayers, p) + g.newLogEvent(fmt.Sprintf("%s %s %d", p.username, betLbl, g.ongoing.MinRaise.Get())) + if p.hasChecked { + p.status.Set("check-" + betLbl) + p.hasChecked = false + } else { + p.status.Set(betLbl) } - g.newLogEvent(logMsg) + g.ongoing.hasBet.Set(true) return doNothing } @@ -843,37 +922,39 @@ func handleAutoActionReceived(g *PokerGame, autoCache map[database.UserID]autoAc return continueGetPlayerEventLoop } -func applyAutoAction(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, +func applyAutoAction(g *PokerGame, p *pokerPlayer, newlyAllInPlayers *[]*pokerPlayer, - lastRaisePlayerIdx, playerAlive *int, playerToPlayIdx int, autoAction autoAction, + lastBetPlayerIdx, lastRaisePlayerIdx, playerAlive *int, playerToPlayIdx int, autoAction autoAction, autoCache map[database.UserID]autoAction) (actionResult int) { pUserID := p.userID roomUserTopic := g.roomID.UserTopic(pUserID) if autoAction.action > NoAction { time.Sleep(500 * time.Millisecond) - actionResult = handlePlayerActionEvent(g, p, minBet, newlyAllInPlayers, lastRaisePlayerIdx, playerAlive, playerToPlayIdx, autoAction.evt) + actionResult = handlePlayerActionEvent(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerAlive, playerToPlayIdx, autoAction.evt) } delete(autoCache, pUserID) setAutoAction(g, roomUserTopic, "") return actionResult } -func handlePlayerActionEvent(g *PokerGame, p *pokerPlayer, minBet *database.PokerChip, +func handlePlayerActionEvent(g *PokerGame, p *pokerPlayer, newlyAllInPlayers *[]*pokerPlayer, - lastRaisePlayerIdx, playerAlive *int, playerToPlayIdx int, evt playerEvent) (actionResult int) { + lastBetPlayerIdx, lastRaisePlayerIdx, playerAlive *int, playerToPlayIdx int, evt playerEvent) (actionResult int) { p.lastActionTS = time.Now() if evt.Fold { - actionResult = doFold(g, p, minBet, playerAlive) + actionResult = doFold(g, p, playerAlive) } else if evt.Check { - actionResult = doCheck(g, p, minBet) + actionResult = doCheck(g, p) } else if evt.Call { - actionResult = doCall(g, p, minBet, newlyAllInPlayers) + actionResult = doCall(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx) } else if evt.AllIn { - actionResult = doAllIn(g, p, minBet, newlyAllInPlayers, lastRaisePlayerIdx, playerToPlayIdx) + actionResult = doAllIn(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx) + } else if evt.Raise { + actionResult = doRaise(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx, evt) } else if evt.Bet > 0 { - actionResult = doBet(g, p, minBet, newlyAllInPlayers, lastRaisePlayerIdx, playerToPlayIdx, evt) + actionResult = doBet(g, p, newlyAllInPlayers, lastBetPlayerIdx, lastRaisePlayerIdx, playerToPlayIdx, evt) } else { actionResult = continueGetPlayerEventLoop } @@ -882,6 +963,8 @@ func handlePlayerActionEvent(g *PokerGame, p *pokerPlayer, minBet *database.Poke // Return either or not the game ended because only 1 player left playing (or none) func execBettingRound(g *PokerGame, skip int, minBet database.PokerChip) bool { + g.ongoing.MinBet.Set(minBet) + g.ongoing.MinRaise.Set(g.PokerTableMinBet) db := g.db ongoing := g.ongoing roomID := g.roomID @@ -889,9 +972,18 @@ func execBettingRound(g *PokerGame, skip int, minBet database.PokerChip) bool { _, dealerIdx := ongoing.getPlayerBySeatIdx(int(g.dealerSeatIdx.Load())) playerToPlayIdx := (dealerIdx + skip) % len(ongoing.players) lastRaisePlayerIdx := -1 + lastBetPlayerIdx := -1 newlyAllInPlayers := make([]*pokerPlayer, 0) autoCache := make(map[database.UserID]autoAction) + for _, p := range ongoing.players { + p.hasChecked = false + if p.canBet() { + p.status.Set("") + } + } + PokerPubSub.Pub(roomTopic, RedrawSeatsEvent{}) + playerAlive := ongoing.countAlivePlayers() // Avoid asking for actions if only 1 player can do so (because others are all-in) @@ -912,16 +1004,19 @@ RoundIsSettledLoop: pUserID := p.userID roomUserTopic := roomID.UserTopic(pUserID) - if playerToPlayIdx == lastRaisePlayerIdx { + if playerToPlayIdx == lastBetPlayerIdx { break AllPlayersLoop } - if lastRaisePlayerIdx == -1 { - lastRaisePlayerIdx = playerToPlayIdx - } + lastRaisePlayerIdx = utils.Ternary(lastRaisePlayerIdx == -1, playerToPlayIdx, lastRaisePlayerIdx) + lastBetPlayerIdx = utils.Ternary(lastBetPlayerIdx == -1, playerToPlayIdx, lastBetPlayerIdx) if !p.canBet() { continue AllPlayersLoop } + minBet = g.ongoing.MinBet.Get() + + PokerPubSub.Pub(roomUserTopic, RefreshButtonsEvent{}) + setWaitTurn(g, p.seatIdx) PokerPubSub.Pub(roomUserTopic, PokerYourTurnEvent{}) @@ -933,14 +1028,14 @@ RoundIsSettledLoop: actionResult := doNothing // Check for pre-selected action if autoActionVal, ok := autoCache[pUserID]; ok { - actionResult = applyAutoAction(g, p, &minBet, &newlyAllInPlayers, - &lastRaisePlayerIdx, &playerAlive, playerToPlayIdx, autoActionVal, autoCache) + actionResult = applyAutoAction(g, p, &newlyAllInPlayers, + &lastBetPlayerIdx, &lastRaisePlayerIdx, &playerAlive, playerToPlayIdx, autoActionVal, autoCache) goto checkActionResult } select { case evt = <-g.playersEventCh: case <-waitCh: // Waited too long, either auto-check or auto-fold - actionResult = doTimeout(g, p, &minBet, &playerAlive) + actionResult = doTimeout(g, p, &playerAlive) goto checkActionResult } if evt.Unsit { @@ -951,8 +1046,8 @@ RoundIsSettledLoop: actionResult = handleAutoActionReceived(g, autoCache, evt) goto checkActionResult } - actionResult = handlePlayerActionEvent(g, p, &minBet, &newlyAllInPlayers, - &lastRaisePlayerIdx, &playerAlive, playerToPlayIdx, evt) + actionResult = handlePlayerActionEvent(g, p, &newlyAllInPlayers, + &lastBetPlayerIdx, &lastRaisePlayerIdx, &playerAlive, playerToPlayIdx, evt) goto checkActionResult checkActionResult: @@ -966,6 +1061,7 @@ RoundIsSettledLoop: break RoundIsSettledLoop } PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: ""}) + PokerPubSub.Pub(roomTopic, RedrawSeatsEvent{}) break GetPlayerEventLoop } // End of repeat until we get an event from the player we're interested in } // End of repeat until all players have played @@ -999,6 +1095,7 @@ RoundIsSettled: PokerPubSub.Pub(roomTopic, PokerMainPotUpdatedEvent{MainPot: mainPot}) ongoing.setMainPot(mainPot) + g.ongoing.hasBet.Set(false) return playerAlive <= 1 } @@ -1018,10 +1115,10 @@ func refundUncalledBet(db *database.DkfDB, ongoing *ongoingGame, pokerTableID in } newArray := make([]*pokerPlayer, lenPlayers) copy(newArray, ongoing.players) - sort.Slice(newArray, func(i, j int) bool { return newArray[i].getBet() > newArray[j].getBet() }) + sort.Slice(newArray, func(i, j int) bool { return newArray[i].GetBet() > newArray[j].GetBet() }) firstPlayer := newArray[0] secondPlayer := newArray[1] - diff := firstPlayer.getBet() - secondPlayer.getBet() + diff := firstPlayer.GetBet() - secondPlayer.GetBet() if diff > 0 { firstPlayer.refundPartialBet(db, pokerTableID, diff) PokerPubSub.Pub(roomTopic, RedrawSeatsEvent{}) @@ -1039,8 +1136,8 @@ type Seat struct { // Positions of the dealer token for each seats var dealerTokenPos = [][]int{ - {125, 714}, - {246, 732}, + {140, 714}, + {261, 732}, {384, 607}, {369, 379}, {367, 190}, @@ -1136,13 +1233,14 @@ func computeAllInMaxGain(ongoing *ongoingGame, newlyAllInPlayers []*pokerPlayer, for _, p := range newlyAllInPlayers { maxGain := mainPot for _, op := range ongoing.players { - maxGain += utils.MinInt(op.getBet(), p.getBet()) + maxGain += utils.MinInt(op.GetBet(), p.GetBet()) } p.allInMaxGain = maxGain } } func dealerThread(g *PokerGame, eligiblePlayers seatedPlayers) { + eligiblePlayers.resetStatuses() g.ongoing = newOngoing(eligiblePlayers) roomID := g.roomID @@ -1174,6 +1272,7 @@ func dealerThread(g *PokerGame, eligiblePlayers seatedPlayers) { applyBigBlindBet(g, bigBlindBet, bbIdx) time.Sleep(animationTime) + g.ongoing.hasBet.Set(true) // Deal players cards dealPlayersCards(g, seats, &idx) @@ -1490,6 +1589,13 @@ func cardToPokerCard(name string) string { return r.Replace(name) } +func (g *PokerGame) OngoingPlayer(userID database.UserID) *pokerPlayer { + if g.ongoing != nil { + return g.ongoing.players.get(userID) + } + return nil +} + func (g *PokerGame) Deal(userID database.UserID) { roomTopic := g.roomID.Topic() roomUserTopic := g.roomID.UserTopic(userID) @@ -1571,6 +1677,7 @@ func BuildPayloadHtml(g *PokerGame, authUser *database.User, payload any) (html html += drawGameIsOverHtml(g) case PlayerBetEvent: html += drawPlayerBetEvent(evt) + html += drawSeatsStyle(authUser, g) case ErrorMsgEvent: html += drawErrorMsgEvent(evt) case AutoActionEvent: @@ -1703,18 +1810,13 @@ func buildActionsDiv(roomID RoomID) (html string) { <td style="vertical-align: top;" rowspan="2"> <iframe src="/poker/{{ .RoomID }}/bet" id="betBtn"></iframe> </td> - <td> - <iframe src="/poker/{{ .RoomID }}/call" id="callBtn"></iframe> - <iframe src="/poker/{{ .RoomID }}/check" id="checkBtn"></iframe> - <iframe src="/poker/{{ .RoomID }}/fold" id="foldBtn"></iframe> - </td> </tr> <tr> <td></td> <td><div id="autoAction"></div></td> </tr> <tr> - <td colspan="3"><div id="errorMsg"></div></td> + <td colspan="2"><div id="errorMsg"></div></td> </tr> </table>` data := map[string]any{ @@ -1741,6 +1843,7 @@ func buildSeatsHtml(g *PokerGame, authUser *database.User) (html string) { html += ` <iframe src="/poker/` + g.roomID.String() + `/sit/` + idxStr + `" class="takeSeat takeSeat` + idxStr + `"></iframe>` html += ` <div class="inner"></div>` html += ` <div id="seat` + idxStr + `_cash" class="cash"></div>` + html += ` <div id="seat` + idxStr + `_status" class="status"></div>` html += `</div>` } html += `</div>` @@ -1766,15 +1869,17 @@ func drawSeatsStyle(authUser *database.User, g *PokerGame) string { } html += fmt.Sprintf(`#seat%s .inner:before { content: "%s"; }`, idxStr, pUsername.String()) html += drawSeatCashLabel(idxStr, itoa2(p.getCash())) + html += drawSeatStatusLabel(idxStr, p.getStatus()) if ongoing != nil { - if op := ongoing.players.get(pUserID); op != nil && op.getBet() > 0 { - html += drawSeatPotLabel(idxStr, itoa2(op.getBet())) + if op := ongoing.players.get(pUserID); op != nil && op.GetBet() > 0 { + html += drawSeatPotLabel(idxStr, itoa2(op.GetBet())) } } } else { html += fmt.Sprintf(`#seat%s { border: 1px solid #333; }`, idxStr) html += fmt.Sprintf(`#seat%s .inner:before { content: ""; }`, idxStr) html += drawSeatCashLabel(idxStr, "") + html += drawSeatStatusLabel(idxStr, "") } } }) @@ -1790,6 +1895,10 @@ func drawSeatCashLabel(seatIdxStr, cashStr string) string { return fmt.Sprintf(`#seat%s_cash:before { content: "%s"; }`, seatIdxStr, cashStr) } +func drawSeatStatusLabel(seatIdxStr, statusStr string) string { + return fmt.Sprintf(`#seat%s_status:before { content: "%s"; }`, seatIdxStr, statusStr) +} + func drawAutoActionMsgEvent(evt AutoActionEvent) (html string) { display := utils.Ternary(evt.Message != "", "block", "none") html += fmt.Sprintf(`<style>#autoAction { display: %s; } #autoAction:before { content: "%s"; }</style>`, display, evt.Message) @@ -2116,8 +2225,8 @@ body { border:1px solid black; } .takeSeat { - width: 33px; - height: 25px; + width: 65px; + height: 40px; display: flex; margin-left: auto; margin-right: auto; @@ -2129,11 +2238,12 @@ body { background-color: rgba(45, 45, 45, 0.4); padding: 1px 2px; min-width: 80px; - min-height: 35px; + min-height: 50px; color: #ddd; } .seat .inner { display: flex; justify-content: center; } .seat .cash { display: flex; justify-content: center; } +.seat .status { display: flex; justify-content: center; } .dev_seat1_card1 { top: 55px; left: 610px; transform: rotateZ(-95deg); width:50px; height:70px; background-color: white; position: absolute; } .dev_seat1_card2 {} @@ -2181,20 +2291,20 @@ body { .takeSeat4 { } .takeSeat5 { } .takeSeat6 { } -#actionsDiv { position: absolute; top: 470px; left: 130px; } +#actionsDiv { position: absolute; top: 470px; left: 100px; } #dealBtn { width: 80px; height: 30px; display: inline-block; vertical-align: top; } #unSitBtn { width: 80px; height: 30px; display: inline-block; vertical-align: top; } #checkBtn { width: 60px; height: 30px; display: inline-block; vertical-align: top; } #foldBtn { width: 50px; height: 30px; display: inline-block; vertical-align: top; } #callBtn { width: 50px; height: 30px; display: inline-block; vertical-align: top; } -#betBtn { width: 145px; height: 70px; display: inline-block; vertical-align: top; } +#betBtn { width: 300px; height: 100px; display: inline-block; vertical-align: top; } .countdown { --duration: ` + itoa(MaxUserCountdown) + `; --size: 30; } #countdown1 { top: 46px; left: 717px; position: absolute; display: none; z-index: 100; } #countdown2 { top: 167px; left: 729px; position: absolute; display: none; z-index: 100; } -#countdown3 { top: 405px; left: 668px; position: absolute; display: none; z-index: 100; } +#countdown3 { top: 420px; left: 668px; position: absolute; display: none; z-index: 100; } #countdown4 { top: 404px; left: 375px; position: absolute; display: none; z-index: 100; } #countdown5 { top: 404px; left: 185px; position: absolute; display: none; z-index: 100; } -#countdown6 { top: 404px; left: 59px; position: absolute; display: none; z-index: 100; } +#countdown6 { top: 419px; left: 59px; position: absolute; display: none; z-index: 100; } #mainPot { position: absolute; top: 220px; left: 250px; font-size: 20px; font-family: Arial,Helvetica,sans-serif; } #winner { position: absolute; top: 265px; left: 250px; } #errorMsg { @@ -2222,7 +2332,7 @@ body { #chat-top-bar { height: 57px; width: 100%; background-color: #222; } #chat-content { height: 193px; width: 100%; background-color: #222; } #eventLogs { position: absolute; bottom: 0px; right: 0px; width: 243px; height: 250px; background-color: #444; } -#dealerToken { top: 125px; left: 714px; width: 20px; height: 20px; background-color: #ccc; border: 1px solid #333; border-radius: 11px; position: absolute; } +#dealerToken { top: 140px; left: 714px; width: 20px; height: 20px; background-color: #ccc; border: 1px solid #333; border-radius: 11px; position: absolute; } #dealerToken .inner { padding: 2px 4px; } #dealerToken .inner:before { content: "D"; } #soundsStatus { diff --git a/pkg/web/middlewares/middlewares.go b/pkg/web/middlewares/middlewares.go @@ -35,6 +35,7 @@ var GzipMiddleware = middleware.GzipWithConfig( c.Path() == "/chess/:key/analyze" || c.Path() == "/poker/:roomID/stream" || c.Path() == "/poker/:roomID/logs" || + c.Path() == "/poker/:roomID/bet" || c.Path() == "/api/v1/chat/messages/:roomName/stream" || c.Path() == "/uploads/:filename" || c.Path() == "/" { @@ -184,9 +185,6 @@ func CSRFMiddleware() echo.MiddlewareFunc { c.Path() == "/chess/:key" || c.Path() == "/poker/:roomID/sit/:pos" || c.Path() == "/poker/:roomID/unsit" || - c.Path() == "/poker/:roomID/check" || - c.Path() == "/poker/:roomID/fold" || - c.Path() == "/poker/:roomID/call" || c.Path() == "/poker/:roomID/bet" || c.Path() == "/poker/:roomID/logs" || c.Path() == "/poker/:roomID/deal" @@ -302,9 +300,6 @@ func IsAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { !strings.Contains(c.Path(), "/poker/:roomID/stream") && !strings.Contains(c.Path(), "/poker/:roomID/sit/:pos") && !strings.Contains(c.Path(), "/poker/:roomID/unsit") && - !strings.Contains(c.Path(), "/poker/:roomID/check") && - !strings.Contains(c.Path(), "/poker/:roomID/fold") && - !strings.Contains(c.Path(), "/poker/:roomID/call") && !strings.Contains(c.Path(), "/poker/:roomID/bet") && !strings.Contains(c.Path(), "/poker/:roomID/logs") && !strings.Contains(c.Path(), "/poker/:roomID/deal") { diff --git a/pkg/web/web.go b/pkg/web/web.go @@ -102,12 +102,6 @@ func getMainServer(db *database.DkfDB, i18nBundle *i18n.Bundle, renderer *tmp.Te authGroup.GET("/poker/:roomID", handlers.PokerTableHandler) authGroup.GET("/poker/:roomID/stream", handlers.PokerStreamHandler) authGroup.GET("/poker/:roomID/logs", handlers.PokerLogsHandler) - authGroup.GET("/poker/:roomID/check", handlers.PokerCheckHandler) - authGroup.POST("/poker/:roomID/check", handlers.PokerCheckHandler) - authGroup.GET("/poker/:roomID/fold", handlers.PokerFoldHandler) - authGroup.POST("/poker/:roomID/fold", handlers.PokerFoldHandler) - authGroup.GET("/poker/:roomID/call", handlers.PokerCallHandler) - authGroup.POST("/poker/:roomID/call", handlers.PokerCallHandler) authGroup.GET("/poker/:roomID/bet", handlers.PokerBetHandler) authGroup.POST("/poker/:roomID/bet", handlers.PokerBetHandler) authGroup.GET("/poker/:roomID/deal", handlers.PokerDealHandler)