dkforest

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

commit 22daa95a96f890b0dc0908589ae9a90df8bbb87e
parent 97a8e7a489e81742b3eeff843c3636e1404cb698
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Mon, 12 Jun 2023 14:38:19 -0700

cleanup

Diffstat:
Apkg/web/handlers/chess.go | 466+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/handlers/handlers.go | 451-------------------------------------------------------------------------------
2 files changed, 466 insertions(+), 451 deletions(-)

diff --git a/pkg/web/handlers/chess.go b/pkg/web/handlers/chess.go @@ -0,0 +1,466 @@ +package handlers + +import ( + "bytes" + "dkforest/pkg/config" + "dkforest/pkg/database" + "dkforest/pkg/pubsub" + "dkforest/pkg/utils" + "dkforest/pkg/web/handlers/interceptors" + "fmt" + "github.com/labstack/echo" + "github.com/notnil/chess" + "github.com/sirupsen/logrus" + "html/template" + "net/http" + "strings" + "time" +) + +type StylesBuilder []string + +func (b *StylesBuilder) Append(v string) { + *b = append(*b, v) +} + +func (b *StylesBuilder) Appendf(format string, a ...any) { + *b = append(*b, fmt.Sprintf(format, a...)) +} + +func (b *StylesBuilder) Build() string { + return fmt.Sprintf("<style>%s</style>", strings.Join(*b, " ")) +} + +func chessGamefoolMate(g *interceptors.ChessGame) { + g.MoveStr("f3") + g.MoveStr("e5") + g.MoveStr("g4") +} + +func chessGameCheck(g *interceptors.ChessGame) { + g.MoveStr("Nc3") + g.MoveStr("h6") + g.MoveStr("Nb5") + g.MoveStr("h5") +} + +func chessGamePromoW(g *interceptors.ChessGame) { + g.MoveStr("h4") + g.MoveStr("g5") + g.MoveStr("hxg5") + g.MoveStr("h5") + g.MoveStr("g6") + g.MoveStr("h4") + g.MoveStr("g7") + g.MoveStr("h3") +} + +func chessGamePromoB(g *interceptors.ChessGame) { + g.MoveStr("a3") + g.MoveStr("c5") + g.MoveStr("a4") + g.MoveStr("c4") + g.MoveStr("a5") + g.MoveStr("c3") + g.MoveStr("a6") + g.MoveStr("cxb2") + g.MoveStr("axb7") +} + +func chessGameKingSideCastle(g *interceptors.ChessGame) { + g.MoveStr("e3") + g.MoveStr("e6") + g.MoveStr("Be2") + g.MoveStr("Be7") + g.MoveStr("Nf3") + g.MoveStr("Nf6") +} + +func chessGameQueenSideCastle(g *interceptors.ChessGame) { + g.MoveStr("d4") + g.MoveStr("d5") + g.MoveStr("Qd3") + g.MoveStr("Qd6") + g.MoveStr("Bd2") + g.MoveStr("Bd7") + g.MoveStr("Nc3") + g.MoveStr("Nc6") +} + +func chessGameEnPassant(g *interceptors.ChessGame) { + g.MoveStr("d4") + g.MoveStr("f6") + g.MoveStr("d5") + g.MoveStr("e5") +} + +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>` + +func ChessHandler(c echo.Context) error { + authUser := c.Get("authUser").(*database.User) + db := c.Get("database").(*database.DkfDB) + var data chessData + data.Games = interceptors.ChessInstance.GetGames() + + if c.Request().Method == http.MethodPost { + data.Username = database.Username(c.Request().PostFormValue("username")) + player2, err := db.GetUserByUsername(data.Username) + if err != nil { + data.Error = "invalid username" + return c.Render(http.StatusOK, "chess", data) + } + if _, err := interceptors.ChessInstance.NewGame1("", config.GeneralRoomID, *authUser, player2); err != nil { + data.Error = err.Error() + return c.Render(http.StatusOK, "chess", data) + } + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + return c.Render(http.StatusOK, "chess", data) +} + +func ChessGameFormHandler(c echo.Context) error { + key := c.Param("key") + csrf, _ := c.Get("csrf").(string) + authUser := c.Get("authUser").(*database.User) + g := interceptors.ChessInstance.GetGame(key) + isFlipped := authUser.ID == g.Player2.ID + + htmlTmpl := cssReset + interceptors.ChessCSS + ` +<form method="post" action="/chess/{{ .Key }}"> + <input type="hidden" name="csrf" value="{{ .CSRF }}" /> + <table class="newBoard"> + {{ range $row := .Rows }} + <tr> + {{ range $col := $.Cols }} + {{ $id := GetID $row $col }} + <td> + <input name="sq_{{ $id }}" id="sq_{{ $id }}" type="checkbox" value="1" /> + <label for="sq_{{ $id }}"></label> + </td> + {{ end }} + </tr> + {{ end }} + </table> + <div style="width: 100%; display: flex; margin: 5px 0;"> + <div><button type="submit" style="background-color: #aaa;">Move</button></div> + <div style="margin-left: auto;"> + <span style="color: #aaa; margin-left: 20px;">Promo:</span> + <select name="promotion" style="background-color: #aaa;"> + <option value="queen">Queen</option> + <option value="rook">Rook</option> + <option value="knight">Knight</option> + <option value="bishop">Bishop</option> + </select> + </div> + </div> +</form>` + + data := map[string]any{ + "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7}, + "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7}, + "Key": key, + "CSRF": csrf, + } + + fns := template.FuncMap{ + "GetID": func(row, col int) int { return interceptors.GetID(row, col, isFlipped) }, + } + + var buf bytes.Buffer + _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data) + + return c.HTML(http.StatusOK, buf.String()) +} + +func ChessGameHandler(c echo.Context) error { + debugChess := true + + authUser := c.Get("authUser").(*database.User) + key := c.Param("key") + + g := interceptors.ChessInstance.GetGame(key) + if g == nil { + if debugChess && config.Development.IsTrue() { + // Chess debug + db := c.Get("database").(*database.DkfDB) + user1, _ := db.GetUserByID(1) + user2, _ := db.GetUserByID(30814) + interceptors.ChessInstance.NewGame(key, user1, user2) + g = interceptors.ChessInstance.GetGame(key) + chessGameEnPassant(g) + } else { + return c.Redirect(http.StatusFound, "/") + } + } + + game := g.Game + + isFlipped := authUser.ID == g.Player2.ID + + if c.Request().Method == http.MethodPost { + if !g.IsPlayer(authUser.ID) { + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + msg := c.Request().PostFormValue("message") + if msg == "resign" { + resignColor := utils.Ternary(isFlipped, chess.Black, chess.White) + game.Resign(resignColor) + interceptors.ChessPubSub.Pub(key, interceptors.ChessMove{}) + } else { + if err := interceptors.ChessInstance.SendMove(key, authUser.ID, g, c); err != nil { + logrus.Error(err) + } + } + return c.Redirect(http.StatusFound, c.Request().Referer()) + } + + isSpectator := !g.IsPlayer(authUser.ID) + if isSpectator && c.QueryParam("r") != "" { + isFlipped = true + } + + //isYourTurnFn := func() bool { + // return authUser.ID == g.Player1.ID && game.Position().Turn() == chess.White || + // authUser.ID == g.Player2.ID && game.Position().Turn() == chess.Black + //} + //isYourTurn := isYourTurnFn() + + send := func(s string) { + _, _ = c.Response().Write([]byte(s)) + } + + // If you are not a spectator, and it's your turn to play, we just render the form directly. + if game.Outcome() != chess.NoOutcome { + c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + send(cssReset) + send(`<style>html, body { background-color: #222; }</style>`) + card := g.DrawPlayerCard(key, isFlipped, false, authUser.ChessSoundsEnabled) + send(card) + return nil + } + + quit := closeSignalChan(c) + + if err := usersStreamsManager.Add(authUser.ID, key); err != nil { + return nil + } + defer usersStreamsManager.Remove(authUser.ID, key) + + 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(`<style>html, body { background-color: #222; }</style>`) + + authorizedChannels := make([]string, 0) + authorizedChannels = append(authorizedChannels, key) + + sub := interceptors.ChessPubSub.Subscribe(authorizedChannels) + defer sub.Close() + + var card1 string + if isSpectator { + card1 = g.DrawSpectatorCard(isFlipped, authUser.ChessSoundsEnabled) + } else { + card1 = g.DrawPlayerCard(key, isFlipped, false, authUser.ChessSoundsEnabled) + } + send(fmt.Sprintf(`<div id="div_0">%s</div>`, card1)) + + go func(c echo.Context, key string, p1ID, p2ID database.UserID) { + p1Online := false + p2Online := false + var once utils.Once + for { + select { + case <-once.After(100 * time.Millisecond): + case <-time.After(5 * time.Second): + case <-quit: + return + } + p1Count := usersStreamsManager.GetUserStreamsCountFor(p1ID, key) + p2Count := usersStreamsManager.GetUserStreamsCountFor(p2ID, key) + if p1Online && p1Count == 0 { + p1Online = false + send(`<style>#p1Status { background-color: darkred !important; }</style>`) + } else if !p1Online && p1Count > 0 { + p1Online = true + send(`<style>#p1Status { background-color: green !important; }</style>`) + } + if p2Online && p2Count == 0 { + p2Online = false + send(`<style>#p2Status { background-color: darkred !important; }</style>`) + } else if !p2Online && p2Count > 0 { + p2Online = true + send(`<style>#p2Status { background-color: green !important; }</style>`) + } + c.Response().Flush() + } + }(c, key, g.Player1.ID, g.Player2.ID) + + var animationIdx int +Loop: + for { + select { + case <-quit: + break Loop + default: + } + + if game.Outcome() != chess.NoOutcome { + send(`<meta http-equiv="refresh" content="0" />`) + break + } + + _, payload, err := sub.ReceiveTimeout2(1*time.Second, quit) + if err != nil { + if err == pubsub.ErrCancelled { + break Loop + } + continue + } + + if authUser.ChessSoundsEnabled { + if payload.Move.HasTag(chess.Capture) || payload.Move.HasTag(chess.EnPassant) { + send(`<audio src="/public/sounds/chess/Capture.ogg" autoplay></audio>`) + } else { + send(`<audio src="/public/sounds/chess/Move.ogg" autoplay></audio>`) + } + } + + var styles StylesBuilder + const animationMs = 400 + + animate := func(s1, s2 chess.Square, id string) { + x1, y1 := int(s1.File()), int(s1.Rank()) + x2, y2 := int(s2.File()), int(s2.Rank()) + if isFlipped { + x1 = 7 - x1 + x2 = 7 - x2 + } else { + y1 = 7 - y1 + y2 = 7 - y2 + } + animationIdx++ + animationName := fmt.Sprintf("move_anim_%d", animationIdx) + styles.Appendf(`@keyframes %s { from { left: calc(%d*12.5%%); top: calc(%d*12.5%%); } to { left: calc(%d*12.5%%); top: calc(%d*12.5%%); } }`, animationName, x1, y1, x2, y2) + styles.Appendf(`#%s { animation-name: %s; animation-duration: %dms; animation-fill-mode: forwards; }`, id, animationName, animationMs) + } + + animate(payload.Move.S1(), payload.Move.S2(), payload.IDStr1) + + if payload.Move.Promo() != chess.NoPieceType || payload.IDStr2 != "" { + // Ensure the capturing piece is draw above the one being captured + if payload.IDStr2 != "" { + styles.Appendf(`#%s { z-index: 2; }`, payload.IDStr2) + styles.Appendf(`#%s { z-index: 3; }`, payload.IDStr1) + } + // Wait until end of moving animation before hiding the captured piece or change promotion image + go func(payload interceptors.ChessMove, c echo.Context) { + select { + case <-time.After(animationMs * time.Millisecond): + case <-quit: + return + } + if payload.IDStr2 != "" { + send(fmt.Sprintf(`<style>#%s { display: none; }</style>`, payload.IDStr2)) + } + if payload.Move.Promo() != chess.NoPieceType { + pieceColor := utils.Ternary(payload.Move.S2().Rank() == chess.Rank8, chess.White, chess.Black) + promoImg := "/public/img/chess/" + pieceColor.String() + strings.ToUpper(payload.Move.Promo().String()) + ".png" + send(fmt.Sprintf(`<style>#%s { background-image: url("%s") !important; }</style>`, payload.IDStr1, promoImg)) + } + c.Response().Flush() + }(payload, c) + } + + // Animate rook during castle + if payload.Move.S1() == chess.E1 && payload.Move.HasTag(chess.KingSideCastle) { + animate(chess.H1, chess.F1, interceptors.WhiteKingSideRookID) + } else if payload.Move.S1() == chess.E8 && payload.Move.HasTag(chess.KingSideCastle) { + animate(chess.H8, chess.F8, interceptors.BlackKingSideRookID) + } else if payload.Move.S1() == chess.E1 && payload.Move.HasTag(chess.QueenSideCastle) { + animate(chess.A1, chess.D1, interceptors.WhiteQueenSideRookID) + } else if payload.Move.S1() == chess.E8 && payload.Move.HasTag(chess.QueenSideCastle) { + animate(chess.A8, chess.D8, interceptors.BlackQueenSideRookID) + } + // En passant + if payload.EnPassant != "" { + styles.Appendf(`#%s { display: none; }`, payload.EnPassant) + } + + // Render advantages + whiteAdv, whiteScore, blackAdv, blackScore := interceptors.CalcAdvantage(game.Position()) + styles.Appendf(`#white-advantage:before { content: "%s" !important; }`, whiteAdv) + styles.Appendf(`#white-advantage .score:after { content: "%s" !important; }`, whiteScore) + styles.Appendf(`#black-advantage:before { content: "%s" !important; }`, blackAdv) + styles.Appendf(`#black-advantage .score:after { content: "%s" !important; }`, blackScore) + + // Render last move + styles.Appendf(`.square { background-color: transparent !important; }`) + styles.Appendf(`.square_%d, .square_%d { background-color: %s !important; }`, + int(payload.Move.S1()), int(payload.Move.S2()), interceptors.LastMoveColor) + + // Reset kings background to transparent + styles.Appendf(`#%s { background-color: transparent !important; }`, interceptors.WhiteKingID) + styles.Appendf(`#%s { background-color: transparent !important; }`, interceptors.BlackKingID) + // Render "checks" red background + if payload.CheckW { + styles.Appendf(`#%s { background-color: %s !important; }`, interceptors.WhiteKingID, interceptors.CheckColor) + } else if payload.CheckB { + styles.Appendf(`#%s { background-color: %s !important; }`, interceptors.BlackKingID, interceptors.CheckColor) + } + + send(styles.Build()) + + c.Response().Flush() + } + return nil +} diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -11,7 +11,6 @@ import ( "dkforest/pkg/pubsub" "dkforest/pkg/utils/crypto" v1 "dkforest/pkg/web/handlers/api/v1" - "dkforest/pkg/web/handlers/interceptors" "dkforest/pkg/web/handlers/streamModals" "encoding/base64" "encoding/csv" @@ -22,11 +21,9 @@ import ( "fmt" "github.com/PuerkitoBio/goquery" "github.com/jinzhu/gorm" - "github.com/notnil/chess" _ "golang.org/x/image/bmp" _ "golang.org/x/image/webp" html2 "html" - "html/template" "image" _ "image/gif" "image/png" @@ -4797,454 +4794,6 @@ func Stego1ChallengeHandler(c echo.Context) error { return c.Render(http.StatusOK, "vip.stego1", data) } -func ChessHandler(c echo.Context) error { - authUser := c.Get("authUser").(*database.User) - db := c.Get("database").(*database.DkfDB) - var data chessData - data.Games = interceptors.ChessInstance.GetGames() - - if c.Request().Method == http.MethodPost { - data.Username = database.Username(c.Request().PostFormValue("username")) - player2, err := db.GetUserByUsername(data.Username) - if err != nil { - data.Error = "invalid username" - return c.Render(http.StatusOK, "chess", data) - } - if _, err := interceptors.ChessInstance.NewGame1("", config.GeneralRoomID, *authUser, player2); err != nil { - data.Error = err.Error() - return c.Render(http.StatusOK, "chess", data) - } - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - return c.Render(http.StatusOK, "chess", data) -} - -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>` - -func chessGamefoolMate(g *interceptors.ChessGame) { - g.MoveStr("f3") - g.MoveStr("e5") - g.MoveStr("g4") -} - -func chessGameCheck(g *interceptors.ChessGame) { - g.MoveStr("Nc3") - g.MoveStr("h6") - g.MoveStr("Nb5") - g.MoveStr("h5") -} - -func chessGamePromoW(g *interceptors.ChessGame) { - g.MoveStr("h4") - g.MoveStr("g5") - g.MoveStr("hxg5") - g.MoveStr("h5") - g.MoveStr("g6") - g.MoveStr("h4") - g.MoveStr("g7") - g.MoveStr("h3") -} - -func chessGamePromoB(g *interceptors.ChessGame) { - g.MoveStr("a3") - g.MoveStr("c5") - g.MoveStr("a4") - g.MoveStr("c4") - g.MoveStr("a5") - g.MoveStr("c3") - g.MoveStr("a6") - g.MoveStr("cxb2") - g.MoveStr("axb7") -} - -func chessGameKingSideCastle(g *interceptors.ChessGame) { - g.MoveStr("e3") - g.MoveStr("e6") - g.MoveStr("Be2") - g.MoveStr("Be7") - g.MoveStr("Nf3") - g.MoveStr("Nf6") -} - -func chessGameQueenSideCastle(g *interceptors.ChessGame) { - g.MoveStr("d4") - g.MoveStr("d5") - g.MoveStr("Qd3") - g.MoveStr("Qd6") - g.MoveStr("Bd2") - g.MoveStr("Bd7") - g.MoveStr("Nc3") - g.MoveStr("Nc6") -} - -func chessGameEnPassant(g *interceptors.ChessGame) { - g.MoveStr("d4") - g.MoveStr("f6") - g.MoveStr("d5") - g.MoveStr("e5") -} - -func ChessGameFormHandler(c echo.Context) error { - key := c.Param("key") - csrf, _ := c.Get("csrf").(string) - authUser := c.Get("authUser").(*database.User) - g := interceptors.ChessInstance.GetGame(key) - isFlipped := authUser.ID == g.Player2.ID - - htmlTmpl := cssReset + interceptors.ChessCSS + ` -<form method="post" action="/chess/{{ .Key }}"> - <input type="hidden" name="csrf" value="{{ .CSRF }}" /> - <table class="newBoard"> - {{ range $row := .Rows }} - <tr> - {{ range $col := $.Cols }} - {{ $id := GetID $row $col }} - <td> - <input name="sq_{{ $id }}" id="sq_{{ $id }}" type="checkbox" value="1" /> - <label for="sq_{{ $id }}"></label> - </td> - {{ end }} - </tr> - {{ end }} - </table> - <div style="width: 100%; display: flex; margin: 5px 0;"> - <div><button type="submit" style="background-color: #aaa;">Move</button></div> - <div style="margin-left: auto;"> - <span style="color: #aaa; margin-left: 20px;">Promo:</span> - <select name="promotion" style="background-color: #aaa;"> - <option value="queen">Queen</option> - <option value="rook">Rook</option> - <option value="knight">Knight</option> - <option value="bishop">Bishop</option> - </select> - </div> - </div> -</form>` - - data := map[string]any{ - "Rows": []int{0, 1, 2, 3, 4, 5, 6, 7}, - "Cols": []int{0, 1, 2, 3, 4, 5, 6, 7}, - "Key": key, - "CSRF": csrf, - } - - fns := template.FuncMap{ - "GetID": func(row, col int) int { return interceptors.GetID(row, col, isFlipped) }, - } - - var buf bytes.Buffer - _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data) - - return c.HTML(http.StatusOK, buf.String()) -} - -type StylesBuilder []string - -func (b *StylesBuilder) Append(v string) { - *b = append(*b, v) -} - -func (b *StylesBuilder) Appendf(format string, a ...any) { - *b = append(*b, fmt.Sprintf(format, a...)) -} - -func (b *StylesBuilder) Build() string { - return fmt.Sprintf("<style>%s</style>", strings.Join(*b, " ")) -} - -func ChessGameHandler(c echo.Context) error { - debugChess := true - - authUser := c.Get("authUser").(*database.User) - key := c.Param("key") - - g := interceptors.ChessInstance.GetGame(key) - if g == nil { - if debugChess && config.Development.IsTrue() { - // Chess debug - db := c.Get("database").(*database.DkfDB) - user1, _ := db.GetUserByID(1) - user2, _ := db.GetUserByID(30814) - interceptors.ChessInstance.NewGame(key, user1, user2) - g = interceptors.ChessInstance.GetGame(key) - chessGameEnPassant(g) - } else { - return c.Redirect(http.StatusFound, "/") - } - } - - game := g.Game - - isFlipped := authUser.ID == g.Player2.ID - - if c.Request().Method == http.MethodPost { - if !g.IsPlayer(authUser.ID) { - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - msg := c.Request().PostFormValue("message") - if msg == "resign" { - resignColor := utils.Ternary(isFlipped, chess.Black, chess.White) - game.Resign(resignColor) - interceptors.ChessPubSub.Pub(key, interceptors.ChessMove{}) - } else { - if err := interceptors.ChessInstance.SendMove(key, authUser.ID, g, c); err != nil { - logrus.Error(err) - } - } - return c.Redirect(http.StatusFound, c.Request().Referer()) - } - - isSpectator := !g.IsPlayer(authUser.ID) - if isSpectator && c.QueryParam("r") != "" { - isFlipped = true - } - - //isYourTurnFn := func() bool { - // return authUser.ID == g.Player1.ID && game.Position().Turn() == chess.White || - // authUser.ID == g.Player2.ID && game.Position().Turn() == chess.Black - //} - //isYourTurn := isYourTurnFn() - - send := func(s string) { - _, _ = c.Response().Write([]byte(s)) - } - - // If you are not a spectator, and it's your turn to play, we just render the form directly. - if game.Outcome() != chess.NoOutcome { - c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) - c.Response().WriteHeader(http.StatusOK) - send(cssReset) - send(`<style>html, body { background-color: #222; }</style>`) - card := g.DrawPlayerCard(key, isFlipped, false, authUser.ChessSoundsEnabled) - send(card) - return nil - } - - quit := closeSignalChan(c) - - if err := usersStreamsManager.Add(authUser.ID, key); err != nil { - return nil - } - defer usersStreamsManager.Remove(authUser.ID, key) - - 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(`<style>html, body { background-color: #222; }</style>`) - - authorizedChannels := make([]string, 0) - authorizedChannels = append(authorizedChannels, key) - - sub := interceptors.ChessPubSub.Subscribe(authorizedChannels) - defer sub.Close() - - var card1 string - if isSpectator { - card1 = g.DrawSpectatorCard(isFlipped, authUser.ChessSoundsEnabled) - } else { - card1 = g.DrawPlayerCard(key, isFlipped, false, authUser.ChessSoundsEnabled) - } - send(fmt.Sprintf(`<div id="div_0">%s</div>`, card1)) - - go func(c echo.Context, key string, p1ID, p2ID database.UserID) { - p1Online := false - p2Online := false - var once utils.Once - for { - select { - case <-once.After(100 * time.Millisecond): - case <-time.After(5 * time.Second): - case <-quit: - return - } - p1Count := usersStreamsManager.GetUserStreamsCountFor(p1ID, key) - p2Count := usersStreamsManager.GetUserStreamsCountFor(p2ID, key) - if p1Online && p1Count == 0 { - p1Online = false - send(`<style>#p1Status { background-color: darkred !important; }</style>`) - } else if !p1Online && p1Count > 0 { - p1Online = true - send(`<style>#p1Status { background-color: green !important; }</style>`) - } - if p2Online && p2Count == 0 { - p2Online = false - send(`<style>#p2Status { background-color: darkred !important; }</style>`) - } else if !p2Online && p2Count > 0 { - p2Online = true - send(`<style>#p2Status { background-color: green !important; }</style>`) - } - c.Response().Flush() - } - }(c, key, g.Player1.ID, g.Player2.ID) - - var animationIdx int -Loop: - for { - select { - case <-quit: - break Loop - default: - } - - if game.Outcome() != chess.NoOutcome { - send(`<meta http-equiv="refresh" content="0" />`) - break - } - - _, payload, err := sub.ReceiveTimeout2(1*time.Second, quit) - if err != nil { - if err == pubsub.ErrCancelled { - break Loop - } - continue - } - - if authUser.ChessSoundsEnabled { - if payload.Move.HasTag(chess.Capture) || payload.Move.HasTag(chess.EnPassant) { - send(`<audio src="/public/sounds/chess/Capture.ogg" autoplay></audio>`) - } else { - send(`<audio src="/public/sounds/chess/Move.ogg" autoplay></audio>`) - } - } - - var styles StylesBuilder - const animationMs = 400 - - animate := func(s1, s2 chess.Square, id string) { - x1, y1 := int(s1.File()), int(s1.Rank()) - x2, y2 := int(s2.File()), int(s2.Rank()) - if isFlipped { - x1 = 7 - x1 - x2 = 7 - x2 - } else { - y1 = 7 - y1 - y2 = 7 - y2 - } - animationIdx++ - animationName := fmt.Sprintf("move_anim_%d", animationIdx) - styles.Appendf(`@keyframes %s { from { left: calc(%d*12.5%%); top: calc(%d*12.5%%); } to { left: calc(%d*12.5%%); top: calc(%d*12.5%%); } }`, animationName, x1, y1, x2, y2) - styles.Appendf(`#%s { animation-name: %s; animation-duration: %dms; animation-fill-mode: forwards; }`, id, animationName, animationMs) - } - - animate(payload.Move.S1(), payload.Move.S2(), payload.IDStr1) - - if payload.Move.Promo() != chess.NoPieceType || payload.IDStr2 != "" { - // Ensure the capturing piece is draw above the one being captured - if payload.IDStr2 != "" { - styles.Appendf(`#%s { z-index: 2; }`, payload.IDStr2) - styles.Appendf(`#%s { z-index: 3; }`, payload.IDStr1) - } - // Wait until end of moving animation before hiding the captured piece or change promotion image - go func(payload interceptors.ChessMove, c echo.Context) { - select { - case <-time.After(animationMs * time.Millisecond): - case <-quit: - return - } - if payload.IDStr2 != "" { - send(fmt.Sprintf(`<style>#%s { display: none; }</style>`, payload.IDStr2)) - } - if payload.Move.Promo() != chess.NoPieceType { - pieceColor := utils.Ternary(payload.Move.S2().Rank() == chess.Rank8, chess.White, chess.Black) - promoImg := "/public/img/chess/" + pieceColor.String() + strings.ToUpper(payload.Move.Promo().String()) + ".png" - send(fmt.Sprintf(`<style>#%s { background-image: url("%s") !important; }</style>`, payload.IDStr1, promoImg)) - } - c.Response().Flush() - }(payload, c) - } - - // Animate rook during castle - if payload.Move.S1() == chess.E1 && payload.Move.HasTag(chess.KingSideCastle) { - animate(chess.H1, chess.F1, interceptors.WhiteKingSideRookID) - } else if payload.Move.S1() == chess.E8 && payload.Move.HasTag(chess.KingSideCastle) { - animate(chess.H8, chess.F8, interceptors.BlackKingSideRookID) - } else if payload.Move.S1() == chess.E1 && payload.Move.HasTag(chess.QueenSideCastle) { - animate(chess.A1, chess.D1, interceptors.WhiteQueenSideRookID) - } else if payload.Move.S1() == chess.E8 && payload.Move.HasTag(chess.QueenSideCastle) { - animate(chess.A8, chess.D8, interceptors.BlackQueenSideRookID) - } - // En passant - if payload.EnPassant != "" { - styles.Appendf(`#%s { display: none; }`, payload.EnPassant) - } - - // Render advantages - whiteAdv, whiteScore, blackAdv, blackScore := interceptors.CalcAdvantage(game.Position()) - styles.Appendf(`#white-advantage:before { content: "%s" !important; }`, whiteAdv) - styles.Appendf(`#white-advantage .score:after { content: "%s" !important; }`, whiteScore) - styles.Appendf(`#black-advantage:before { content: "%s" !important; }`, blackAdv) - styles.Appendf(`#black-advantage .score:after { content: "%s" !important; }`, blackScore) - - // Render last move - styles.Appendf(`.square { background-color: transparent !important; }`) - styles.Appendf(`.square_%d, .square_%d { background-color: %s !important; }`, - int(payload.Move.S1()), int(payload.Move.S2()), interceptors.LastMoveColor) - - // Reset kings background to transparent - styles.Appendf(`#%s { background-color: transparent !important; }`, interceptors.WhiteKingID) - styles.Appendf(`#%s { background-color: transparent !important; }`, interceptors.BlackKingID) - // Render "checks" red background - if payload.CheckW { - styles.Appendf(`#%s { background-color: %s !important; }`, interceptors.WhiteKingID, interceptors.CheckColor) - } else if payload.CheckB { - styles.Appendf(`#%s { background-color: %s !important; }`, interceptors.BlackKingID, interceptors.CheckColor) - } - - send(styles.Build()) - - c.Response().Flush() - } - return nil -} - func ChatStreamHandler(c echo.Context) error { return chatHandler(c, false, true) }