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
105 changes: 105 additions & 0 deletions handlers/fun/chess/game_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package chess

import (
"errors"
"fmt"
"oj/api"
"strings"

"github.com/notnil/chess"
)

type GameState struct {
Board [8][8]*UISquare
Game *chess.Game
ValidMoves []*chess.Move
MatchID int64
}

func gameStateFromMatch(match api.ChessMatch) (*GameState, error) {
reader := strings.NewReader(match.Pgn)
fn, err := chess.PGN(reader)
if err != nil {
return nil, err
}
game := chess.NewGame(fn)

pos := game.Position()

svgPiece := [13]string{
"", // empty piece
"/assets/chess/wK.svg",
"/assets/chess/wQ.svg",
"/assets/chess/wR.svg",
"/assets/chess/wB.svg",
"/assets/chess/wN.svg",
"/assets/chess/wP.svg",
"/assets/chess/bK.svg",
"/assets/chess/bQ.svg",
"/assets/chess/bR.svg",
"/assets/chess/bB.svg",
"/assets/chess/bN.svg",
"/assets/chess/bP.svg",
}

state := GameState{Game: game}

squareMap := pos.Board().SquareMap()

for i := 0; i < 64; i++ {
piece := squareMap[chess.Square(i)]
rank := 7 - i/8
file := i % 8
state.Board[rank][file] = &UISquare{
SVGPiece: svgPiece[piece],
Rank: rank,
File: file,
MatchID: match.ID,
Action: fmt.Sprintf("select?rank=%d&file=%d", rank, file),
}
}

return &state, nil
}

var ErrSquareNotOccupied = errors.New("no piece on square")

func (s *GameState) selectSquare(rank, file int) error {
var selected *UISquare

for _, row := range s.Board {
for _, square := range row {
if square.Rank == rank && square.File == file {
if square.Occupied() {
selected = square
square.Selected = true
square.Action = "unselect"
}
}
}
}

if selected == nil {
return ErrSquareNotOccupied
}

// add the dots to the places the peice on the selected square can move to
moves := s.Game.ValidMoves()

selectedSquare := chess.Square((7-selected.Rank)*8 + selected.File)

for _, move := range moves {
if move.S1() == selectedSquare {
s.ValidMoves = append(s.ValidMoves, move)
}
}

for _, move := range s.ValidMoves {
target := move.S2()
square := s.Board[7-target/8][target%8]
square.Dot = true
square.Action = fmt.Sprintf("move?s1=%d&s2=%d", move.S1(), move.S2())
}

return nil
}
174 changes: 38 additions & 136 deletions handlers/fun/chess/page.go → handlers/fun/chess/handlers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package chess

