commit 22daa95a96f890b0dc0908589ae9a90df8bbb87e
parent 97a8e7a489e81742b3eeff843c3636e1404cb698
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Mon, 12 Jun 2023 14:38:19 -0700
cleanup
Diffstat:
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)
}