Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ node_modules/
# SSL/TLS Certificates
/etc/letsencrypt/
certbot/

.DS_Store
11 changes: 5 additions & 6 deletions attributions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

---

## Chess Pieces

### Standard Chess Pieces (Files: Chess_pawn_w.svg, Chess_rook_b.svg, etc.)
## Chess Pieces (Modified)
* **Author:** Cburnett
* **Source:** Files obtained from Wikimedia Commons[Category:SVG chess pieces by Cburnett](https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces_by_Cburnett).
* **License:** Creative Commons Attribution-Share Alike 3.0 Unported. Full license text available at: [https://creativecommons.org/licenses/by-sa/3.0/deed.en](https://creativecommons.org/licenses/by-sa/3.0/deed.en)
* **Note:** This attribution covers all chess piece SVG files included in this project's `frontend/assets/chess` folder.
* **Source:** [Wikimedia Commons, Category:SVG chess pieces by Cburnett](https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces_by_Cburnett).
* **License:** [Creative Commons Attribution-Share Alike 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/deed.en)
* **Coverage:** This attribution covers all chess piece included in the `frontend/assets/chess` folder.
* **Modification:** These files are modified to use CSS variables for color.

---

2 changes: 2 additions & 0 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type WS struct {
RoomBuffer int64
MsgBuffer int64
SendBuffer int64
RecvBuffer int64
}

func (c *DB) ConnectionStrings() (string, string) {
Expand Down Expand Up @@ -89,6 +90,7 @@ func Load() (*AppConfig, error) {
RoomBuffer: 20,
MsgBuffer: 256,
SendBuffer: 64,
RecvBuffer: 64,
}

return cfg, nil
Expand Down
133 changes: 61 additions & 72 deletions backend/internal/game/chess.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package game

import (
"encoding/json"
"fmt"
"time"

Expand All @@ -10,107 +9,97 @@ import (

type chessGame struct {
baseGame
game *chess.Game
GameName string
game *chess.Game
}

func newChess() Factory {
return func(creator string, payload json.RawMessage) (Game, error) {
return func(updator func(GameUpdate)) (Game, error) {
game := &chessGame{
baseGame: newBase(2, "chess"),
baseGame: newBase(2, "chess", updator),
game: chess.NewGame(),
}
game.Players = []string{creator}
game.Turn = 0
game.self = game
return game, nil
}
}
func (g *chessGame) Move(sender string, payload json.RawMessage) (*GameState, error) {
mv, _, err := g.validateMove(sender, payload)
if err != nil {
return nil, err
}

moveStr := rowCol2Move(mv.From) + rowCol2Move(mv.To)
if mv.Change != "" {
moveStr += mv.Change
}
notation := chess.UCINotation{}
move, err := notation.Decode(g.game.Position(), moveStr)
if err != nil {
return nil, fmt.Errorf("invalid move format %q: %w", moveStr, err)
}
if g.game.Move(move, nil) != nil {
return nil, fmt.Errorf("invalid move %q", moveStr)
}

if g.game.Outcome() != chess.NoOutcome {
g.Status = StatusFin
g.EndedAt = time.Now()
switch g.game.Outcome() {
case chess.WhiteWon:
g.Winner = g.Players[0]
case chess.BlackWon:
g.Winner = g.Players[1]
}
}

g.Turn = 1 - g.Turn

return g.State(), nil
}

func (g *chessGame) State() *GameState {
func (c *chessGame) getBoardLocked() any {
board := make([][]int, 8)
for i := range 8 {
board[i] = make([]int, 8)
for j := range 8 {
sq := chess.Square((7-i)*8 + j)
piece := g.game.Position().Board().Piece(sq)
piece := c.game.Position().Board().Piece(sq)
if piece != chess.NoPiece {
board[i][j] = pieceToCode(piece)
} else {
board[i][j] = 0
}
}
}

state := g.state(board)
state.ValidMoves = g.validMoves()
return state
return board
}

func (g *chessGame) Tick() (*GameState, string) {
if g.handleTimeout() {
return g.State(), TickBroadcast
func (c *chessGame) getValidMovesLocked() []GameMove {
validMoves := []GameMove{}
if c.status == StatusInProgress {
for _, mv := range c.game.ValidMoves() {
from := mv.S1()
to := mv.S2()
validMoves = append(validMoves, GameMove{
From: Position{
Row: 7 - int(from.Rank()),
Col: int(from.File()),
},
To: Position{
Row: 7 - int(to.Rank()),
Col: int(to.File()),
},
})
}
}
return validMoves
}

if g.Status == StatusFin && time.Since(g.EndedAt) > CleanupDelay {
return nil, TickFinished
}
func (c *chessGame) Move(sender string, mv *GameMove) error {
c.mu.Lock()
defer c.mu.Unlock()

return nil, TickNoChange
}
_, err := c.checkTurnLocked(sender)
if err != nil {
return err
}

func (g *chessGame) validMoves() []GameMove {
validMoves := []GameMove{}
if g.Status != StatusInProgress {
return validMoves
moveStr := rowCol2Move(mv.From) + rowCol2Move(mv.To)
if mv.Change != "" {
moveStr += mv.Change
}
for _, mv := range g.game.ValidMoves() {
from := mv.S1()
to := mv.S2()
validMoves = append(validMoves, GameMove{
From: Position{
Row: 7 - int(from.Rank()),
Col: int(from.File()),
},
To: Position{
Row: 7 - int(to.Rank()),
Col: int(to.File()),
},
})
notation := chess.UCINotation{}
move, err := notation.Decode(c.game.Position(), moveStr)
if err != nil {
return fmt.Errorf("invalid move format %q: %w", moveStr, err)
}
return validMoves
if c.game.Move(move, nil) != nil {
return fmt.Errorf("invalid move %q", moveStr)
}

if c.game.Outcome() != chess.NoOutcome {
c.status = StatusFin
c.endedAt = time.Now()
switch c.game.Outcome() {
case chess.WhiteWon:
c.winner = c.players[0]
case chess.BlackWon:
c.winner = c.players[1]
}
}
c.turn = 1 - c.turn
c.notify(GameUpdate{
State: c.stateLocked(),
Action: UpdateAction,
})
return nil
}

func rowCol2Move(pos Position) string {
Expand Down
64 changes: 29 additions & 35 deletions backend/internal/game/connect4.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
package game

import (
"encoding/json"
"fmt"
"errors"
"time"
)

type connect4 struct {
baseGame
board [6][7]int
GameName string
board [6][7]int
}

func newConnect4() Factory {
return func(creator string, payload json.RawMessage) (Game, error) {
return func(updator func(GameUpdate)) (Game, error) {
game := &connect4{
baseGame: newBase(2, "connect4"),
baseGame: newBase(2, "connect4", updator),
board: [6][7]int{},
}
game.Players = []string{creator}
game.Turn = 0
game.self = game
return game, nil
}
}

func (c *connect4) Move(sender string, payload json.RawMessage) (*GameState, error) {
mv, idx, err := c.validateMove(sender, payload)
func (c *connect4) getBoardLocked() any {
return c.board
}

func (c *connect4) Move(sender string, mv *GameMove) error {
c.mu.Lock()
defer c.mu.Unlock()

idx, err := c.checkTurnLocked(sender)
if err != nil {
return nil, err
return err
}

if mv.To.Col < 0 || mv.To.Col > 6 {
return nil, fmt.Errorf("invalid move")
return errors.New("invalid move")
}

// Find the first empty row in the column
var droppedRow int = -1
for row := 5; row >= 0; row-- {
if c.board[row][mv.To.Col] == 0 {
Expand All @@ -44,24 +49,23 @@ func (c *connect4) Move(sender string, payload json.RawMessage) (*GameState, err
}

if droppedRow == -1 {
return nil, fmt.Errorf("invalid move")
return errors.New("invalid move")
}

if win := c.checkWinner(droppedRow, mv.To.Col); win != 0 {
c.Status = StatusFin
c.Winner = c.Players[win-1]
c.EndedAt = time.Now()
c.status = StatusFin
c.winner = c.players[win-1]
c.endedAt = time.Now()
} else if c.checkDraw() {
c.Status = StatusFin
c.EndedAt = time.Now()
c.status = StatusFin
c.endedAt = time.Now()
}

c.Turn = 1 - c.Turn
return c.State(), nil
}

func (c *connect4) State() *GameState {
return c.state(c.board)
c.turn = 1 - c.turn
c.notify(GameUpdate{
State: c.stateLocked(),
Action: UpdateAction,
})
return nil
}

func (c *connect4) checkWinner(startRow, startCol int) int {
Expand Down Expand Up @@ -105,13 +109,3 @@ func (c *connect4) checkDraw() bool {
}
return true
}

func (c *connect4) Tick() (*GameState, string) {
if c.handleTimeout() {
return c.State(), TickFinished
}
if !c.EndedAt.IsZero() && time.Since(c.EndedAt) > CleanupDelay {
return c.State(), TickFinished
}
return nil, TickNoChange
}
8 changes: 3 additions & 5 deletions backend/internal/game/factory.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package game

import (
"encoding/json"
"fmt"
)

type Factory func(creator string, payload json.RawMessage) (Game, error)
type Factory func(updator func(GameUpdate)) (Game, error)

// just in case we need payload for alternative modes
type GameInfo struct {
Factory Factory
}
Expand All @@ -32,10 +30,10 @@ func (r *Registry) RegisterAll() {
r.register("chess", newChess())
}

func (r *Registry) Create(name, creator string, payload json.RawMessage) (Game, error) {
func (r *Registry) Create(name string, updator func(GameUpdate)) (Game, error) {
info, ok := r.games[name]
if !ok {
return nil, fmt.Errorf("game type not supported: %s", name)
}
return info.Factory(creator, payload)
return info.Factory(updator)
}
Loading