commit 9a4c6ad590a9ea0eabdcfbf7b6702da583096ab4
parent b234b2b594f569c7de2d27192cade3570385ed9b
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Tue, 5 Dec 2023 21:03:00 -0500
cleanup
Diffstat:
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)