import (
_ "embed"
"errors"
"fmt"
"io"
Expand All @@ -18,23 +17,47 @@ import (

"github.com/go-chi/chi/v5"
"github.com/notnil/chess"

g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
)

type GameState struct {
Board [8][8]*UISquare
Game *chess.Game
ValidMoves []*chess.Move
MatchID int64
}

type UIGameState struct {
gameState *GameState
flip bool
}

func (s UIGameState) Render(w io.Writer) error {
rows := make([][]g.Node, 0, 8)

if s.flip {
for rank := 7; rank >= 0; rank -= 1 {
row := make([]g.Node, 0, 8)
for file := 0; file < 8; file += 1 {
row = append(row, s.gameState.Board[rank][file])
}
rows = append(rows, row)
}
} else {
for rank := 0; rank < 8; rank += 1 {
row := make([]g.Node, 0, 8)
for file := 0; file < 8; file += 1 {
row = append(row, s.gameState.Board[rank][file])
}
rows = append(rows, row)
}
}
return h.Div(
h.Class("board"),
h.Div(h.Style("height:100%; width:100%; display:flex; flex-direction:column"),
g.Map(rows, func(row []g.Node) g.Node {
return h.Div(h.Style("flex:1; display:flex"),
g.Map(row, func(square g.Node) g.Node {
return h.Div(h.Style("flex:1"), square)
}))
})),
).Render(w)
}

type UISquare struct {
SVGPiece string
Selected bool
Expand Down Expand Up @@ -91,124 +114,14 @@ func (s UISquare) Render(w io.Writer) error {
).Render(w)
}

func (s UIGameState) Render(w io.Writer) error {
rows := make([][]g.Node, 0, 8)

if s.flip {
for rank := 7; rank >= 0; rank -= 1 {
row := make([]g.Node, 0, 8)
for file := 0; file < 8; file += 1 {
row = append(row, s.gameState.Board[rank][file])
}
rows = append(rows, row)
}
} else {
for rank := 0; rank < 8; rank += 1 {
row := make([]g.Node, 0, 8)
for file := 0; file < 8; file += 1 {
row = append(row, s.gameState.Board[rank][file])
}
rows = append(rows, row)
}
}
return h.Div(
h.Class("board"),
h.Div(h.Style("height:100%; width:100%; display:flex; flex-direction:column"),
g.Map(rows, func(row []g.Node) g.Node {
return h.Div(h.Style("flex:1; display:flex"),
g.Map(row, func(square g.Node) g.Node {
return h.Div(h.Style("flex:1"), square)
}))
})),
).Render(w)
}

func gameStateFromMatch(match api.ChessMatch) (*GameState, error) {
reader := strings.NewReader(match.Pgn)
fn, err := chess.PGN(reader)
if err != nil {
return nil, err
}
game := chess.NewGame(fn)

pos := game.Position()

svgPiece := [13]string{
"", // empty piece
"/assets/chess/wK.svg",
"/assets/chess/wQ.svg",
"/assets/chess/wR.svg",
"/assets/chess/wB.svg",
"/assets/chess/wN.svg",
"/assets/chess/wP.svg",
"/assets/chess/bK.svg",
"/assets/chess/bQ.svg",
"/assets/chess/bR.svg",
"/assets/chess/bB.svg",
"/assets/chess/bN.svg",
"/assets/chess/bP.svg",
}

state := GameState{Game: game}

squareMap := pos.Board().SquareMap()

for i := 0; i < 64; i++ {
piece := squareMap[chess.Square(i)]
rank := 7 - i/8
file := i % 8
state.Board[rank][file] = &UISquare{
SVGPiece: svgPiece[piece],
Rank: rank,
File: file,
MatchID: match.ID,
Action: fmt.Sprintf("select?rank=%d&file=%d", rank, file),
}
}

return &state, nil
}

var ErrSquareNotOccupied = errors.New("no piece on square")

func (s *GameState) selectSquare(rank, file int) error {
var selected *UISquare

for _, row := range s.Board {
for _, square := range row {
if square.Rank == rank && square.File == file {
if square.Occupied() {
selected = square
square.Selected = true
square.Action = "unselect"
}
}
}
}

if selected == nil {
return ErrSquareNotOccupied
}

// add the dots to the places the peice on the selected square can move to
moves := s.Game.ValidMoves()

selectedSquare := chess.Square((7-selected.Rank)*8 + selected.File)

for _, move := range moves {
if move.S1() == selectedSquare {
s.ValidMoves = append(s.ValidMoves, move)
}
func userMatchColor(user api.User, match api.ChessMatch) (chess.Color, error) {
if user.ID == match.BlackUserID {
return chess.Black, nil
}

for _, move := range s.ValidMoves {
target := move.S2()
square := s.Board[7-target/8][target%8]
square.Dot = true
square.Action = fmt.Sprintf("move?s1=%d&s2=%d", move.S1(), move.S2())
if user.ID == match.WhiteUserID {
return chess.White, nil
}

return nil
return chess.NoColor, fmt.Errorf("current user not part of match")
}

func (s *service) HandleSelect(w http.ResponseWriter, r *http.Request) {
Expand All @@ -232,7 +145,6 @@ func (s *service) HandleSelect(w http.ResponseWriter, r *http.Request) {
http.Error(w, "userMatchColor: "+err.Error(), http.StatusInternalServerError)
return
}
fmt.Println("turn=", state.Game.Position().Turn(), "user=", currentUserColor)

uiGameState := UIGameState{gameState: state, flip: match.BlackUserID == currentUser.ID}

Expand All @@ -255,16 +167,6 @@ func (s *service) HandleSelect(w http.ResponseWriter, r *http.Request) {
uiGameState.Render(w)
}

func userMatchColor(user api.User, match api.ChessMatch) (chess.Color, error) {
if user.ID == match.BlackUserID {
return chess.Black, nil
}
if user.ID == match.WhiteUserID {
return chess.White, nil
}
return chess.NoColor, fmt.Errorf("current user not part of match")
}

func (s *service) HandleDeselect(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
currentUser := auth.FromContext(ctx)
Expand Down
2 changes: 1 addition & 1 deletion handlers/fun/chess/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func matchOpponent(ctx context.Context, qtx *api.Queries, match api.ChessMatch,
}
opponent, err := qtx.UserByID(ctx, opponentID)
if err != nil {
return nil, fmt.Errorf("UserByID", err)
return nil, fmt.Errorf("UserByID: %w", err)
}
return &opponent, nil
}