Compare commits
46 Commits
ce6dd39af1
...
cec8aaf274
Author | SHA1 | Date | |
---|---|---|---|
cec8aaf274 | |||
f3cd10cbb8 | |||
d7645edea9 | |||
bf481bc452 | |||
fe82dee3bb | |||
ee4e1f0edf | |||
5729ae631a | |||
31d9ff02d5 | |||
d1315b931b | |||
2dc7cbf213 | |||
0175ac077f | |||
7aa65f4ec0 | |||
be247e1f46 | |||
908cc1cfda | |||
aa072ca4aa | |||
d98c0c221d | |||
bbcd0b7725 | |||
4a3315eaa0 | |||
bab667d0ed | |||
e6f9c529f8 | |||
87856f694d | |||
6a60aee89a | |||
cb92e66580 | |||
8f2dc00d9f | |||
6fefeb44f5 | |||
a9a4216d4e | |||
8f70fcb552 | |||
fddbd145f3 | |||
d6ffd8a57d | |||
4d7ab3484d | |||
e12946ca5d | |||
45c0dfce6f | |||
d3966685db | |||
4e12b40478 | |||
516478d258 | |||
6468955c23 | |||
2d99320942 | |||
11b2238c86 | |||
9a6579b6f8 | |||
2c6018755f | |||
72da5d3afd | |||
677c50d007 | |||
7eea9d932f | |||
e6b7582ae9 | |||
5a52d48ef8 | |||
da103ef887 |
3
.env.sample
Normal file
3
.env.sample
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
SN_BASE_URL=
|
||||||
|
SN_API_TOKEN=
|
||||||
|
SN_MEDIA_URL=
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
*.png
|
*.png
|
||||||
!assets/*.png
|
!assets/*.png
|
||||||
|
.env
|
||||||
|
*.sqlite3
|
||||||
|
1187
chess/board.go
1187
chess/board.go
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ekzyis/sn-chess/chess"
|
"github.com/ekzyis/chessbot/chess"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,6 +19,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardInitial(t *testing.T) {
|
func TestBoardInitial(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
assertPiece(t, b, "a1", chess.Rook, chess.Light)
|
assertPiece(t, b, "a1", chess.Rook, chess.Light)
|
||||||
@ -58,20 +61,24 @@ func TestBoardInitial(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMovePawn(t *testing.T) {
|
func TestBoardMovePawn(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
b.Move("e4")
|
assertParse(t, b, "e4")
|
||||||
|
|
||||||
assertNoPiece(t, b, "e2")
|
assertNoPiece(t, b, "e2")
|
||||||
assertPiece(t, b, "e4", chess.Pawn, chess.Light)
|
assertPiece(t, b, "e4", chess.Pawn, chess.Light)
|
||||||
|
|
||||||
b.Move("e5")
|
assertParse(t, b, "e5")
|
||||||
|
|
||||||
assertNoPiece(t, b, "e7")
|
assertNoPiece(t, b, "e7")
|
||||||
assertPiece(t, b, "e5", chess.Pawn, chess.Dark)
|
assertPiece(t, b, "e5", chess.Pawn, chess.Dark)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMovePawnInvalid(t *testing.T) {
|
func TestBoardMovePawnInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
assertMoveError(t, b, "a5", "no pawn found that can move to a5")
|
assertMoveError(t, b, "a5", "no pawn found that can move to a5")
|
||||||
@ -83,7 +90,7 @@ func TestBoardMovePawnInvalid(t *testing.T) {
|
|||||||
assertMoveError(t, b, "g5", "no pawn found that can move to g5")
|
assertMoveError(t, b, "g5", "no pawn found that can move to g5")
|
||||||
assertMoveError(t, b, "h5", "no pawn found that can move to h5")
|
assertMoveError(t, b, "h5", "no pawn found that can move to h5")
|
||||||
|
|
||||||
b.Move("d4")
|
assertParse(t, b, "d4")
|
||||||
|
|
||||||
assertMoveError(t, b, "a4", "no pawn found that can move to a4")
|
assertMoveError(t, b, "a4", "no pawn found that can move to a4")
|
||||||
assertMoveError(t, b, "b4", "no pawn found that can move to b4")
|
assertMoveError(t, b, "b4", "no pawn found that can move to b4")
|
||||||
@ -96,93 +103,436 @@ func TestBoardMovePawnInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMovePawnCapture(t *testing.T) {
|
func TestBoardMovePawnCapture(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
b.Move("e4")
|
assertParse(t, b, "e4 d5 exd5")
|
||||||
b.Move("d5")
|
|
||||||
b.Move("exd5")
|
|
||||||
|
|
||||||
assertNoPiece(t, b, "e4")
|
assertNoPiece(t, b, "e4")
|
||||||
assertPiece(t, b, "d5", chess.Pawn, chess.Light)
|
assertPiece(t, b, "d5", chess.Pawn, chess.Light)
|
||||||
|
|
||||||
|
// test ambiguous capture
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "c4 d5 e4 e5 exd5")
|
||||||
|
|
||||||
|
assertNoPiece(t, b, "e4")
|
||||||
|
assertPiece(t, b, "d5", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "c4", chess.Pawn, chess.Light)
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "c4 d5 e4 e5 cxd5")
|
||||||
|
|
||||||
|
assertNoPiece(t, b, "c4")
|
||||||
|
assertPiece(t, b, "d5", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "e4", chess.Pawn, chess.Light)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardPawnPromotion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
b.Parse("a4 e6 a5 e5 a6 e4 axb7 e3 bxa8=Q")
|
||||||
|
|
||||||
|
assertPiece(t, b, "a8", chess.Queen, chess.Light)
|
||||||
|
assertNoPiece(t, b, "b7")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
b.Parse("a4 e6 a5 e5 a6 e4 axb7 e3 bxa8=R")
|
||||||
|
|
||||||
|
assertPiece(t, b, "a8", chess.Rook, chess.Light)
|
||||||
|
assertNoPiece(t, b, "b7")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
b.Parse("a4 e6 a5 e5 a6 e4 axb7 e3 bxa8=B")
|
||||||
|
|
||||||
|
assertPiece(t, b, "a8", chess.Bishop, chess.Light)
|
||||||
|
assertNoPiece(t, b, "b7")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
b.Parse("a4 e6 a5 e5 a6 e4 axb7 e3 bxa8=N")
|
||||||
|
|
||||||
|
assertPiece(t, b, "a8", chess.Knight, chess.Light)
|
||||||
|
assertNoPiece(t, b, "b7")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
b.Parse("a4 e6 a5 e5 a6 e4 axb7 e3")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "bxa8=K", "invalid promotion: K")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMoveKnight(t *testing.T) {
|
func TestBoardMoveKnight(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
b.Move("Nf3")
|
assertParse(t, b, "Nf3")
|
||||||
|
|
||||||
assertPiece(t, b, "f3", chess.Knight, chess.Light)
|
assertPiece(t, b, "f3", chess.Knight, chess.Light)
|
||||||
assertNoPiece(t, b, "g1")
|
assertNoPiece(t, b, "g1")
|
||||||
|
|
||||||
b.Move("Nh6")
|
assertParse(t, b, "Nh6")
|
||||||
|
|
||||||
assertPiece(t, b, "h6", chess.Knight, chess.Dark)
|
assertPiece(t, b, "h6", chess.Knight, chess.Dark)
|
||||||
assertNoPiece(t, b, "g8")
|
assertNoPiece(t, b, "g8")
|
||||||
|
|
||||||
|
assertParse(t, b, "Nc3")
|
||||||
|
assertPiece(t, b, "c3", chess.Knight, chess.Light)
|
||||||
|
assertNoPiece(t, b, "b1")
|
||||||
|
|
||||||
|
assertParse(t, b, "Na6")
|
||||||
|
assertPiece(t, b, "a6", chess.Knight, chess.Dark)
|
||||||
|
assertNoPiece(t, b, "b8")
|
||||||
|
|
||||||
|
assertParse(t, b, "Nh4")
|
||||||
|
assertPiece(t, b, "h4", chess.Knight, chess.Light)
|
||||||
|
assertNoPiece(t, b, "f3")
|
||||||
|
|
||||||
|
assertParse(t, b, "Nf5")
|
||||||
|
assertPiece(t, b, "f5", chess.Knight, chess.Dark)
|
||||||
|
assertNoPiece(t, b, "h6")
|
||||||
|
|
||||||
|
assertParse(t, b, "Na4")
|
||||||
|
assertPiece(t, b, "a4", chess.Knight, chess.Light)
|
||||||
|
assertNoPiece(t, b, "c3")
|
||||||
|
|
||||||
|
assertParse(t, b, "Nc5")
|
||||||
|
assertPiece(t, b, "c5", chess.Knight, chess.Dark)
|
||||||
|
assertNoPiece(t, b, "a6")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveKnightInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
// out of reach
|
||||||
|
assertMoveError(t, b, "Ng3", "no knight found that can move to g3")
|
||||||
|
assertMoveError(t, b, "Nb3", "no knight found that can move to b3")
|
||||||
|
|
||||||
|
// blocked by own piece
|
||||||
|
assertMoveError(t, b, "Ng1", "g1 blocked by white knight")
|
||||||
|
assertMoveError(t, b, "Nd2", "d2 blocked by white pawn")
|
||||||
|
assertMoveError(t, b, "Ne2", "e2 blocked by white pawn")
|
||||||
|
|
||||||
|
assertParse(t, b, "Nf3")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Ng6", "no knight found that can move to g6")
|
||||||
|
assertMoveError(t, b, "Nb6", "no knight found that can move to b6")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Ne7", "e7 blocked by black pawn")
|
||||||
|
assertMoveError(t, b, "Nd7", "d7 blocked by black pawn")
|
||||||
|
|
||||||
|
// ambiguous moves
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
assertParse(t, b, "e4 e5 Nf3 d6 Nc3 d5 Nb5 d4")
|
||||||
|
assertMoveError(t, b, "Nxd4", "move ambiguous: 2 knights can move to d4")
|
||||||
|
assertMoveError(t, b, "N4xd4", "no knight found that can move to d4")
|
||||||
|
// disambiguate via file
|
||||||
|
assertParse(t, b, "Nfxd4")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
assertParse(t, b, "e4 e5 Nf3 d6 Nc3 d5 Nb5 d4")
|
||||||
|
// disambiguate via rank
|
||||||
|
assertParse(t, b, "N3xd4")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveKnightCapture(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 Nf6 d4 Nxe4")
|
||||||
|
assertPiece(t, b, "e4", chess.Knight, chess.Dark)
|
||||||
|
assertPiece(t, b, "d4", chess.Pawn, chess.Light)
|
||||||
|
assertNoPiece(t, b, "g8")
|
||||||
|
assertNoPiece(t, b, "e2")
|
||||||
|
assertNoPiece(t, b, "d2")
|
||||||
|
|
||||||
|
// test ambiguous capture
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Nf3 d6 Nc3 d5 Nb5 d4 Nbxd4")
|
||||||
|
|
||||||
|
assertPiece(t, b, "e4", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "e5", chess.Pawn, chess.Dark)
|
||||||
|
assertPiece(t, b, "d4", chess.Knight, chess.Light)
|
||||||
|
assertPiece(t, b, "f3", chess.Knight, chess.Light)
|
||||||
|
assertNoPiece(t, b, "g1")
|
||||||
|
assertNoPiece(t, b, "b1")
|
||||||
|
assertNoPiece(t, b, "c3")
|
||||||
|
assertNoPiece(t, b, "d6")
|
||||||
|
assertNoPiece(t, b, "d5")
|
||||||
|
assertNoPiece(t, b, "b5")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Nf3 d6 Nc3 d5 Nb5 d4 Nfxd4")
|
||||||
|
|
||||||
|
assertPiece(t, b, "e4", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "e5", chess.Pawn, chess.Dark)
|
||||||
|
assertPiece(t, b, "d4", chess.Knight, chess.Light)
|
||||||
|
assertPiece(t, b, "b5", chess.Knight, chess.Light)
|
||||||
|
assertNoPiece(t, b, "g1")
|
||||||
|
assertNoPiece(t, b, "b1")
|
||||||
|
assertNoPiece(t, b, "c3")
|
||||||
|
assertNoPiece(t, b, "d6")
|
||||||
|
assertNoPiece(t, b, "d5")
|
||||||
|
assertNoPiece(t, b, "f3")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMoveBishop(t *testing.T) {
|
func TestBoardMoveBishop(t *testing.T) {
|
||||||
b := chess.NewBoard()
|
t.Parallel()
|
||||||
|
|
||||||
b.Move("Bc4")
|
b := chess.NewBoard()
|
||||||
|
|
||||||
assertMoveError(t, b, "Bc4", "no bishop found that can move to c4")
|
assertMoveError(t, b, "Bc4", "no bishop found that can move to c4")
|
||||||
|
|
||||||
b.Move("e3")
|
assertParse(t, b, "e3")
|
||||||
b.Move("e6")
|
assertParse(t, b, "e6")
|
||||||
|
|
||||||
b.Move("Bc4")
|
assertParse(t, b, "Bc4")
|
||||||
|
|
||||||
assertPiece(t, b, "c4", chess.Bishop, chess.Light)
|
assertPiece(t, b, "c4", chess.Bishop, chess.Light)
|
||||||
assertNoPiece(t, b, "f1")
|
assertNoPiece(t, b, "f1")
|
||||||
|
|
||||||
b.Move("Bc5")
|
assertParse(t, b, "Bc5")
|
||||||
|
|
||||||
assertPiece(t, b, "c5", chess.Bishop, chess.Dark)
|
assertPiece(t, b, "c5", chess.Bishop, chess.Dark)
|
||||||
assertNoPiece(t, b, "f8")
|
assertNoPiece(t, b, "f8")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMoveRook(t *testing.T) {
|
func TestBoardMoveBishopInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
b.Move("Ra3")
|
assertMoveError(t, b, "Bc3", "no bishop found that can move to c3")
|
||||||
|
assertMoveError(t, b, "Bc2", "c2 blocked by white pawn")
|
||||||
|
assertMoveError(t, b, "Bb2", "b2 blocked by white pawn")
|
||||||
|
|
||||||
|
// path blocked by white pawn at e2
|
||||||
|
assertMoveError(t, b, "Bd3", "no bishop found that can move to d3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveBishopCapture(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Bc4 d5 Bxd5")
|
||||||
|
|
||||||
|
assertPiece(t, b, "e4", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "e5", chess.Pawn, chess.Dark)
|
||||||
|
assertPiece(t, b, "d5", chess.Bishop, chess.Light)
|
||||||
|
assertNoPiece(t, b, "f1")
|
||||||
|
assertNoPiece(t, b, "e2")
|
||||||
|
assertNoPiece(t, b, "e7")
|
||||||
|
assertNoPiece(t, b, "d7")
|
||||||
|
|
||||||
|
// bishop captures are never ambiguous because they can only move on same color
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveRook(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
assertMoveError(t, b, "Ra3", "no rook found that can move to a3")
|
assertMoveError(t, b, "Ra3", "no rook found that can move to a3")
|
||||||
|
|
||||||
b.Move("a4")
|
assertParse(t, b, "a4 a5 Ra3")
|
||||||
b.Move("a5")
|
|
||||||
|
|
||||||
b.Move("Ra3")
|
|
||||||
|
|
||||||
assertPiece(t, b, "a3", chess.Rook, chess.Light)
|
assertPiece(t, b, "a3", chess.Rook, chess.Light)
|
||||||
assertNoPiece(t, b, "a1")
|
assertNoPiece(t, b, "a1")
|
||||||
|
|
||||||
b.Move("Ra6")
|
assertParse(t, b, "Ra6")
|
||||||
|
|
||||||
assertPiece(t, b, "a6", chess.Rook, chess.Dark)
|
assertPiece(t, b, "a6", chess.Rook, chess.Dark)
|
||||||
assertNoPiece(t, b, "a8")
|
assertNoPiece(t, b, "a8")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoardMoveQueen(t *testing.T) {
|
func TestBoardMoveRookCapture(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
b := chess.NewBoard()
|
b := chess.NewBoard()
|
||||||
|
|
||||||
b.Move("Qd3")
|
assertParse(t, b, "a4 e6 h4 e5 Ra3 e4 Rhh3 e3 Raxe3")
|
||||||
|
|
||||||
|
assertPiece(t, b, "e3", chess.Rook, chess.Light)
|
||||||
|
assertPiece(t, b, "h3", chess.Rook, chess.Light)
|
||||||
|
assertNoPiece(t, b, "a3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveRookInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Rb2", "b2 blocked by white pawn")
|
||||||
|
assertMoveError(t, b, "Rb1", "b1 blocked by white knight")
|
||||||
|
assertMoveError(t, b, "Ra2", "a2 blocked by white pawn")
|
||||||
|
assertMoveError(t, b, "Ra3", "no rook found that can move to a3")
|
||||||
|
|
||||||
|
assertParse(t, b, "e3 e6 a4 d6 Ra3")
|
||||||
|
|
||||||
|
// path blocked by pawn at d3
|
||||||
|
assertMoveError(t, b, "Rh3", "no rook found that can move to h3")
|
||||||
|
|
||||||
|
// ambiguous moves
|
||||||
|
b = chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "a4 e6 h4 e5 Ra3 e4")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Rh3", "move ambiguous: 2 rooks can move to h3")
|
||||||
|
assertParse(t, b, "Rhh3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveQueen(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
assertMoveError(t, b, "Qd3", "no queen found that can move to d3")
|
assertMoveError(t, b, "Qd3", "no queen found that can move to d3")
|
||||||
|
|
||||||
b.Move("d4")
|
assertParse(t, b, "d4")
|
||||||
b.Move("d5")
|
assertParse(t, b, "d5")
|
||||||
|
|
||||||
b.Move("Qd3")
|
assertParse(t, b, "Qd3")
|
||||||
|
|
||||||
assertPiece(t, b, "d3", chess.Queen, chess.Light)
|
assertPiece(t, b, "d3", chess.Queen, chess.Light)
|
||||||
assertNoPiece(t, b, "d1")
|
assertNoPiece(t, b, "d1")
|
||||||
|
|
||||||
b.Move("Qd6")
|
assertParse(t, b, "Qd6")
|
||||||
|
|
||||||
assertPiece(t, b, "d6", chess.Queen, chess.Dark)
|
assertPiece(t, b, "d6", chess.Queen, chess.Dark)
|
||||||
assertNoPiece(t, b, "d8")
|
assertNoPiece(t, b, "d8")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveQueenInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Qd2", "d2 blocked by white pawn")
|
||||||
|
assertMoveError(t, b, "Qd1", "d1 blocked by white queen")
|
||||||
|
assertMoveError(t, b, "Qe1", "e1 blocked by white king")
|
||||||
|
assertMoveError(t, b, "Qc1", "c1 blocked by white bishop")
|
||||||
|
|
||||||
|
// path blocked by white pawn at d2
|
||||||
|
assertMoveError(t, b, "Qd3", "no queen found that can move to d3")
|
||||||
|
|
||||||
|
// TODO: ambiguous queen moves require pawn promotion
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveKing(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Ke2 Ke7 Kf3 Kd6 Kg3 Kc6")
|
||||||
|
assertNoPiece(t, b, "e1")
|
||||||
|
assertNoPiece(t, b, "e8")
|
||||||
|
assertNoPiece(t, b, "e2")
|
||||||
|
assertNoPiece(t, b, "e7")
|
||||||
|
assertNoPiece(t, b, "f3")
|
||||||
|
assertNoPiece(t, b, "d6")
|
||||||
|
|
||||||
|
assertPiece(t, b, "g3", chess.King, chess.Light)
|
||||||
|
assertPiece(t, b, "c6", chess.King, chess.Dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardMoveKingInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Ke1", "e1 blocked by white king")
|
||||||
|
assertMoveError(t, b, "Ke2", "e2 blocked by white pawn")
|
||||||
|
assertMoveError(t, b, "Ke3", "no king found that can move to e3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardCheck(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assert.False(t, b.InCheck())
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Qh5 Nc6 Qxf7")
|
||||||
|
|
||||||
|
assert.True(t, b.InCheck())
|
||||||
|
assert.True(t, strings.HasSuffix(b.Moves[len(b.Moves)-1], "+"), "check move should end with +")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Nf6", "invalid move Nf6: king is in check")
|
||||||
|
assertMoveError(t, b, "Ke7", "invalid move Ke7: king is in check")
|
||||||
|
|
||||||
|
assertParse(t, b, "Kxf7")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardPin(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "d4 e5 Nc3 Bb4")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "Ne4", "invalid move Ne4: king is in check")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardCastle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Nf3 Nf6 Be2 Be7 O-O")
|
||||||
|
|
||||||
|
assertPiece(t, b, "f1", chess.Rook, chess.Light)
|
||||||
|
assertPiece(t, b, "g1", chess.King, chess.Light)
|
||||||
|
assertNoPiece(t, b, "h1")
|
||||||
|
assertNoPiece(t, b, "e1")
|
||||||
|
|
||||||
|
assertParse(t, b, "O-O")
|
||||||
|
|
||||||
|
assertPiece(t, b, "f8", chess.Rook, chess.Dark)
|
||||||
|
assertPiece(t, b, "g8", chess.King, chess.Dark)
|
||||||
|
assertNoPiece(t, b, "h8")
|
||||||
|
assertNoPiece(t, b, "e8")
|
||||||
|
|
||||||
|
b = chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "e4 e5 Qg4 d6 d3 d5 Be3 d4 Nc3 dxe3 O-O-O")
|
||||||
|
|
||||||
|
assertPiece(t, b, "d1", chess.Rook, chess.Light)
|
||||||
|
assertPiece(t, b, "c1", chess.King, chess.Light)
|
||||||
|
assertNoPiece(t, b, "a1")
|
||||||
|
assertNoPiece(t, b, "e1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoardParseAlgebraicNotation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assertParse(t, b, "1.d4 d5 2.c4 e6")
|
||||||
|
|
||||||
|
assertPiece(t, b, "d4", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "d5", chess.Pawn, chess.Dark)
|
||||||
|
assertPiece(t, b, "c4", chess.Pawn, chess.Light)
|
||||||
|
assertPiece(t, b, "e6", chess.Pawn, chess.Dark)
|
||||||
|
|
||||||
|
assertNoPiece(t, b, "d2")
|
||||||
|
assertNoPiece(t, b, "d7")
|
||||||
|
assertNoPiece(t, b, "c2")
|
||||||
|
assertNoPiece(t, b, "e7")
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertParse(t *testing.T, b *chess.Board, moves string) {
|
||||||
|
assert.NoError(t, b.Parse(moves))
|
||||||
|
}
|
||||||
|
|
||||||
func assertPiece(t *testing.T, b *chess.Board, position string, name chess.PieceName, color chess.Color) {
|
func assertPiece(t *testing.T, b *chess.Board, position string, name chess.PieceName, color chess.Color) {
|
||||||
p := b.At(position)
|
p := b.At(position)
|
||||||
|
|
||||||
|
19
chess/game_test.go
Normal file
19
chess/game_test.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package chess_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ekzyis/chessbot/chess"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGame001(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
// this used to not parse because of the + at the end
|
||||||
|
assertParse(t, b, "d4 d5 Bf4 Nf6 e3 Ne4 Nc3 Nf2 Kxf2 e6 Qg4 Be7 Re1 O-O Kg3 Bh4+")
|
||||||
|
|
||||||
|
// this used to crash because of an out of bounds error (position was parsed as '6')
|
||||||
|
assertMoveError(t, b, "Qex6", "square does not exist")
|
||||||
|
}
|
@ -16,6 +16,34 @@ type Piece struct {
|
|||||||
Image image.Image
|
Image image.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Piece) String() string {
|
||||||
|
n := ""
|
||||||
|
switch p.Name {
|
||||||
|
case Pawn:
|
||||||
|
n = "pawn"
|
||||||
|
case Knight:
|
||||||
|
n = "knight"
|
||||||
|
case Bishop:
|
||||||
|
n = "bishop"
|
||||||
|
case Rook:
|
||||||
|
n = "rook"
|
||||||
|
case Queen:
|
||||||
|
n = "queen"
|
||||||
|
case King:
|
||||||
|
n = "king"
|
||||||
|
}
|
||||||
|
|
||||||
|
c := ""
|
||||||
|
switch p.Color {
|
||||||
|
case Light:
|
||||||
|
c = "white"
|
||||||
|
case Dark:
|
||||||
|
c = "black"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s", c, n)
|
||||||
|
}
|
||||||
|
|
||||||
type PieceName string
|
type PieceName string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
6
chess/square.go
Normal file
6
chess/square.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package chess
|
||||||
|
|
||||||
|
type Square struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
}
|
108
db/db.go
Normal file
108
db/db.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
sn "github.com/ekzyis/snappy"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db = getDb()
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDb() *sql.DB {
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if db, err = sql.Open("sqlite3", "chessbot.sqlite3?_foreign_keys=on"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err = migrate(db); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrate(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS items (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
parent_id INTEGER REFERENCES items(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ItemHasReply(parentId int, userId int) (bool, error) {
|
||||||
|
var (
|
||||||
|
count int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = db.QueryRow(`SELECT COUNT(1) FROM items WHERE parent_id = ? AND user_id = ?`, parentId, userId).Scan(&count); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if parent already exists, this means we ignored it
|
||||||
|
if err = db.QueryRow(`SELECT COUNT(1) FROM items WHERE id = ?`, parentId).Scan(&count); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
log.Printf("ignoring known item %d", parentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertItem(item *sn.Item) error {
|
||||||
|
if _, err := db.Exec(``+
|
||||||
|
`INSERT INTO items(id, user_id, text, parent_id) VALUES (?, ?, ?, NULLIF(?, 0)) `+
|
||||||
|
`ON CONFLICT DO UPDATE SET text = EXCLUDED.text, updated_at = CURRENT_TIMESTAMP`,
|
||||||
|
item.Id, item.User.Id, item.Text, item.ParentId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetThread(id int) ([]sn.Item, error) {
|
||||||
|
var (
|
||||||
|
items []sn.Item
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
var item sn.Item
|
||||||
|
item.ParentId = id
|
||||||
|
|
||||||
|
for item.ParentId > 0 {
|
||||||
|
// TODO: can't select created_at because sqlite3 doesn't support timestamps natively
|
||||||
|
// see https://github.com/mattn/go-sqlite3/issues/142
|
||||||
|
if err = db.QueryRow(
|
||||||
|
`SELECT id, user_id, text, COALESCE(parent_id, 0) FROM items WHERE id = ?`, item.ParentId).
|
||||||
|
Scan(&item.Id, &item.User.Id, &item.Text, &item.ParentId); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, errors.New("item not found in db")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append([]sn.Item{item}, items...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
5
go.mod
5
go.mod
@ -1,8 +1,10 @@
|
|||||||
module github.com/ekzyis/sn-chess
|
module github.com/ekzyis/chessbot
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ekzyis/snappy v0.5.5-0.20240923014110-b880c1256e13
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.23
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/image v0.20.0
|
golang.org/x/image v0.20.0
|
||||||
)
|
)
|
||||||
@ -10,5 +12,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -1,5 +1,9 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/ekzyis/snappy v0.5.5-0.20240923014110-b880c1256e13 h1:yxDqMo4HzCVzhxPNTBpST0KVVHVMwGouzwfXsb4WYw4=
|
||||||
|
github.com/ekzyis/snappy v0.5.5-0.20240923014110-b880c1256e13/go.mod h1:UksYI0dU0+cnzz0LQjWB1P0QQP/ghx47e4atP99a5Lk=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
@ -8,5 +12,7 @@ golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
|||||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
271
main.go
271
main.go
@ -1,13 +1,278 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ekzyis/sn-chess/chess"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ekzyis/chessbot/chess"
|
||||||
|
"github.com/ekzyis/chessbot/db"
|
||||||
|
"github.com/ekzyis/chessbot/sn"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
c = sn.GetClient()
|
||||||
|
// TODO: fetch our id from SN API
|
||||||
|
meId = 21858
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
for {
|
||||||
|
tickGameStart(c)
|
||||||
|
tickGameProgress(c)
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tickGameStart(c *sn.Client) {
|
||||||
var (
|
var (
|
||||||
b = chess.NewBoard()
|
mentions []sn.Notification
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
b.Save("board.png")
|
if mentions, err = c.Mentions(); err != nil {
|
||||||
|
log.Printf("failed to fetch mentions: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("fetched %d mention(s)\n", len(mentions))
|
||||||
|
|
||||||
|
for _, n := range mentions {
|
||||||
|
|
||||||
|
if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil {
|
||||||
|
log.Printf("failed to check for existing reply to game start in item %d: %v\n", n.Item.Id, err)
|
||||||
|
continue
|
||||||
|
} else if exists {
|
||||||
|
// TODO: check if move changed
|
||||||
|
log.Printf("reply to game start in item %d already exists\n", n.Item.Id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = handleGameStart(&n.Item); err != nil {
|
||||||
|
|
||||||
|
// don't reply to mentions that we failed to parse as a game start
|
||||||
|
// to support unrelated mentions
|
||||||
|
if err.Error() == "failed to parse game start" {
|
||||||
|
log.Printf("ignoring error for item %d: %v\n", n.Item.Id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err2 := createComment(n.Item.Id, fmt.Sprintf("`%v`", err)); err2 != nil {
|
||||||
|
log.Printf("failed to reply with error to item %d: %v\n", n.Item.Id, err2)
|
||||||
|
} else {
|
||||||
|
log.Printf("replied to game start in item %d with error: %v\n", n.Item.Id, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("started new game via item %d\n", n.Item.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tickGameProgress(c *sn.Client) {
|
||||||
|
var (
|
||||||
|
replies []sn.Notification
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if replies, err = c.Replies(); err != nil {
|
||||||
|
log.Printf("failed to fetch replies: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("fetched %d replies\n", len(replies))
|
||||||
|
|
||||||
|
for _, n := range replies {
|
||||||
|
|
||||||
|
if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil {
|
||||||
|
log.Printf("failed to check for existing reply to game update in item %d: %v\n", n.Item.Id, err)
|
||||||
|
continue
|
||||||
|
} else if exists {
|
||||||
|
// TODO: check if move changed
|
||||||
|
log.Printf("reply to game update in item %d already exists\n", n.Item.Id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = handleGameProgress(&n.Item); err != nil {
|
||||||
|
|
||||||
|
if err.Error() == "failed to parse game update" {
|
||||||
|
log.Printf("ignoring error for item %d: %v\n", n.Item.Id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err2 := createComment(n.Item.Id, fmt.Sprintf("`%v`", err)); err2 != nil {
|
||||||
|
log.Printf("failed to reply with error to item %d: %v\n", n.Item.Id, err2)
|
||||||
|
} else {
|
||||||
|
log.Printf("replied to game start in item %d with error: %v\n", n.Item.Id, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("updated game via item %d\n", n.Item.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGameStart(req *sn.Item) error {
|
||||||
|
var (
|
||||||
|
move string
|
||||||
|
b *chess.Board
|
||||||
|
imgUrl string
|
||||||
|
res string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Immediately save game start request to db so we can store our reply to it in case of error.
|
||||||
|
// We set parentId to 0 such that parent_id will be NULL in the db and not hit foreign key constraints.
|
||||||
|
req.ParentId = 0
|
||||||
|
|
||||||
|
if err = db.InsertItem(req); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert item %d into db: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if move, err = parseGameStart(req.Text); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create board with initial move(s)
|
||||||
|
if b, err = chess.NewGame(move); err != nil {
|
||||||
|
if rand.Float32() > 0.99 {
|
||||||
|
// easter egg error message
|
||||||
|
return errors.New("Nice try, fed.")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to create new game from item %d: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload image of board
|
||||||
|
if imgUrl, err = c.UploadImage(b.Image()); err != nil {
|
||||||
|
return fmt.Errorf("failed to upload image for item %d: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reply with algebraic notation and image
|
||||||
|
res = strings.Trim(fmt.Sprintf("%s\n\n%s", b.AlgebraicNotation(), imgUrl), " ")
|
||||||
|
if _, err = createComment(req.Id, res); err != nil {
|
||||||
|
return fmt.Errorf("failed to reply to item %d: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGameProgress(req *sn.Item) error {
|
||||||
|
var (
|
||||||
|
thread []sn.Item
|
||||||
|
b = chess.NewBoard()
|
||||||
|
move string = strings.Trim(req.Text, " ")
|
||||||
|
imgUrl string
|
||||||
|
res string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// immediately save game update request to db so we can store our reply to it in case of error
|
||||||
|
if err = db.InsertItem(req); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert item %d into db: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch thread to reconstruct all moves so far
|
||||||
|
if thread, err = db.GetThread(req.ParentId); err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch thread for item %d: %v\n", req.ParentId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range thread {
|
||||||
|
if item.User.Id == meId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var moves string
|
||||||
|
if moves, err = parseGameProgress(item.Text); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse and execute existing moves
|
||||||
|
if err = b.Parse(moves); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse and execute new move
|
||||||
|
|
||||||
|
if move, err = parseGameProgress(move); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = b.Parse(move); err != nil {
|
||||||
|
if rand.Float32() > 0.99 {
|
||||||
|
// easter egg error message
|
||||||
|
return errors.New("Nice try, fed.")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload image of updated board
|
||||||
|
if imgUrl, err = c.UploadImage(b.Image()); err != nil {
|
||||||
|
return fmt.Errorf("failed to upload image for item %d: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reply with algebraic notation and image
|
||||||
|
res = strings.Trim(fmt.Sprintf("%s\n\n%s", b.AlgebraicNotation(), imgUrl), " ")
|
||||||
|
if _, err = createComment(req.Id, res); err != nil {
|
||||||
|
return fmt.Errorf("failed to reply to item %d: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createComment(parentId int, text string) (*sn.Item, error) {
|
||||||
|
var (
|
||||||
|
commentId int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if commentId, err = c.CreateComment(parentId, text); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to reply to item %d: %v\n", parentId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var comment *sn.Item
|
||||||
|
if comment, err = c.Item(commentId); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch item %d: %v\n", commentId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.InsertItem(comment); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to insert item %d into db: %v\n", err, comment.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGameStart(input string) (string, error) {
|
||||||
|
for _, line := range strings.Split(input, "\n") {
|
||||||
|
line = strings.Trim(line, " ")
|
||||||
|
|
||||||
|
if !strings.HasPrefix(line, "@chess") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(strings.ReplaceAll(line, "@chess", ""), " "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("failed to parse game start")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGameProgress(input string) (string, error) {
|
||||||
|
lines := strings.Split(input, "\n")
|
||||||
|
words := strings.Split(input, " ")
|
||||||
|
|
||||||
|
if len(lines) == 1 && len(words) == 1 {
|
||||||
|
return strings.Trim(input, " "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(input, "\n") {
|
||||||
|
if !strings.Contains(line, "@chess") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(strings.ReplaceAll(line, "@chess", ""), " "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("failed to parse game update")
|
||||||
}
|
}
|
||||||
|
64
sn/sn.go
Normal file
64
sn/sn.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package sn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
snappy "github.com/ekzyis/snappy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client = snappy.Client
|
||||||
|
type Notification = snappy.Notification
|
||||||
|
type Item = snappy.Item
|
||||||
|
|
||||||
|
var (
|
||||||
|
c *Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetClient() *Client {
|
||||||
|
loadEnv()
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
c = snappy.NewClient(
|
||||||
|
snappy.WithBaseUrl(os.Getenv("SN_BASE_URL")),
|
||||||
|
snappy.WithApiKey(os.Getenv("SN_API_KEY")),
|
||||||
|
snappy.WithMediaUrl(os.Getenv("SN_MEDIA_URL")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEnv() {
|
||||||
|
var (
|
||||||
|
f *os.File
|
||||||
|
s *bufio.Scanner
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if f, err = os.Open(".env"); err != nil {
|
||||||
|
log.Fatalf("error opening .env: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
s = bufio.NewScanner(f)
|
||||||
|
s.Split(bufio.ScanLines)
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
|
||||||
|
// Check if we have exactly 2 parts (key and value)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
os.Setenv(parts[0], parts[1])
|
||||||
|
} else {
|
||||||
|
log.Fatalf(".env: invalid line: %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for errors during scanning
|
||||||
|
if err = s.Err(); err != nil {
|
||||||
|
fmt.Println("error scanning .env:", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user