dkforest

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

commit 9a4c6ad590a9ea0eabdcfbf7b6702da583096ab4
parent b234b2b594f569c7de2d27192cade3570385ed9b
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Tue,  5 Dec 2023 21:03:00 -0500

cleanup

Diffstat:
Dpkg/web/handlers/poker.go | 1162-------------------------------------------------------------------------------
Apkg/web/handlers/poker/events.go | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apkg/web/handlers/poker/poker.go | 1150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/web.go | 31++++++++++++++++---------------
4 files changed, 1225 insertions(+), 1177 deletions(-)

diff --git a/pkg/web/handlers/poker.go b/pkg/web/handlers/poker.go @@ -1,1162 +0,0 @@ -package handlers - -import ( - "dkforest/pkg/database" - "dkforest/pkg/pubsub" - "dkforest/pkg/utils" - hutils "dkforest/pkg/web/handlers/utils" - "errors" - "fmt" - "github.com/labstack/echo" - "net/http" - "strconv" - "strings" - "sync" - "time" -) - -const NbPlayers = 6 -const MaxUserCountdown = 10 -const MinTimeAfterGame = 10 -const BackfacingDeg = "-180deg" -const BurnStackX = 400 -const BurnStackY = 30 -const DealerStackX = 250 -const DealerStackY = 30 - -type Poker struct { - sync.Mutex - games map[string]*PokerGame -} - -func NewPoker() *Poker { - p := &Poker{} - p.games = make(map[string]*PokerGame) - return p -} - -func (p *Poker) GetOrCreateGame(roomID string) *PokerGame { - p.Lock() - defer p.Unlock() - g, found := p.games[roomID] - if !found { - g = &PokerGame{ - PlayersEventCh: make(chan PlayerEvent), - Players: make([]*PokerStandingPlayer, NbPlayers), - } - p.games[roomID] = g - } - return g -} - -func (p *Poker) GetGame(roomID string) *PokerGame { - p.Lock() - defer p.Unlock() - g, found := PokerInstance.games[roomID] - if !found { - return nil - } - return g -} - -type PlayerEvent struct { - Player string - Call bool - Check bool - Fold bool - Bet int -} - -var PokerInstance = NewPoker() - -type Ongoing struct { - Deck []string - Players []*PokerPlayer - Events []PokerEvent - WaitTurnEvent PokerWaitTurnEvent - MainPot int -} - -type PokerStandingPlayer struct { - Username string - Cash int -} - -type PokerPlayer struct { - Username string - Bet int - Cash int - Cards []PlayerCard - Folded bool -} - -type PlayerCard struct { - Idx int - Name string -} - -type PokerGame struct { - sync.Mutex - PlayersEventCh chan PlayerEvent - Players []*PokerStandingPlayer - Ongoing *Ongoing - DealerIdx int - IsGameDone bool - IsGameOver bool -} - -func (g *Ongoing) GetDeckStr() string { - return g.getDeckStr() -} - -func (g *Ongoing) getDeckStr() string { - return strings.Join(g.Deck, "") -} - -func (g *Ongoing) GetDeckHash() string { - return utils.MD5([]byte(g.getDeckStr())) -} - -func (g *Ongoing) GetPlayer(player string) *PokerPlayer { - for _, p := range g.Players { - if p != nil && p.Username == player { - return p - } - } - return nil -} - -func isRoundSettled(players []*PokerPlayer) bool { - allSettled := true - b := -1 - for _, p := range players { - if p != nil { - if p.Folded { - continue - } - if p.Cash == 0 { // all in - continue - } - if b == -1 { - b = p.Bet - } else { - if p.Bet != b { - allSettled = false - break - } - } - } - } - return allSettled -} - -func (g *PokerGame) SitPlayer(authUser *database.User, pos int) error { - if g.Players[pos] != nil { - return errors.New("seat already taken") - } - g.Players[pos] = &PokerStandingPlayer{Username: authUser.Username.String(), Cash: 1000} - return nil -} - -func (g *PokerGame) Deal(roomID string) { - roomTopic := "room_" + roomID - - if g.Ongoing != nil { - if !g.IsGameOver { - fmt.Println("game already ongoing") - return - } else { - g.IsGameOver = false - PokerPubSub.Pub(roomTopic, ResetCardsEvent{}) - time.Sleep(time.Second) - } - } - if g.CountSeated() < 2 { - fmt.Println("need at least 2 players") - return - } - deck := []string{ - "A♠", "2♠", "3♠", "4♠", "5♠", "6♠", "7♠", "8♠", "9♠", "10♠", "J♠", "Q♠", "K♠", - "A♥", "2♥", "3♥", "4♥", "5♥", "6♥", "7♥", "8♥", "9♥", "10♥", "J♥", "Q♥", "K♥", - "A♣", "2♣", "3♣", "4♣", "5♣", "6♣", "7♣", "8♣", "9♣", "10♣", "J♣", "Q♣", "K♣", - "A♦", "2♦", "3♦", "4♦", "5♦", "6♦", "7♦", "8♦", "9♦", "10♦", "J♦", "Q♦", "K♦", - } - utils.Shuffle(deck) - - players := make([]*PokerPlayer, NbPlayers) - for idx := range g.Players { - var player *PokerPlayer - if g.Players[idx] != nil { - player = &PokerPlayer{Username: g.Players[idx].Username, Cash: g.Players[idx].Cash} - } - players[idx] = player - } - - g.Ongoing = &Ongoing{Deck: deck, Players: players, WaitTurnEvent: PokerWaitTurnEvent{Idx: -1}} - - go func() { - waitPlayersActionFn := func() bool { - lastRisePlayerIdx := -1 - minBet := 0 - - playerAlive := 0 - for _, p := range g.Ongoing.Players { - if p != nil && !p.Folded { - playerAlive++ - } - } - - // TODO: implement maximum re-rise - OUTER: - for { // Loop until the round is settled - for i, p := range g.Ongoing.Players { - if p == nil { - continue - } - if i == lastRisePlayerIdx { - break OUTER - } - player := g.Ongoing.GetPlayer(p.Username) - if player.Folded { - continue - } - evt := PokerWaitTurnEvent{Idx: i} - PokerPubSub.Pub(roomTopic, evt) - g.Ongoing.WaitTurnEvent = evt - - // Maximum time allowed for the player to send his action - waitCh := time.After(MaxUserCountdown * time.Second) - LOOP: - for { // Repeat until we get an event from the player we're interested in - var evt PlayerEvent - - select { - case evt = <-g.PlayersEventCh: - case <-waitCh: - // Waited too long, either auto-check or auto-fold - if p.Cash == 0 { // all-in - break LOOP - } - if p.Bet < minBet { - player.Folded = true - PokerPubSub.Pub(roomTopic, PlayerFoldEvent{Card1Idx: player.Cards[0].Idx, Card2Idx: player.Cards[1].Idx}) - playerAlive-- - if playerAlive == 1 { - break OUTER - } - } - break LOOP - } - - if evt.Player != p.Username { - continue - } - roomUserTopic := "room_" + roomID + "_" + p.Username - if evt.Fold { - player.Folded = true - PokerPubSub.Pub(roomTopic, PlayerFoldEvent{Card1Idx: player.Cards[0].Idx, Card2Idx: player.Cards[1].Idx}) - playerAlive-- - if playerAlive == 1 { - break OUTER - } - } else if evt.Check { - if p.Bet < minBet { - msg := fmt.Sprintf("Need to bet %d", minBet-p.Bet) - PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) - continue - } - } else if evt.Call { - bet := minBet - p.Bet - if p.Cash < bet { - bet = p.Cash - p.Bet += bet - p.Cash = 0 - // All in - } else { - p.Bet += bet - p.Cash -= bet - } - PokerPubSub.Pub(roomTopic, PlayerBetEvent{PlayerIdx: i, Player: p.Username, Bet: bet, TotalBet: p.Bet, Cash: p.Cash}) - } else if evt.Bet > 0 { - if (p.Bet + evt.Bet) < minBet { - msg := fmt.Sprintf("Bet (%d) is too low. Must bet at least %d", evt.Bet, minBet-p.Bet) - PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) - continue - } - if (p.Bet + evt.Bet) > minBet { - lastRisePlayerIdx = i - } - minBet = p.Bet + evt.Bet - p.Bet += evt.Bet - p.Cash -= evt.Bet - PokerPubSub.Pub(roomTopic, PlayerBetEvent{PlayerIdx: i, Player: p.Username, Bet: evt.Bet, TotalBet: p.Bet, Cash: p.Cash}) - } - break - } - } - - // All settle when all players have the same bet amount - if isRoundSettled(g.Ongoing.Players) { - break - } - } - - // Transfer players bets into the main pot - for i := range g.Ongoing.Players { - if g.Ongoing.Players[i] != nil { - g.Ongoing.MainPot += g.Ongoing.Players[i].Bet - g.Ongoing.Players[i].Bet = 0 - } - } - - evt := PokerWaitTurnEvent{Idx: -1, MainPot: g.Ongoing.MainPot} - PokerPubSub.Pub(roomTopic, evt) - g.Ongoing.WaitTurnEvent = evt - - return playerAlive == 1 - } - - type Seat struct { - Top int - Left int - Angle string - Top2 int - Left2 int - } - seats := []Seat{ - {Top: 50, Left: 600, Top2: 50 + 5, Left2: 600 + 5, Angle: "-90deg"}, - {Top: 150, Left: 574, Top2: 150 + 5, Left2: 574 + 3, Angle: "-80deg"}, - {Top: 250, Left: 530, Top2: 250 + 5, Left2: 530 + 1, Angle: "-70deg"}, - } - - var card string - idx := 0 - - burnCard := func(pos int) { - card = g.Ongoing.Deck[idx] - idx++ - evt := PokerEvent{ - ID: "card" + itoa(idx), - Name: "", - Idx: idx, - Top: BurnStackY + (pos * 2), - Left: BurnStackX + (pos * 4), - } - PokerPubSub.Pub(roomTopic, evt) - g.Ongoing.Events = append(g.Ongoing.Events, evt) - } - - dealCard := func(pos int) { - card = g.Ongoing.Deck[idx] - idx++ - evt := PokerEvent{ - ID: "card" + itoa(idx), - Name: card, - Idx: idx, - Top: 150, - Left: 100 + (pos * 55), - Reveal: true, - } - PokerPubSub.Pub(roomTopic, evt) - g.Ongoing.Events = append(g.Ongoing.Events, evt) - } - - deckHash := utils.MD5([]byte(strings.Join(g.Ongoing.Deck, ""))) - PokerPubSub.Pub(roomTopic, GameStartedEvent{DeckHash: deckHash}) - - // Deal cards - for j := 1; j <= 2; j++ { - for i, p := range g.Ongoing.Players { - if p == nil { - continue - } - if p.Cash == 0 { - continue - } - d := seats[i] - time.Sleep(time.Second) - card = g.Ongoing.Deck[idx] - idx++ - name := "" - left := d.Left - top := d.Top - if j == 2 { - left = d.Left2 - top = d.Top2 - } - evt := PokerEvent{ - ID: "card" + itoa(idx), - Name: name, - Idx: idx, - Top: top, - Left: left, - Angle: d.Angle, - } - g.Ongoing.Players[i].Cards = append(g.Ongoing.Players[i].Cards, PlayerCard{Idx: idx, Name: card}) - PokerPubSub.Pub(roomTopic, evt) - PokerPubSub.Pub(roomTopic+"_"+p.Username, YourCardEvent{Idx: j, Name: card}) - g.Ongoing.Events = append(g.Ongoing.Events, evt) - } - } - - // Wait for players to bet/call/check/fold... - time.Sleep(time.Second) - if waitPlayersActionFn() { - goto END - } - - // Burn - time.Sleep(time.Second) - burnCard(0) - - // Flop (3 first cards) - for i := 1; i <= 3; i++ { - time.Sleep(time.Second) - dealCard(i) - } - - // Wait for players to bet/call/check/fold... - time.Sleep(time.Second) - if waitPlayersActionFn() { - goto END - } - - // Burn - time.Sleep(time.Second) - burnCard(1) - - // Turn (4th card) - time.Sleep(time.Second) - dealCard(4) - - // Wait for players to bet/call/check/fold... - time.Sleep(time.Second) - if waitPlayersActionFn() { - goto END - } - - // Burn - time.Sleep(time.Second) - burnCard(2) - - // River (5th card) - time.Sleep(time.Second) - dealCard(5) - - // Wait for players to bet/call/check/fold... - time.Sleep(time.Second) - if waitPlayersActionFn() { - goto END - } - - // Show cards - for _, p := range g.Ongoing.Players { - if p != nil && !p.Folded { - fmt.Println(p.Username, p.Cards) - PokerPubSub.Pub(roomTopic, ShowCardsEvent{Cards: p.Cards}) - } - } - - END: - - // TODO: evaluate hands, and crown winner - var winner *PokerPlayer - for _, p := range g.Ongoing.Players { - if p != nil { - winner = p - break - } - } - winner.Cash += g.Ongoing.MainPot - g.Ongoing.MainPot = 0 - - // Sync "ongoing players" with "room players" objects - for idx := range g.Players { - if g.Ongoing.Players[idx] != nil && g.Players[idx] != nil { - g.Players[idx].Cash = g.Ongoing.Players[idx].Cash - } - } - - g.IsGameDone = true - PokerPubSub.Pub(roomTopic, GameIsDoneEvent{DeckStr: strings.Join(g.Ongoing.Deck, "")}) - - // Wait a minimum of X seconds before allowing a new game - time.Sleep(MinTimeAfterGame * time.Second) - g.IsGameOver = true - fmt.Println("GAME IS OVER") - }() -} - -func (g *PokerGame) CountSeated() (count int) { - for _, p := range g.Players { - if p != nil { - count++ - } - } - return -} - -func (g *PokerGame) IsSeated(player string) (bool, int) { - isSeated := false - pos := 0 - for idx, p := range g.Players { - if p != nil && p.Username == player { - isSeated = true - pos = idx + 1 - break - } - } - return isSeated, pos -} - -type PokerEvent struct { - ID string - Idx int - Name string - Top int - Left int - Reveal bool - Angle string -} - -type GameStartedEvent struct { - DeckHash string -} - -type GameIsDoneEvent struct { - DeckStr string -} - -type ResetCardsEvent struct { -} - -type PlayerBetEvent struct { - PlayerIdx int - Player string - Bet int - TotalBet int - Cash int -} - -type ErrorMsgEvent struct { - Message string -} - -type PlayerFoldEvent struct { - Card1Idx, Card2Idx int -} - -type PokerWaitTurnEvent struct { - Idx int - MainPot int -} - -type YourCardEvent struct { - Idx int - Name string -} - -type ShowCardsEvent struct { - Cards []PlayerCard -} - -type PokerSeatTakenEvent struct { -} - -type PokerSeatLeftEvent struct { - Idx int -} - -var PokerPubSub = pubsub.NewPubSub[any]() - -func PokerCheckHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := c.Param("roomID") - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - select { - case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Check: true}: - default: - } - } - return c.HTML(http.StatusOK, `<form method="post"><button>Check</button></form>`) -} - -func PokerBetHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := c.Param("roomID") - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - bet := 100 - if c.Request().Method == http.MethodPost { - bet, _ = strconv.Atoi(c.Request().PostFormValue("bet")) - select { - case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Bet: bet}: - default: - } - } - return c.HTML(http.StatusOK, `<form method="post"><input type="number" name="bet" value="`+itoa(bet)+`" style="width: 90px;" /><button>Bet</button></form>`) -} - -func PokerCallHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := c.Param("roomID") - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - select { - case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Call: true}: - default: - } - } - return c.HTML(http.StatusOK, `<form method="post"><button>Call</button></form>`) -} - -func PokerFoldHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := c.Param("roomID") - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - select { - case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Fold: true}: - default: - } - } - return c.HTML(http.StatusOK, `<form method="post"><button>Fold</button></form>`) -} - -func PokerDealHandler(c echo.Context) error { - roomID := c.Param("roomID") - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - g.Deal(roomID) - } - return c.HTML(http.StatusOK, `<form method="post"><button>Deal</button></form>`) -} - -func PokerUnSitHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - roomID := c.Param("roomID") - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.NoContent(http.StatusNotFound) - } - if c.Request().Method == http.MethodPost { - var idx int - found := false - for i, p := range g.Players { - if p != nil && p.Username == authUser.Username.String() { - g.Players[i] = nil - idx = i - found = true - break - } - } - if found { - myTopic := "room_" + roomID - PokerPubSub.Pub(myTopic, PokerSeatLeftEvent{Idx: idx + 1}) - } - } - return c.HTML(http.StatusOK, `<form method="post"><button>UnSit</button></form>`) -} - -func PokerSitHandler(c echo.Context) error { - html := `<form method="post"><button>SIT</button></form>` - authUser := c.Get("authUser").(*database.User) - pos, _ := strconv.Atoi(c.Param("pos")) - if pos < 1 || pos > NbPlayers { - return c.HTML(http.StatusOK, html) - } - pos-- - roomID := c.Param("roomID") - roomTopic := "room_" + roomID - g := PokerInstance.GetGame(roomID) - if g == nil { - return c.HTML(http.StatusOK, html) - } - if c.Request().Method == http.MethodPost { - if err := g.SitPlayer(authUser, pos); err != nil { - fmt.Println(err) - return c.HTML(http.StatusOK, html) - } - PokerPubSub.Pub(roomTopic, PokerSeatTakenEvent{}) - } - return c.HTML(http.StatusOK, html) -} - -func isHeartOrDiamond(name string) bool { - return strings.Contains(name, "♥") || - strings.Contains(name, "♦") -} - -func colorForCard(name string) string { - color := "black" - if isHeartOrDiamond(name) { - color = "darkred" - } - return color -} - -func colorForCard1(name string) string { - color := "black" - if isHeartOrDiamond(name) { - color = "red" - } - return color -} - -func buildYourCardsHtml(authUser *database.User, g *PokerGame) string { - html := `<div style="position: absolute; top: 450px; left: 200px;"><div id="yourCard1"></div><div id="yourCard2"></div></div>` - if g.Ongoing != nil { - cards := make([]PlayerCard, 0) - for _, p := range g.Ongoing.Players { - if p != nil && p.Username == authUser.Username.String() { - cards = p.Cards - break - } - } - html += `<style>` - if len(cards) >= 1 { - html += `#yourCard1:before { content: "` + cards[0].Name + `"; color: ` + colorForCard(cards[0].Name) + `; }` - } - if len(cards) == 2 { - html += `#yourCard2:before { content: "` + cards[1].Name + `"; color: ` + colorForCard(cards[1].Name) + `; }` - } - html += `</style>` - } - return html -} - -func buildCardsHtml() (html string) { - for i := 52; i >= 1; i-- { - html += `<div class="card-holder" id="card` + itoa(i) + `"><div class="back"></div><div class="card"><div class=inner></div></div></div>` - } - return -} - -func buildTakeSeatHtml(authUser *database.User, g *PokerGame, roomID string) string { - takeSeatBtns := "" - seated, _ := g.IsSeated(authUser.Username.String()) - for i, p := range g.Players { - takeSeatBtns += `<iframe src="/poker/` + roomID + `/sit/` + itoa(i+1) + `" class="takeSeat takeSeat` + itoa(i+1) + `"></iframe>` - if p != nil || seated { - takeSeatBtns += `<style>.takeSeat` + itoa(i+1) + ` { display: none; }</style>` - } - } - return takeSeatBtns -} - -func buildMainPotHtml(g *PokerGame) string { - html := `<div id="mainPot"></div>` - mainPot := 0 - if g.Ongoing != nil { - mainPot = g.Ongoing.MainPot - } - html += `<style>#mainPot:before { content: "Pot: ` + itoa(mainPot) + `"; }</style>` - return html -} - -func buildCountdownsHtml() (html string) { - for i := 1; i <= NbPlayers; i++ { - html += `<div id="countdown` + itoa(i) + `" class="timer" style="--duration: ` + itoa(MaxUserCountdown) + `;--size: 30;"><div class="mask"></div></div>` - } - return -} - -func buildActionsBtns(roomID string) string { - return ` -<div style="position: absolute; top: 400px; left: 200px;"> - <iframe src="/poker/` + roomID + `/bet" id="betBtn"></iframe> - <iframe src="/poker/` + roomID + `/call" id="callBtn"></iframe> - <iframe src="/poker/` + roomID + `/check" id="checkBtn"></iframe> - <iframe src="/poker/` + roomID + `/fold" id="foldBtn"></iframe> -</div>` -} - -func buildSeatsHtml(g *PokerGame) string { - seats := ` -<div>` - for i, p := range g.Players { - seats += `<div id="seat` + itoa(i+1) + `"></div>` - seats += `<div id="seat` + itoa(i+1) + `_cash"></div>` - if p != nil { - seats += `<style>#seat` + itoa(i+1) + `:before { content: "` + p.Username + `"; }</style>` - seats += `<style>#seat` + itoa(i+1) + `_cash:before { content: "` + itoa(p.Cash) + `"; }</style>` - } - } - seats += ` -</div>` - return seats -} - -func drawErrorMsgEvent(evt ErrorMsgEvent) string { - return `<style>#errorMsg:before { content: "` + evt.Message + `"; }</style>` -} - -func drawPlayerBetEvent(evt PlayerBetEvent) string { - return `<style>#seat` + itoa(evt.PlayerIdx+1) + `_cash:before { content: "` + itoa(evt.Cash) + `"; }</style>` -} - -func drawYourCardEvent(evt YourCardEvent) string { - return `<style>#yourCard` + itoa(evt.Idx) + `:before { content: "` + evt.Name + `"; color: ` + colorForCard(evt.Name) + `; }</style>` -} - -func drawGameStartedEvent(evt GameStartedEvent) string { - return `<style>#deckHash:before { content: "` + evt.DeckHash + `"; }</style>` -} - -func drawGameIsDoneHtml(g *PokerGame, evt GameIsDoneEvent) (html string) { - html += `<style>#deckStr:before { content: "` + evt.DeckStr + `"; }</style>` - for i, p := range g.Players { - if p != nil { - html += `<style>#seat` + itoa(i+1) + `_cash:before { content: "` + itoa(p.Cash) + `"; }</style>` - } - } - return -} - -func drawPlayerFoldEvent(evt PlayerFoldEvent) (html string) { - transition := `transition: 1s ease-in-out; transform: translateX(` + itoa(BurnStackX) + `px) translateY(` + itoa(BurnStackY) + `px) rotateY(` + BackfacingDeg + `);` - html = `<style>#card` + itoa(evt.Card1Idx) + ` { ` + transition + ` } - #card` + itoa(evt.Card2Idx) + ` { ` + transition + ` }</style>` - return -} - -func drawShowCardsEvent(evt ShowCardsEvent) (html string) { - // TODO: Proper showing cards - html += `<style>` - html += `#card` + itoa(evt.Cards[0].Idx) + ` { transition: 1s ease-in-out; transform: rotateY(0); }` - html += `#card` + itoa(evt.Cards[1].Idx) + ` { transition: 1s ease-in-out; transform: rotateY(0); }` - html += `#card` + itoa(evt.Cards[0].Idx) + ` .card .inner:before { content: "` + evt.Cards[0].Name + `"; color: ` + colorForCard(evt.Cards[0].Name) + `; }` - html += `#card` + itoa(evt.Cards[1].Idx) + ` .card .inner:before { content: "` + evt.Cards[1].Name + `"; color: ` + colorForCard(evt.Cards[1].Name) + `; }` - html += `</style>` - return -} - -func drawResetCardsEvent() (html string) { - html += `<style>` - for i := 1; i <= 52; i++ { - html += `#card` + itoa(i) + ` { transition: 1s ease-in-out; transform: translateX(` + itoa(DealerStackX) + `px) translateY(` + itoa(DealerStackY) + `px) rotateY(` + BackfacingDeg + `); } - #card` + itoa(i) + ` .card .inner:before { content: ""; }` - } - html += ` - #yourCard1:before { content: ""; } - #yourCard2:before { content: ""; } - #deckHash:before { content: ""; } - #deckStr:before { content: ""; } - #mainPot:before { content: "Pot: 0"; } - </style>` - return -} - -func drawSeatsHtml(authUser *database.User, g *PokerGame) string { - html := "<style>" - seated, _ := g.IsSeated(authUser.Username.String()) - for i, p := range g.Players { - if p != nil || seated { - html += `.takeSeat` + itoa(i+1) + ` { display: none; }` - } else { - html += `.takeSeat` + itoa(i+1) + ` { display: block; }` - } - if p != nil { - html += `#seat` + itoa(i+1) + `:before { content: "` + p.Username + `"; }` - html += `#seat` + itoa(i+1) + `_cash:before { content: "` + itoa(p.Cash) + `"; }` - } else { - html += `#seat` + itoa(i+1) + `:before { content: ""; }` - html += `#seat` + itoa(i+1) + `_cash:before { content: ""; }` - } - } - html += "</style>" - return html -} - -func drawCountDownStyle(evt PokerWaitTurnEvent) string { - html := "<style>" - html += `#countdown1, #countdown2, #countdown3, #countdown4, #countdown5, #countdown6 { display: none; }` - if evt.Idx == -1 { - html += `#mainPot:before { content: "Pot: ` + itoa(evt.MainPot) + `"; }` - } else if evt.Idx == 0 { - html += `#countdown1 { top: 50px; left: 600px; display: block; animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; }` - } else if evt.Idx == 1 { - html += `#countdown2 { top: 150px; left: 574px; display: block; animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; }` - } else if evt.Idx == 2 { - html += `#countdown3 { top: 250px; left: 530px; display: block; animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; }` - } - html += "</style>" - return html -} - -func getPokerEventHtml(payload PokerEvent, animationTime string) string { - transform := `transform: translate(` + itoa(payload.Left) + `px, ` + itoa(payload.Top) + `px)` - if payload.Angle != "" { - transform += ` rotateZ(` + payload.Angle + `)` - } - if !payload.Reveal { - transform += ` rotateY(` + BackfacingDeg + `)` - } - transform += ";" - pokerEvtHtml := `<style> -#` + payload.ID + ` { - z-index: ` + itoa(payload.Idx) + `; - transition: ` + animationTime + ` ease-in-out; - ` + transform + ` -} -#` + payload.ID + ` .card .inner:before { content: "` + payload.Name + `"; color: ` + colorForCard1(payload.Name) + `; } -</style>` - return pokerEvtHtml -} - -func itoa(i int) string { - return strconv.Itoa(i) -} - -var pokerCss = `<style> -html, body { height: 100%; width: 100%; } -body { - background:linear-gradient(135deg, #449144 33%,#008a00 95%); -} -.card-holder{ - position: absolute; - top: 0; - left: 0; - transform: translateX(` + itoa(DealerStackX) + `px) translateY(` + itoa(DealerStackY) + `px) rotateY(` + BackfacingDeg + `); - transform-style: preserve-3d; - backface-visibility: hidden; - width:50px; - height:70px; - display:inline-block; - box-shadow:1px 2px 2px rgba(0,0,0,.8); - margin:2px; -} -.card { - box-shadow: inset 2px 2px 0 #fff, inset -2px -2px 0 #fff; - transform-style: preserve-3d; - position:absolute; - top:0; - left:0; - bottom:0; - right:0; - backface-visibility: hidden; - background-color:#fcfcfc; - border-radius:2%; - display:block; - width:100%; - height:100%; - border:1px solid black; -} -.card .inner { - padding: 5px; - font-size: 25px; -} -.back{ - position:absolute; - top:0; - left:0; - bottom:0; - right:0; - width:100%; - height:100%; - backface-visibility: hidden; - transform: rotateY(` + BackfacingDeg + `); - background: #36c; - background: - linear-gradient(135deg, #f26c32 0%,#c146a1 50%,#a80077 51%,#f9703e 100%); - border-radius:2%; - box-shadow: inset 3px 3px 0 #fff, inset -3px -3px 0 #fff; - display:block; - border:1px solid black; -} -.takeSeat { - width: 40px; - height: 30px; -} -#seat1 { position: absolute; top: 80px; left: 700px; } -#seat2 { position: absolute; top: 200px; left: 670px; } -#seat3 { position: absolute; top: 300px; left: 610px; } -#seat1_cash { position: absolute; top: 100px; left: 700px; } -#seat2_cash { position: absolute; top: 220px; left: 670px; } -#seat3_cash { position: absolute; top: 320px; left: 610px; } -#seat1Pot { position: absolute; top: 80px; left: 500px; } -.takeSeat1 { position: absolute; top: 80px; left: 700px; } -.takeSeat2 { position: absolute; top: 200px; left: 670px; } -.takeSeat3 { position: absolute; top: 300px; left: 610px; } -.takeSeat4 { position: absolute; top: 300px; left: 550px; } -.takeSeat5 { position: absolute; top: 300px; left: 500px; } -.takeSeat6 { position: absolute; top: 300px; left: 450px; } -#dealBtn { position: absolute; top: 400px; left: 50px; width: 80px; height: 30px; } -#unSitBtn { position: absolute; top: 430px; left: 50px; width: 80px; height: 30px; } -#checkBtn { width: 60px; height: 30px; } -#foldBtn { width: 50px; height: 30px; } -#callBtn { width: 50px; height: 30px; } -#betBtn { width: 145px; height: 30px; } -#countdown1 { position: absolute; display: none; z-index: 100; } -#countdown2 { position: absolute; display: none; z-index: 100; } -#countdown3 { position: absolute; display: none; z-index: 100; } -#countdown4 { position: absolute; display: none; z-index: 100; } -#countdown5 { position: absolute; display: none; z-index: 100; } -#countdown6 { position: absolute; display: none; z-index: 100; } -#mainPot { position: absolute; top: 240px; left: 250px; } -#yourCard1 { font-size: 22px; display: inline-block; margin-right: 15px; } -#yourCard2 { font-size: 22px; display: inline-block; } -#errorMsg { position: absolute; top: 500px; left: 150px; color: darkred; } - - -.timer { - background: -webkit-linear-gradient(left, black 50%, #eee 50%); - border-radius: 100%; - height: calc(var(--size) * 1px); - width: calc(var(--size) * 1px); - position: relative; - animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; - -webkit-mask: radial-gradient(transparent 50%,#000 50%); - mask: radial-gradient(transparent 50%,#000 50%); -} -.mask { - border-radius: 100% 0 0 100% / 50% 0 0 50%; - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 50%; - animation: mask calc(var(--duration) * 1s) steps(500, start) forwards; - -webkit-transform-origin: 100% 50%; -} -@-webkit-keyframes time { - 100% { - -webkit-transform: rotate(360deg); - } -} -@-webkit-keyframes mask { - 0% { - background: #eee; - -webkit-transform: rotate(0deg); - } - 50% { - background: #eee; - -webkit-transform: rotate(-180deg); - } - 50.01% { - background: black; - -webkit-transform: rotate(0deg); - } - 100% { - background: black; - -webkit-transform: rotate(-180deg); - } -} - -</style>` - -func PokerHandler(c echo.Context) error { - roomID := c.Param("roomID") - g := PokerInstance.GetOrCreateGame(roomID) - - authUser := c.Get("authUser").(*database.User) - - send := func(s string) { - _, _ = c.Response().Write([]byte(s)) - } - - quit := hutils.CloseSignalChan(c) - roomTopic := "room_" + roomID - roomUserTopic := "room_" + roomID + "_" + authUser.Username.String() - - sub := PokerPubSub.Subscribe([]string{roomTopic, roomUserTopic}) - defer sub.Close() - - c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) - c.Response().WriteHeader(http.StatusOK) - c.Response().Header().Set("Transfer-Encoding", "chunked") - c.Response().Header().Set("Connection", "keep-alive") - - send(cssReset) - send(pokerCss) - - send(buildCardsHtml()) - send(buildTakeSeatHtml(authUser, g, roomID)) - send(buildSeatsHtml(g)) - send(buildActionsBtns(roomID)) - send(buildYourCardsHtml(authUser, g)) - actions := `<iframe src="/poker/` + roomID + `/deal" id="dealBtn"></iframe>` - actions += `<iframe src="/poker/` + roomID + `/unsit" id="unSitBtn"></iframe>` - send(actions) - send(`<div id="errorMsg"></div>`) - send(`<div id="seat1Pot">BET: 0</div>`) - send(buildMainPotHtml(g)) - send(buildCountdownsHtml()) - - send(`<div id="deckStr"></div>`) - send(`<div id="deckHash"></div>`) - - if g.Ongoing != nil { - send(drawCountDownStyle(g.Ongoing.WaitTurnEvent)) - send(`<style>#deckHash:before { content: "` + g.Ongoing.GetDeckHash() + `"; }</style>`) - if g.IsGameDone { - send(`<style>#deckStr:before { content: "` + g.Ongoing.GetDeckStr() + `"; }</style>`) - } - for _, payload := range g.Ongoing.Events { - send(getPokerEventHtml(payload, "0s")) - } - } - c.Response().Flush() - -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 evt := payload.(type) { - case PokerSeatLeftEvent: - send(drawSeatsHtml(authUser, g)) - case GameStartedEvent: - send(drawGameStartedEvent(evt)) - case GameIsDoneEvent: - send(drawGameIsDoneHtml(g, evt)) - case YourCardEvent: - send(drawYourCardEvent(evt)) - case PlayerBetEvent: - send(drawPlayerBetEvent(evt)) - case ErrorMsgEvent: - send(drawErrorMsgEvent(evt)) - case PlayerFoldEvent: - send(drawPlayerFoldEvent(evt)) - case ShowCardsEvent: - send(drawShowCardsEvent(evt)) - case ResetCardsEvent: - send(drawResetCardsEvent()) - case PokerSeatTakenEvent: - send(drawSeatsHtml(authUser, g)) - case PokerWaitTurnEvent: - send(drawCountDownStyle(evt)) - case PokerEvent: - send(getPokerEventHtml(evt, "1s")) - } - c.Response().Flush() - continue - } - return nil -} diff --git a/pkg/web/handlers/poker/events.go b/pkg/web/handlers/poker/events.go @@ -0,0 +1,59 @@ +package poker + +type PokerEvent struct { + ID string + Idx int + Name string + Top int + Left int + Reveal bool + Angle string +} + +type GameStartedEvent struct { + DeckHash string +} + +type GameIsDoneEvent struct { + DeckStr string +} + +type ResetCardsEvent struct { +} + +type PlayerBetEvent struct { + PlayerIdx int + Player string + Bet int + TotalBet int + Cash int +} + +type ErrorMsgEvent struct { + Message string +} + +type PlayerFoldEvent struct { + Card1Idx, Card2Idx int +} + +type PokerWaitTurnEvent struct { + Idx int + MainPot int +} + +type YourCardEvent struct { + Idx int + Name string +} + +type ShowCardsEvent struct { + Cards []PlayerCard +} + +type PokerSeatTakenEvent struct { +} + +type PokerSeatLeftEvent struct { + Idx int +} diff --git a/pkg/web/handlers/poker/poker.go b/pkg/web/handlers/poker/poker.go @@ -0,0 +1,1150 @@ +package poker + +import ( + "dkforest/pkg/database" + "dkforest/pkg/pubsub" + "dkforest/pkg/utils" + hutils "dkforest/pkg/web/handlers/utils" + "errors" + "fmt" + "github.com/labstack/echo" + "net/http" + "strconv" + "strings" + "sync" + "time" +) + +const NbPlayers = 6 +const MaxUserCountdown = 10 +const MinTimeAfterGame = 10 +const BackfacingDeg = "-180deg" +const BurnStackX = 400 +const BurnStackY = 30 +const DealerStackX = 250 +const DealerStackY = 30 + +type Poker struct { + sync.Mutex + games map[string]*PokerGame +} + +func NewPoker() *Poker { + p := &Poker{} + p.games = make(map[string]*PokerGame) + return p +} + +func (p *Poker) GetOrCreateGame(roomID string) *PokerGame { + p.Lock() + defer p.Unlock() + g, found := p.games[roomID] + if !found { + g = &PokerGame{ + PlayersEventCh: make(chan PlayerEvent), + Players: make([]*PokerStandingPlayer, NbPlayers), + } + p.games[roomID] = g + } + return g +} + +func (p *Poker) GetGame(roomID string) *PokerGame { + p.Lock() + defer p.Unlock() + g, found := PokerInstance.games[roomID] + if !found { + return nil + } + return g +} + +type PlayerEvent struct { + Player string + Call bool + Check bool + Fold bool + Bet int +} + +var PokerInstance = NewPoker() + +type Ongoing struct { + Deck []string + Players []*PokerPlayer + Events []PokerEvent + WaitTurnEvent PokerWaitTurnEvent + MainPot int +} + +type PokerStandingPlayer struct { + Username string + Cash int +} + +type PokerPlayer struct { + Username string + Bet int + Cash int + Cards []PlayerCard + Folded bool +} + +type PlayerCard struct { + Idx int + Name string +} + +type PokerGame struct { + sync.Mutex + PlayersEventCh chan PlayerEvent + Players []*PokerStandingPlayer + Ongoing *Ongoing + DealerIdx int + IsGameDone bool + IsGameOver bool +} + +func (g *Ongoing) GetDeckStr() string { + return g.getDeckStr() +} + +func (g *Ongoing) getDeckStr() string { + return strings.Join(g.Deck, "") +} + +func (g *Ongoing) GetDeckHash() string { + return utils.MD5([]byte(g.getDeckStr())) +} + +func (g *Ongoing) GetPlayer(player string) *PokerPlayer { + for _, p := range g.Players { + if p != nil && p.Username == player { + return p + } + } + return nil +} + +func isRoundSettled(players []*PokerPlayer) bool { + allSettled := true + b := -1 + for _, p := range players { + if p != nil { + if p.Folded { + continue + } + if p.Cash == 0 { // all in + continue + } + if b == -1 { + b = p.Bet + } else { + if p.Bet != b { + allSettled = false + break + } + } + } + } + return allSettled +} + +func (g *PokerGame) SitPlayer(authUser *database.User, pos int) error { + if g.Players[pos] != nil { + return errors.New("seat already taken") + } + g.Players[pos] = &PokerStandingPlayer{Username: authUser.Username.String(), Cash: 1000} + return nil +} + +func (g *PokerGame) Deal(roomID string) { + roomTopic := "room_" + roomID + + if g.Ongoing != nil { + if !g.IsGameOver { + fmt.Println("game already ongoing") + return + } else { + g.IsGameOver = false + PokerPubSub.Pub(roomTopic, ResetCardsEvent{}) + time.Sleep(time.Second) + } + } + if g.CountSeated() < 2 { + fmt.Println("need at least 2 players") + return + } + deck := []string{ + "A♠", "2♠", "3♠", "4♠", "5♠", "6♠", "7♠", "8♠", "9♠", "10♠", "J♠", "Q♠", "K♠", + "A♥", "2♥", "3♥", "4♥", "5♥", "6♥", "7♥", "8♥", "9♥", "10♥", "J♥", "Q♥", "K♥", + "A♣", "2♣", "3♣", "4♣", "5♣", "6♣", "7♣", "8♣", "9♣", "10♣", "J♣", "Q♣", "K♣", + "A♦", "2♦", "3♦", "4♦", "5♦", "6♦", "7♦", "8♦", "9♦", "10♦", "J♦", "Q♦", "K♦", + } + utils.Shuffle(deck) + + players := make([]*PokerPlayer, NbPlayers) + for idx := range g.Players { + var player *PokerPlayer + if g.Players[idx] != nil { + player = &PokerPlayer{Username: g.Players[idx].Username, Cash: g.Players[idx].Cash} + } + players[idx] = player + } + + g.Ongoing = &Ongoing{Deck: deck, Players: players, WaitTurnEvent: PokerWaitTurnEvent{Idx: -1}} + + go func() { + waitPlayersActionFn := func() bool { + lastRisePlayerIdx := -1 + minBet := 0 + + playerAlive := 0 + for _, p := range g.Ongoing.Players { + if p != nil && !p.Folded { + playerAlive++ + } + } + + // TODO: implement maximum re-rise + OUTER: + for { // Loop until the round is settled + for i, p := range g.Ongoing.Players { + if p == nil { + continue + } + if i == lastRisePlayerIdx { + break OUTER + } + player := g.Ongoing.GetPlayer(p.Username) + if player.Folded { + continue + } + evt := PokerWaitTurnEvent{Idx: i} + PokerPubSub.Pub(roomTopic, evt) + g.Ongoing.WaitTurnEvent = evt + + // Maximum time allowed for the player to send his action + waitCh := time.After(MaxUserCountdown * time.Second) + LOOP: + for { // Repeat until we get an event from the player we're interested in + var evt PlayerEvent + + select { + case evt = <-g.PlayersEventCh: + case <-waitCh: + // Waited too long, either auto-check or auto-fold + if p.Cash == 0 { // all-in + break LOOP + } + if p.Bet < minBet { + player.Folded = true + PokerPubSub.Pub(roomTopic, PlayerFoldEvent{Card1Idx: player.Cards[0].Idx, Card2Idx: player.Cards[1].Idx}) + playerAlive-- + if playerAlive == 1 { + break OUTER + } + } + break LOOP + } + + if evt.Player != p.Username { + continue + } + roomUserTopic := "room_" + roomID + "_" + p.Username + if evt.Fold { + player.Folded = true + PokerPubSub.Pub(roomTopic, PlayerFoldEvent{Card1Idx: player.Cards[0].Idx, Card2Idx: player.Cards[1].Idx}) + playerAlive-- + if playerAlive == 1 { + break OUTER + } + } else if evt.Check { + if p.Bet < minBet { + msg := fmt.Sprintf("Need to bet %d", minBet-p.Bet) + PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) + continue + } + } else if evt.Call { + bet := minBet - p.Bet + if p.Cash < bet { + bet = p.Cash + p.Bet += bet + p.Cash = 0 + // All in + } else { + p.Bet += bet + p.Cash -= bet + } + PokerPubSub.Pub(roomTopic, PlayerBetEvent{PlayerIdx: i, Player: p.Username, Bet: bet, TotalBet: p.Bet, Cash: p.Cash}) + } else if evt.Bet > 0 { + if (p.Bet + evt.Bet) < minBet { + msg := fmt.Sprintf("Bet (%d) is too low. Must bet at least %d", evt.Bet, minBet-p.Bet) + PokerPubSub.Pub(roomUserTopic, ErrorMsgEvent{Message: msg}) + continue + } + if (p.Bet + evt.Bet) > minBet { + lastRisePlayerIdx = i + } + minBet = p.Bet + evt.Bet + p.Bet += evt.Bet + p.Cash -= evt.Bet + PokerPubSub.Pub(roomTopic, PlayerBetEvent{PlayerIdx: i, Player: p.Username, Bet: evt.Bet, TotalBet: p.Bet, Cash: p.Cash}) + } + break + } + } + + // All settle when all players have the same bet amount + if isRoundSettled(g.Ongoing.Players) { + break + } + } + + // Transfer players bets into the main pot + for i := range g.Ongoing.Players { + if g.Ongoing.Players[i] != nil { + g.Ongoing.MainPot += g.Ongoing.Players[i].Bet + g.Ongoing.Players[i].Bet = 0 + } + } + + evt := PokerWaitTurnEvent{Idx: -1, MainPot: g.Ongoing.MainPot} + PokerPubSub.Pub(roomTopic, evt) + g.Ongoing.WaitTurnEvent = evt + + return playerAlive == 1 + } + + type Seat struct { + Top int + Left int + Angle string + Top2 int + Left2 int + } + seats := []Seat{ + {Top: 50, Left: 600, Top2: 50 + 5, Left2: 600 + 5, Angle: "-90deg"}, + {Top: 150, Left: 574, Top2: 150 + 5, Left2: 574 + 3, Angle: "-80deg"}, + {Top: 250, Left: 530, Top2: 250 + 5, Left2: 530 + 1, Angle: "-70deg"}, + } + + var card string + idx := 0 + + burnCard := func(pos int) { + card = g.Ongoing.Deck[idx] + idx++ + evt := PokerEvent{ + ID: "card" + itoa(idx), + Name: "", + Idx: idx, + Top: BurnStackY + (pos * 2), + Left: BurnStackX + (pos * 4), + } + PokerPubSub.Pub(roomTopic, evt) + g.Ongoing.Events = append(g.Ongoing.Events, evt) + } + + dealCard := func(pos int) { + card = g.Ongoing.Deck[idx] + idx++ + evt := PokerEvent{ + ID: "card" + itoa(idx), + Name: card, + Idx: idx, + Top: 150, + Left: 100 + (pos * 55), + Reveal: true, + } + PokerPubSub.Pub(roomTopic, evt) + g.Ongoing.Events = append(g.Ongoing.Events, evt) + } + + deckHash := utils.MD5([]byte(strings.Join(g.Ongoing.Deck, ""))) + PokerPubSub.Pub(roomTopic, GameStartedEvent{DeckHash: deckHash}) + + // Deal cards + for j := 1; j <= 2; j++ { + for i, p := range g.Ongoing.Players { + if p == nil { + continue + } + if p.Cash == 0 { + continue + } + d := seats[i] + time.Sleep(time.Second) + card = g.Ongoing.Deck[idx] + idx++ + name := "" + left := d.Left + top := d.Top + if j == 2 { + left = d.Left2 + top = d.Top2 + } + evt := PokerEvent{ + ID: "card" + itoa(idx), + Name: name, + Idx: idx, + Top: top, + Left: left, + Angle: d.Angle, + } + g.Ongoing.Players[i].Cards = append(g.Ongoing.Players[i].Cards, PlayerCard{Idx: idx, Name: card}) + PokerPubSub.Pub(roomTopic, evt) + PokerPubSub.Pub(roomTopic+"_"+p.Username, YourCardEvent{Idx: j, Name: card}) + g.Ongoing.Events = append(g.Ongoing.Events, evt) + } + } + + // Wait for players to bet/call/check/fold... + time.Sleep(time.Second) + if waitPlayersActionFn() { + goto END + } + + // Burn + time.Sleep(time.Second) + burnCard(0) + + // Flop (3 first cards) + for i := 1; i <= 3; i++ { + time.Sleep(time.Second) + dealCard(i) + } + + // Wait for players to bet/call/check/fold... + time.Sleep(time.Second) + if waitPlayersActionFn() { + goto END + } + + // Burn + time.Sleep(time.Second) + burnCard(1) + + // Turn (4th card) + time.Sleep(time.Second) + dealCard(4) + + // Wait for players to bet/call/check/fold... + time.Sleep(time.Second) + if waitPlayersActionFn() { + goto END + } + + // Burn + time.Sleep(time.Second) + burnCard(2) + + // River (5th card) + time.Sleep(time.Second) + dealCard(5) + + // Wait for players to bet/call/check/fold... + time.Sleep(time.Second) + if waitPlayersActionFn() { + goto END + } + + // Show cards + for _, p := range g.Ongoing.Players { + if p != nil && !p.Folded { + fmt.Println(p.Username, p.Cards) + PokerPubSub.Pub(roomTopic, ShowCardsEvent{Cards: p.Cards}) + } + } + + END: + + // TODO: evaluate hands, and crown winner + var winner *PokerPlayer + for _, p := range g.Ongoing.Players { + if p != nil { + winner = p + break + } + } + winner.Cash += g.Ongoing.MainPot + g.Ongoing.MainPot = 0 + + // Sync "ongoing players" with "room players" objects + for idx := range g.Players { + if g.Ongoing.Players[idx] != nil && g.Players[idx] != nil { + g.Players[idx].Cash = g.Ongoing.Players[idx].Cash + } + } + + g.IsGameDone = true + PokerPubSub.Pub(roomTopic, GameIsDoneEvent{DeckStr: strings.Join(g.Ongoing.Deck, "")}) + + // Wait a minimum of X seconds before allowing a new game + time.Sleep(MinTimeAfterGame * time.Second) + g.IsGameOver = true + fmt.Println("GAME IS OVER") + }() +} + +func (g *PokerGame) CountSeated() (count int) { + for _, p := range g.Players { + if p != nil { + count++ + } + } + return +} + +func (g *PokerGame) IsSeated(player string) (bool, int) { + isSeated := false + pos := 0 + for idx, p := range g.Players { + if p != nil && p.Username == player { + isSeated = true + pos = idx + 1 + break + } + } + return isSeated, pos +} + +var PokerPubSub = pubsub.NewPubSub[any]() + +func PokerCheckHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + roomID := c.Param("roomID") + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.NoContent(http.StatusNotFound) + } + if c.Request().Method == http.MethodPost { + select { + case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Check: true}: + default: + } + } + return c.HTML(http.StatusOK, `<form method="post"><button>Check</button></form>`) +} + +func PokerBetHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + roomID := c.Param("roomID") + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.NoContent(http.StatusNotFound) + } + bet := 100 + if c.Request().Method == http.MethodPost { + bet, _ = strconv.Atoi(c.Request().PostFormValue("bet")) + select { + case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Bet: bet}: + default: + } + } + return c.HTML(http.StatusOK, `<form method="post"><input type="number" name="bet" value="`+itoa(bet)+`" style="width: 90px;" /><button>Bet</button></form>`) +} + +func PokerCallHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + roomID := c.Param("roomID") + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.NoContent(http.StatusNotFound) + } + if c.Request().Method == http.MethodPost { + select { + case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Call: true}: + default: + } + } + return c.HTML(http.StatusOK, `<form method="post"><button>Call</button></form>`) +} + +func PokerFoldHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + roomID := c.Param("roomID") + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.NoContent(http.StatusNotFound) + } + if c.Request().Method == http.MethodPost { + select { + case g.PlayersEventCh <- PlayerEvent{Player: authUser.Username.String(), Fold: true}: + default: + } + } + return c.HTML(http.StatusOK, `<form method="post"><button>Fold</button></form>`) +} + +func PokerDealHandler(c echo.Context) error { + roomID := c.Param("roomID") + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.NoContent(http.StatusNotFound) + } + if c.Request().Method == http.MethodPost { + g.Deal(roomID) + } + return c.HTML(http.StatusOK, `<form method="post"><button>Deal</button></form>`) +} + +func PokerUnSitHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + roomID := c.Param("roomID") + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.NoContent(http.StatusNotFound) + } + if c.Request().Method == http.MethodPost { + var idx int + found := false + for i, p := range g.Players { + if p != nil && p.Username == authUser.Username.String() { + g.Players[i] = nil + idx = i + found = true + break + } + } + if found { + myTopic := "room_" + roomID + PokerPubSub.Pub(myTopic, PokerSeatLeftEvent{Idx: idx + 1}) + } + } + return c.HTML(http.StatusOK, `<form method="post"><button>UnSit</button></form>`) +} + +func PokerSitHandler(c echo.Context) error { + html := `<form method="post"><button>SIT</button></form>` + authUser := c.Get("authUser").(*database.User) + pos, _ := strconv.Atoi(c.Param("pos")) + if pos < 1 || pos > NbPlayers { + return c.HTML(http.StatusOK, html) + } + pos-- + roomID := c.Param("roomID") + roomTopic := "room_" + roomID + g := PokerInstance.GetGame(roomID) + if g == nil { + return c.HTML(http.StatusOK, html) + } + if c.Request().Method == http.MethodPost { + if err := g.SitPlayer(authUser, pos); err != nil { + fmt.Println(err) + return c.HTML(http.StatusOK, html) + } + PokerPubSub.Pub(roomTopic, PokerSeatTakenEvent{}) + } + return c.HTML(http.StatusOK, html) +} + +func isHeartOrDiamond(name string) bool { + return strings.Contains(name, "♥") || + strings.Contains(name, "♦") +} + +func colorForCard(name string) string { + color := "black" + if isHeartOrDiamond(name) { + color = "darkred" + } + return color +} + +func colorForCard1(name string) string { + color := "black" + if isHeartOrDiamond(name) { + color = "red" + } + return color +} + +func buildYourCardsHtml(authUser *database.User, g *PokerGame) string { + html := `<div style="position: absolute; top: 450px; left: 200px;"><div id="yourCard1"></div><div id="yourCard2"></div></div>` + if g.Ongoing != nil { + cards := make([]PlayerCard, 0) + for _, p := range g.Ongoing.Players { + if p != nil && p.Username == authUser.Username.String() { + cards = p.Cards + break + } + } + html += `<style>` + if len(cards) >= 1 { + html += `#yourCard1:before { content: "` + cards[0].Name + `"; color: ` + colorForCard(cards[0].Name) + `; }` + } + if len(cards) == 2 { + html += `#yourCard2:before { content: "` + cards[1].Name + `"; color: ` + colorForCard(cards[1].Name) + `; }` + } + html += `</style>` + } + return html +} + +func buildCardsHtml() (html string) { + for i := 52; i >= 1; i-- { + html += `<div class="card-holder" id="card` + itoa(i) + `"><div class="back"></div><div class="card"><div class=inner></div></div></div>` + } + return +} + +func buildTakeSeatHtml(authUser *database.User, g *PokerGame, roomID string) string { + takeSeatBtns := "" + seated, _ := g.IsSeated(authUser.Username.String()) + for i, p := range g.Players { + takeSeatBtns += `<iframe src="/poker/` + roomID + `/sit/` + itoa(i+1) + `" class="takeSeat takeSeat` + itoa(i+1) + `"></iframe>` + if p != nil || seated { + takeSeatBtns += `<style>.takeSeat` + itoa(i+1) + ` { display: none; }</style>` + } + } + return takeSeatBtns +} + +func buildMainPotHtml(g *PokerGame) string { + html := `<div id="mainPot"></div>` + mainPot := 0 + if g.Ongoing != nil { + mainPot = g.Ongoing.MainPot + } + html += `<style>#mainPot:before { content: "Pot: ` + itoa(mainPot) + `"; }</style>` + return html +} + +func buildCountdownsHtml() (html string) { + for i := 1; i <= NbPlayers; i++ { + html += `<div id="countdown` + itoa(i) + `" class="timer" style="--duration: ` + itoa(MaxUserCountdown) + `;--size: 30;"><div class="mask"></div></div>` + } + return +} + +func buildActionsBtns(roomID string) string { + return ` +<div style="position: absolute; top: 400px; left: 200px;"> + <iframe src="/poker/` + roomID + `/bet" id="betBtn"></iframe> + <iframe src="/poker/` + roomID + `/call" id="callBtn"></iframe> + <iframe src="/poker/` + roomID + `/check" id="checkBtn"></iframe> + <iframe src="/poker/` + roomID + `/fold" id="foldBtn"></iframe> +</div>` +} + +func buildSeatsHtml(g *PokerGame) string { + seats := ` +<div>` + for i, p := range g.Players { + seats += `<div id="seat` + itoa(i+1) + `"></div>` + seats += `<div id="seat` + itoa(i+1) + `_cash"></div>` + if p != nil { + seats += `<style>#seat` + itoa(i+1) + `:before { content: "` + p.Username + `"; }</style>` + seats += `<style>#seat` + itoa(i+1) + `_cash:before { content: "` + itoa(p.Cash) + `"; }</style>` + } + } + seats += ` +</div>` + return seats +} + +func drawErrorMsgEvent(evt ErrorMsgEvent) string { + return `<style>#errorMsg:before { content: "` + evt.Message + `"; }</style>` +} + +func drawPlayerBetEvent(evt PlayerBetEvent) string { + return `<style>#seat` + itoa(evt.PlayerIdx+1) + `_cash:before { content: "` + itoa(evt.Cash) + `"; }</style>` +} + +func drawYourCardEvent(evt YourCardEvent) string { + return `<style>#yourCard` + itoa(evt.Idx) + `:before { content: "` + evt.Name + `"; color: ` + colorForCard(evt.Name) + `; }</style>` +} + +func drawGameStartedEvent(evt GameStartedEvent) string { + return `<style>#deckHash:before { content: "` + evt.DeckHash + `"; }</style>` +} + +func drawGameIsDoneHtml(g *PokerGame, evt GameIsDoneEvent) (html string) { + html += `<style>#deckStr:before { content: "` + evt.DeckStr + `"; }</style>` + for i, p := range g.Players { + if p != nil { + html += `<style>#seat` + itoa(i+1) + `_cash:before { content: "` + itoa(p.Cash) + `"; }</style>` + } + } + return +} + +func drawPlayerFoldEvent(evt PlayerFoldEvent) (html string) { + transition := `transition: 1s ease-in-out; transform: translateX(` + itoa(BurnStackX) + `px) translateY(` + itoa(BurnStackY) + `px) rotateY(` + BackfacingDeg + `);` + html = `<style>#card` + itoa(evt.Card1Idx) + ` { ` + transition + ` } + #card` + itoa(evt.Card2Idx) + ` { ` + transition + ` }</style>` + return +} + +func drawShowCardsEvent(evt ShowCardsEvent) (html string) { + // TODO: Proper showing cards + html += `<style>` + html += `#card` + itoa(evt.Cards[0].Idx) + ` { transition: 1s ease-in-out; transform: rotateY(0); }` + html += `#card` + itoa(evt.Cards[1].Idx) + ` { transition: 1s ease-in-out; transform: rotateY(0); }` + html += `#card` + itoa(evt.Cards[0].Idx) + ` .card .inner:before { content: "` + evt.Cards[0].Name + `"; color: ` + colorForCard(evt.Cards[0].Name) + `; }` + html += `#card` + itoa(evt.Cards[1].Idx) + ` .card .inner:before { content: "` + evt.Cards[1].Name + `"; color: ` + colorForCard(evt.Cards[1].Name) + `; }` + html += `</style>` + return +} + +func drawResetCardsEvent() (html string) { + html += `<style>` + for i := 1; i <= 52; i++ { + html += `#card` + itoa(i) + ` { transition: 1s ease-in-out; transform: translateX(` + itoa(DealerStackX) + `px) translateY(` + itoa(DealerStackY) + `px) rotateY(` + BackfacingDeg + `); } + #card` + itoa(i) + ` .card .inner:before { content: ""; }` + } + html += ` + #yourCard1:before { content: ""; } + #yourCard2:before { content: ""; } + #deckHash:before { content: ""; } + #deckStr:before { content: ""; } + #mainPot:before { content: "Pot: 0"; } + </style>` + return +} + +func drawSeatsHtml(authUser *database.User, g *PokerGame) string { + html := "<style>" + seated, _ := g.IsSeated(authUser.Username.String()) + for i, p := range g.Players { + if p != nil || seated { + html += `.takeSeat` + itoa(i+1) + ` { display: none; }` + } else { + html += `.takeSeat` + itoa(i+1) + ` { display: block; }` + } + if p != nil { + html += `#seat` + itoa(i+1) + `:before { content: "` + p.Username + `"; }` + html += `#seat` + itoa(i+1) + `_cash:before { content: "` + itoa(p.Cash) + `"; }` + } else { + html += `#seat` + itoa(i+1) + `:before { content: ""; }` + html += `#seat` + itoa(i+1) + `_cash:before { content: ""; }` + } + } + html += "</style>" + return html +} + +func drawCountDownStyle(evt PokerWaitTurnEvent) string { + html := "<style>" + html += `#countdown1, #countdown2, #countdown3, #countdown4, #countdown5, #countdown6 { display: none; }` + if evt.Idx == -1 { + html += `#mainPot:before { content: "Pot: ` + itoa(evt.MainPot) + `"; }` + } else if evt.Idx == 0 { + html += `#countdown1 { top: 50px; left: 600px; display: block; animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; }` + } else if evt.Idx == 1 { + html += `#countdown2 { top: 150px; left: 574px; display: block; animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; }` + } else if evt.Idx == 2 { + html += `#countdown3 { top: 250px; left: 530px; display: block; animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; }` + } + html += "</style>" + return html +} + +func getPokerEventHtml(payload PokerEvent, animationTime string) string { + transform := `transform: translate(` + itoa(payload.Left) + `px, ` + itoa(payload.Top) + `px)` + if payload.Angle != "" { + transform += ` rotateZ(` + payload.Angle + `)` + } + if !payload.Reveal { + transform += ` rotateY(` + BackfacingDeg + `)` + } + transform += ";" + pokerEvtHtml := `<style> +#` + payload.ID + ` { + z-index: ` + itoa(payload.Idx) + `; + transition: ` + animationTime + ` ease-in-out; + ` + transform + ` +} +#` + payload.ID + ` .card .inner:before { content: "` + payload.Name + `"; color: ` + colorForCard1(payload.Name) + `; } +</style>` + return pokerEvtHtml +} + +func itoa(i int) string { + return strconv.Itoa(i) +} + +var cssReset = `<style> + html, body, div, span, applet, object, iframe, + h1, h2, h3, h4, h5, h6, p, blockquote, pre, + a, abbr, acronym, address, big, cite, code, + del, dfn, em, img, ins, kbd, q, s, samp, + small, strike, strong, sub, sup, tt, var, + b, u, i, center, + dl, dt, dd, ol, ul, li, + fieldset, form, label, legend, + table, caption, tbody, tfoot, thead, tr, th, td, + article, aside, canvas, details, embed, + figure, figcaption, footer, header, hgroup, + menu, nav, output, ruby, section, summary, + time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + } + + article, aside, details, figcaption, figure, + footer, header, hgroup, menu, nav, section { + display: block; + } + body { + line-height: 1; + } + ol, ul { + list-style: none; + } + blockquote, q { + quotes: none; + } + blockquote:before, blockquote:after, + q:before, q:after { + content: ''; + content: none; + } + table { + border-collapse: collapse; + border-spacing: 0; + } +</style>` + +var pokerCss = `<style> +html, body { height: 100%; width: 100%; } +body { + background:linear-gradient(135deg, #449144 33%,#008a00 95%); +} +.card-holder{ + position: absolute; + top: 0; + left: 0; + transform: translateX(` + itoa(DealerStackX) + `px) translateY(` + itoa(DealerStackY) + `px) rotateY(` + BackfacingDeg + `); + transform-style: preserve-3d; + backface-visibility: hidden; + width:50px; + height:70px; + display:inline-block; + box-shadow:1px 2px 2px rgba(0,0,0,.8); + margin:2px; +} +.card { + box-shadow: inset 2px 2px 0 #fff, inset -2px -2px 0 #fff; + transform-style: preserve-3d; + position:absolute; + top:0; + left:0; + bottom:0; + right:0; + backface-visibility: hidden; + background-color:#fcfcfc; + border-radius:2%; + display:block; + width:100%; + height:100%; + border:1px solid black; +} +.card .inner { + padding: 5px; + font-size: 25px; +} +.back{ + position:absolute; + top:0; + left:0; + bottom:0; + right:0; + width:100%; + height:100%; + backface-visibility: hidden; + transform: rotateY(` + BackfacingDeg + `); + background: #36c; + background: + linear-gradient(135deg, #f26c32 0%,#c146a1 50%,#a80077 51%,#f9703e 100%); + border-radius:2%; + box-shadow: inset 3px 3px 0 #fff, inset -3px -3px 0 #fff; + display:block; + border:1px solid black; +} +.takeSeat { + width: 40px; + height: 30px; +} +#seat1 { position: absolute; top: 80px; left: 700px; } +#seat2 { position: absolute; top: 200px; left: 670px; } +#seat3 { position: absolute; top: 300px; left: 610px; } +#seat1_cash { position: absolute; top: 100px; left: 700px; } +#seat2_cash { position: absolute; top: 220px; left: 670px; } +#seat3_cash { position: absolute; top: 320px; left: 610px; } +#seat1Pot { position: absolute; top: 80px; left: 500px; } +.takeSeat1 { position: absolute; top: 80px; left: 700px; } +.takeSeat2 { position: absolute; top: 200px; left: 670px; } +.takeSeat3 { position: absolute; top: 300px; left: 610px; } +.takeSeat4 { position: absolute; top: 300px; left: 550px; } +.takeSeat5 { position: absolute; top: 300px; left: 500px; } +.takeSeat6 { position: absolute; top: 300px; left: 450px; } +#dealBtn { position: absolute; top: 400px; left: 50px; width: 80px; height: 30px; } +#unSitBtn { position: absolute; top: 430px; left: 50px; width: 80px; height: 30px; } +#checkBtn { width: 60px; height: 30px; } +#foldBtn { width: 50px; height: 30px; } +#callBtn { width: 50px; height: 30px; } +#betBtn { width: 145px; height: 30px; } +#countdown1 { position: absolute; display: none; z-index: 100; } +#countdown2 { position: absolute; display: none; z-index: 100; } +#countdown3 { position: absolute; display: none; z-index: 100; } +#countdown4 { position: absolute; display: none; z-index: 100; } +#countdown5 { position: absolute; display: none; z-index: 100; } +#countdown6 { position: absolute; display: none; z-index: 100; } +#mainPot { position: absolute; top: 240px; left: 250px; } +#yourCard1 { font-size: 22px; display: inline-block; margin-right: 15px; } +#yourCard2 { font-size: 22px; display: inline-block; } +#errorMsg { position: absolute; top: 500px; left: 150px; color: darkred; } + + +.timer { + background: -webkit-linear-gradient(left, black 50%, #eee 50%); + border-radius: 100%; + height: calc(var(--size) * 1px); + width: calc(var(--size) * 1px); + position: relative; + animation: time calc(var(--duration) * 1s) steps(1000, start) forwards; + -webkit-mask: radial-gradient(transparent 50%,#000 50%); + mask: radial-gradient(transparent 50%,#000 50%); +} +.mask { + border-radius: 100% 0 0 100% / 50% 0 0 50%; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 50%; + animation: mask calc(var(--duration) * 1s) steps(500, start) forwards; + -webkit-transform-origin: 100% 50%; +} +@-webkit-keyframes time { + 100% { + -webkit-transform: rotate(360deg); + } +} +@-webkit-keyframes mask { + 0% { + background: #eee; + -webkit-transform: rotate(0deg); + } + 50% { + background: #eee; + -webkit-transform: rotate(-180deg); + } + 50.01% { + background: black; + -webkit-transform: rotate(0deg); + } + 100% { + background: black; + -webkit-transform: rotate(-180deg); + } +} + +</style>` + +func PokerHandler(c echo.Context) error { + roomID := c.Param("roomID") + g := PokerInstance.GetOrCreateGame(roomID) + + authUser := c.Get("authUser").(*database.User) + + send := func(s string) { + _, _ = c.Response().Write([]byte(s)) + } + + quit := hutils.CloseSignalChan(c) + roomTopic := "room_" + roomID + roomUserTopic := "room_" + roomID + "_" + authUser.Username.String() + + sub := PokerPubSub.Subscribe([]string{roomTopic, roomUserTopic}) + defer sub.Close() + + c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + c.Response().Header().Set("Transfer-Encoding", "chunked") + c.Response().Header().Set("Connection", "keep-alive") + + send(cssReset) + send(pokerCss) + + send(buildCardsHtml()) + send(buildTakeSeatHtml(authUser, g, roomID)) + send(buildSeatsHtml(g)) + send(buildActionsBtns(roomID)) + send(buildYourCardsHtml(authUser, g)) + actions := `<iframe src="/poker/` + roomID + `/deal" id="dealBtn"></iframe>` + actions += `<iframe src="/poker/` + roomID + `/unsit" id="unSitBtn"></iframe>` + send(actions) + send(`<div id="errorMsg"></div>`) + send(`<div id="seat1Pot">BET: 0</div>`) + send(buildMainPotHtml(g)) + send(buildCountdownsHtml()) + + send(`<div id="deckStr"></div>`) + send(`<div id="deckHash"></div>`) + + if g.Ongoing != nil { + send(drawCountDownStyle(g.Ongoing.WaitTurnEvent)) + send(`<style>#deckHash:before { content: "` + g.Ongoing.GetDeckHash() + `"; }</style>`) + if g.IsGameDone { + send(`<style>#deckStr:before { content: "` + g.Ongoing.GetDeckStr() + `"; }</style>`) + } + for _, payload := range g.Ongoing.Events { + send(getPokerEventHtml(payload, "0s")) + } + } + c.Response().Flush() + +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 evt := payload.(type) { + case PokerSeatLeftEvent: + send(drawSeatsHtml(authUser, g)) + case GameStartedEvent: + send(drawGameStartedEvent(evt)) + case GameIsDoneEvent: + send(drawGameIsDoneHtml(g, evt)) + case YourCardEvent: + send(drawYourCardEvent(evt)) + case PlayerBetEvent: + send(drawPlayerBetEvent(evt)) + case ErrorMsgEvent: + send(drawErrorMsgEvent(evt)) + case PlayerFoldEvent: + send(drawPlayerFoldEvent(evt)) + case ShowCardsEvent: + send(drawShowCardsEvent(evt)) + case ResetCardsEvent: + send(drawResetCardsEvent()) + case PokerSeatTakenEvent: + send(drawSeatsHtml(authUser, g)) + case PokerWaitTurnEvent: + send(drawCountDownStyle(evt)) + case PokerEvent: + send(getPokerEventHtml(evt, "1s")) + } + c.Response().Flush() + continue + } + return nil +} diff --git a/pkg/web/web.go b/pkg/web/web.go @@ -11,6 +11,7 @@ import ( "dkforest/pkg/web/clientFrontends" "dkforest/pkg/web/handlers" v1 "dkforest/pkg/web/handlers/api/v1" + "dkforest/pkg/web/handlers/poker" "dkforest/pkg/web/middlewares" "fmt" "github.com/labstack/echo" @@ -97,21 +98,21 @@ func getMainServer(db *database.DkfDB, i18nBundle *i18n.Bundle, renderer *tmp.Te authGroup.POST("/captcha", handlers.CaptchaHandler, middlewares.AuthRateLimitMiddleware(time.Second, 1)) authGroup.GET("/donate", handlers.DonateHandler) authGroup.GET("/shop", handlers.ShopHandler) - authGroup.GET("/poker/:roomID", handlers.PokerHandler) - 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) - authGroup.POST("/poker/:roomID/deal", handlers.PokerDealHandler) - authGroup.GET("/poker/:roomID/unsit", handlers.PokerUnSitHandler) - authGroup.POST("/poker/:roomID/unsit", handlers.PokerUnSitHandler) - authGroup.GET("/poker/:roomID/sit/:pos", handlers.PokerSitHandler) - authGroup.POST("/poker/:roomID/sit/:pos", handlers.PokerSitHandler) + authGroup.GET("/poker/:roomID", poker.PokerHandler) + authGroup.GET("/poker/:roomID/check", poker.PokerCheckHandler) + authGroup.POST("/poker/:roomID/check", poker.PokerCheckHandler) + authGroup.GET("/poker/:roomID/fold", poker.PokerFoldHandler) + authGroup.POST("/poker/:roomID/fold", poker.PokerFoldHandler) + authGroup.GET("/poker/:roomID/call", poker.PokerCallHandler) + authGroup.POST("/poker/:roomID/call", poker.PokerCallHandler) + authGroup.GET("/poker/:roomID/bet", poker.PokerBetHandler) + authGroup.POST("/poker/:roomID/bet", poker.PokerBetHandler) + authGroup.GET("/poker/:roomID/deal", poker.PokerDealHandler) + authGroup.POST("/poker/:roomID/deal", poker.PokerDealHandler) + authGroup.GET("/poker/:roomID/unsit", poker.PokerUnSitHandler) + authGroup.POST("/poker/:roomID/unsit", poker.PokerUnSitHandler) + authGroup.GET("/poker/:roomID/sit/:pos", poker.PokerSitHandler) + authGroup.POST("/poker/:roomID/sit/:pos", poker.PokerSitHandler) authGroup.GET("/chess", handlers.ChessHandler) authGroup.POST("/chess", handlers.ChessHandler) authGroup.GET("/chess/analyze", handlers.ChessAnalyzeHandler)