Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
a83ccdd054 | |||
|
d5723e7e17 | ||
ec8384f282 | |||
e7498352b8 | |||
da5f67df46 | |||
8347bf0cbc | |||
7a5b044b9d | |||
392cf19ae6 | |||
c1ad076828 | |||
b6991ce3e5 | |||
2037fb0578 | |||
894ad95d73 | |||
bd24198b05 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
!assets/*.png
|
!assets/*.png
|
||||||
.env
|
.env
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
chessbot
|
||||||
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.PHONY: build test
|
||||||
|
|
||||||
|
SOURCES := main.go $(shell find chess -name '*.go')
|
||||||
|
|
||||||
|
build: chessbot
|
||||||
|
|
||||||
|
chessbot: $(SOURCES)
|
||||||
|
go build -o chessbot .
|
||||||
|
|
||||||
|
test:
|
||||||
|
## -count=1 is used to disable cache
|
||||||
|
## use -run <regexp> to only run tests that match <regexp>
|
||||||
|
go test ./chess -v -count=1
|
@ -12,13 +12,19 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
"golang.org/x/image/font/basicfont"
|
"golang.org/x/image/font/basicfont"
|
||||||
|
"golang.org/x/image/font/opentype"
|
||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Board struct {
|
type Board struct {
|
||||||
tiles [8][8]*Piece
|
tiles [8][8]*Piece
|
||||||
turn Color
|
turn Color
|
||||||
Moves []string
|
Moves []string
|
||||||
|
moveIndicators []Tile
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tile struct {
|
||||||
|
X, Y int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBoard() *Board {
|
func NewBoard() *Board {
|
||||||
@ -101,7 +107,7 @@ func (b *Board) Image() *image.RGBA {
|
|||||||
x := xi * 128
|
x := xi * 128
|
||||||
y := yi * 128
|
y := yi * 128
|
||||||
rect = image.Rect(x, y, x+128, y+128)
|
rect = image.Rect(x, y, x+128, y+128)
|
||||||
bg = image.NewUniform(getTileColor(xi, yi))
|
bg = image.NewUniform(getTileColor(b, xi, yi))
|
||||||
draw.Draw(img, rect, bg, p, draw.Src)
|
draw.Draw(img, rect, bg, p, draw.Src)
|
||||||
|
|
||||||
piece = b.tiles[xi][yi]
|
piece = b.tiles[xi][yi]
|
||||||
@ -186,17 +192,23 @@ func drawCoordinate(img *image.RGBA, x, y int, flipped bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawString := func(s string, origin fixed.Point26_6) {
|
drawString := func(s string, origin fixed.Point26_6) {
|
||||||
color := getTileColor(x, y)
|
color := getTileColor(nil, x, y)
|
||||||
if !flipped && color == Light {
|
if !flipped && color == Light {
|
||||||
color = Dark
|
color = Dark
|
||||||
} else if !flipped {
|
} else if !flipped {
|
||||||
color = Light
|
color = Light
|
||||||
}
|
}
|
||||||
// TODO: use SN font and make it bold
|
|
||||||
|
face, err := loadFontFace("lightningvolt.ttf")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error loading font: %v\n", err)
|
||||||
|
face = basicfont.Face7x13
|
||||||
|
}
|
||||||
|
|
||||||
d := &font.Drawer{
|
d := &font.Drawer{
|
||||||
Dst: img,
|
Dst: img,
|
||||||
Src: image.NewUniform(color),
|
Src: image.NewUniform(color),
|
||||||
Face: basicfont.Face7x13,
|
Face: face,
|
||||||
Dot: origin,
|
Dot: origin,
|
||||||
}
|
}
|
||||||
d.DrawString(s)
|
d.DrawString(s)
|
||||||
@ -209,11 +221,36 @@ func drawCoordinate(img *image.RGBA, x, y int, flipped bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if row != "" {
|
if row != "" {
|
||||||
origin = fixed.P((x+1)*128-12, y*128+15)
|
origin = fixed.P((x+1)*128-20, y*128+23)
|
||||||
drawString(row, origin)
|
drawString(row, origin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadFontFace(path string) (font.Face, error) {
|
||||||
|
fontBytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ttfFont, err := opentype.Parse(fontBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
faceOptions := &opentype.FaceOptions{
|
||||||
|
Size: 32,
|
||||||
|
DPI: 72,
|
||||||
|
Hinting: font.HintingNone,
|
||||||
|
}
|
||||||
|
|
||||||
|
fontFace, err := opentype.NewFace(ttfFont, faceOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontFace, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Board) SetPiece(name PieceName, color Color, position string) error {
|
func (b *Board) SetPiece(name PieceName, color Color, position string) error {
|
||||||
var (
|
var (
|
||||||
piece *Piece
|
piece *Piece
|
||||||
@ -360,7 +397,7 @@ func (b *Board) Move(move string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure the move is marked as a check if it was
|
// make sure the move is marked as a check if it was
|
||||||
if b.InCheck() && !strings.HasSuffix(move, "+") {
|
if b.InCheck() && !strings.HasSuffix(move, "+") && !strings.HasSuffix(move, "#") {
|
||||||
move += "+"
|
move += "+"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,6 +416,7 @@ func parseMove(move string) (string, int, int, string, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
move = strings.TrimSuffix(move, "+")
|
move = strings.TrimSuffix(move, "+")
|
||||||
|
move = strings.TrimSuffix(move, "#")
|
||||||
|
|
||||||
if move == "O-O" {
|
if move == "O-O" {
|
||||||
return "K", 5, 7, "g1", nil
|
return "K", 5, 7, "g1", nil
|
||||||
@ -801,6 +839,7 @@ func (b *Board) movePawn(position string, fromX int, fromY int, promotion string
|
|||||||
if promotion != "" {
|
if promotion != "" {
|
||||||
return b.promotePawn(toX, toY, promotion)
|
return b.promotePawn(toX, toY, promotion)
|
||||||
}
|
}
|
||||||
|
b.moveIndicators = []Tile{{fromX, fromY}, {toX, toY}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -823,6 +862,7 @@ func (b *Board) movePawn(position string, fromX int, fromY int, promotion string
|
|||||||
if promotion != "" {
|
if promotion != "" {
|
||||||
return b.promotePawn(toX, toY, promotion)
|
return b.promotePawn(toX, toY, promotion)
|
||||||
}
|
}
|
||||||
|
b.moveIndicators = []Tile{{toX, yPrev}, {toX, toY}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -839,6 +879,7 @@ func (b *Board) movePawn(position string, fromX int, fromY int, promotion string
|
|||||||
if promotion != "" {
|
if promotion != "" {
|
||||||
return b.promotePawn(toX, toY, promotion)
|
return b.promotePawn(toX, toY, promotion)
|
||||||
}
|
}
|
||||||
|
b.moveIndicators = []Tile{{toX, yPrev}, {toX, toY}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -948,6 +989,7 @@ func (b *Board) moveRook(position string, queen bool, fromX int, fromY int) erro
|
|||||||
p = b.getPiece(xPrev, yPrev)
|
p = b.getPiece(xPrev, yPrev)
|
||||||
b.tiles[x][y] = p
|
b.tiles[x][y] = p
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -978,6 +1020,7 @@ func (b *Board) moveBishop(position string, queen bool) error {
|
|||||||
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
// direction blocked by other piece
|
// direction blocked by other piece
|
||||||
@ -996,6 +1039,7 @@ func (b *Board) moveBishop(position string, queen bool) error {
|
|||||||
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
@ -1013,6 +1057,7 @@ func (b *Board) moveBishop(position string, queen bool) error {
|
|||||||
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
@ -1030,6 +1075,7 @@ func (b *Board) moveBishop(position string, queen bool) error {
|
|||||||
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
if ((!queen && piece.Name == Bishop) || (queen && piece.Name == Queen)) && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
@ -1123,6 +1169,7 @@ func (b *Board) moveKnight(position string, fromX int, fromY int) error {
|
|||||||
p = b.getPiece(xPrev, yPrev)
|
p = b.getPiece(xPrev, yPrev)
|
||||||
b.tiles[x][y] = p
|
b.tiles[x][y] = p
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1194,6 +1241,8 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
b.tiles[5][y] = rook
|
b.tiles[5][y] = rook
|
||||||
b.tiles[7][y] = nil
|
b.tiles[7][y] = nil
|
||||||
|
|
||||||
|
b.moveIndicators = []Tile{{4, y}, {x, y}, {5, y}, {7, y}}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1227,6 +1276,8 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
b.tiles[3][y] = rook
|
b.tiles[3][y] = rook
|
||||||
b.tiles[0][y] = nil
|
b.tiles[0][y] = nil
|
||||||
|
|
||||||
|
b.moveIndicators = []Tile{{2, y}, {4, y}, {3, y}, {0, y}}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1238,6 +1289,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1248,6 +1300,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1258,6 +1311,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1268,6 +1322,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1278,6 +1333,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1288,6 +1344,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1298,6 +1355,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1308,6 +1366,7 @@ func (b *Board) moveKing(position string, castle bool) error {
|
|||||||
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
if piece != nil && piece.Name == King && piece.Color == b.turn {
|
||||||
b.tiles[xPrev][yPrev] = nil
|
b.tiles[xPrev][yPrev] = nil
|
||||||
b.tiles[x][y] = piece
|
b.tiles[x][y] = piece
|
||||||
|
b.moveIndicators = []Tile{{xPrev, yPrev}, {x, y}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1392,8 +1451,25 @@ func (b *Board) getCollision(position string) (*Piece, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTileColor(x, y int) Color {
|
func getTileColor(b *Board, x, y int) Color {
|
||||||
if x%2 == y%2 {
|
|
||||||
|
lightTile := x%2 == y%2
|
||||||
|
|
||||||
|
if b != nil {
|
||||||
|
// highlight move
|
||||||
|
// TODO: refactor using alpha channels
|
||||||
|
for _, t := range b.moveIndicators {
|
||||||
|
if t.X == x && t.Y == y {
|
||||||
|
if lightTile {
|
||||||
|
return LightGreen
|
||||||
|
} else {
|
||||||
|
return DarkGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lightTile {
|
||||||
return Light
|
return Light
|
||||||
} else {
|
} else {
|
||||||
return Dark
|
return Dark
|
||||||
|
@ -472,6 +472,22 @@ func TestBoardCheck(t *testing.T) {
|
|||||||
assertParse(t, b, "Kxf7")
|
assertParse(t, b, "Kxf7")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoardCheckmate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := chess.NewBoard()
|
||||||
|
|
||||||
|
assert.False(t, b.InCheck())
|
||||||
|
|
||||||
|
// fool's mate
|
||||||
|
assertParse(t, b, "f3 e6 g4 Qh4#")
|
||||||
|
|
||||||
|
assert.True(t, b.InCheck())
|
||||||
|
assert.True(t, strings.HasSuffix(b.Moves[len(b.Moves)-1], "#"), "checkmate move should end with #")
|
||||||
|
|
||||||
|
assertMoveError(t, b, "a3", "invalid move a3: king is in check")
|
||||||
|
}
|
||||||
|
|
||||||
func TestBoardPin(t *testing.T) {
|
func TestBoardPin(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -58,8 +58,10 @@ const (
|
|||||||
type Color color.Color
|
type Color color.Color
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Light Color = color.RGBA{240, 217, 181, 255}
|
Light Color = color.RGBA{240, 217, 181, 255}
|
||||||
Dark Color = color.RGBA{181, 136, 99, 255}
|
Dark Color = color.RGBA{181, 136, 99, 255}
|
||||||
|
LightGreen Color = color.RGBA{205, 210, 106, 255}
|
||||||
|
DarkGreen Color = color.RGBA{170, 162, 58, 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewPiece(name PieceName, color Color) (*Piece, error) {
|
func NewPiece(name PieceName, color Color) (*Piece, error) {
|
||||||
|
4
db/db.go
4
db/db.go
@ -62,10 +62,6 @@ func ItemHasReply(parentId int, userId int) (bool, error) {
|
|||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
log.Printf("ignoring known item %d", parentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
go.mod
3
go.mod
@ -3,7 +3,7 @@ module github.com/ekzyis/chessbot
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ekzyis/snappy v0.6.2
|
github.com/ekzyis/snappy v0.7.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.23
|
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
|
||||||
@ -12,6 +12,7 @@ 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
|
||||||
|
golang.org/x/text v0.18.0 // indirect
|
||||||
gopkg.in/guregu/null.v4 v4.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,7 +1,7 @@
|
|||||||
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.6.2 h1:iOPgoS0cSUNk8leQJku0bBgnsha/+DcYUwTYqGLINY4=
|
github.com/ekzyis/snappy v0.7.0 h1:RcFTUHdZFTBFOnh6cG9HCL/RPK6L13qdxDrjBNZFjEM=
|
||||||
github.com/ekzyis/snappy v0.6.2/go.mod h1:UksYI0dU0+cnzz0LQjWB1P0QQP/ghx47e4atP99a5Lk=
|
github.com/ekzyis/snappy v0.7.0/go.mod h1:UksYI0dU0+cnzz0LQjWB1P0QQP/ghx47e4atP99a5Lk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
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/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=
|
||||||
@ -10,6 +10,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
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=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
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 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
|
||||||
|
BIN
lightningvolt.ttf
Normal file
BIN
lightningvolt.ttf
Normal file
Binary file not shown.
136
main.go
136
main.go
@ -14,21 +14,54 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
c = sn.GetClient()
|
c = sn.GetClient()
|
||||||
// TODO: fetch our id from SN API
|
me *sn.User
|
||||||
// prod: 25176 | local: 21858
|
|
||||||
meId = 25176
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
updateMe()
|
||||||
tickGameStart(c)
|
tickGameStart(c)
|
||||||
tickGameProgress(c)
|
tickGameProgress(c)
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMe() {
|
||||||
|
var (
|
||||||
|
oldMe *sn.User
|
||||||
|
err error
|
||||||
|
warnThreshold = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
maybeWarn := func() {
|
||||||
|
if me.Privates.Sats < warnThreshold {
|
||||||
|
log.Printf("~~~ warning: low balance ~~~\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if me == nil {
|
||||||
|
// make sure first update is successful
|
||||||
|
if me, err = c.Me(); err != nil {
|
||||||
|
log.Fatalf("failed to fetch me: %v\n", err)
|
||||||
|
}
|
||||||
|
log.Printf("fetched me: id=%d name=%s balance=%d\n", me.Id, me.Name, me.Privates.Sats)
|
||||||
|
maybeWarn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldMe = me
|
||||||
|
if me, err = c.Me(); err != nil {
|
||||||
|
log.Printf("failed to update me: %v\n", err)
|
||||||
|
me = oldMe
|
||||||
|
} else {
|
||||||
|
log.Printf("updated me. balance: %d\n", me.Privates.Sats)
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeWarn()
|
||||||
|
}
|
||||||
|
|
||||||
func tickGameStart(c *sn.Client) {
|
func tickGameStart(c *sn.Client) {
|
||||||
var (
|
var (
|
||||||
mentions []sn.Notification
|
mentions []sn.Notification
|
||||||
@ -44,36 +77,22 @@ func tickGameStart(c *sn.Client) {
|
|||||||
|
|
||||||
for _, n := range mentions {
|
for _, n := range mentions {
|
||||||
|
|
||||||
// we only care about current notifications
|
if !isRecent(n.Item.CreatedAt) {
|
||||||
x := time.Now().Add(-30 * time.Second)
|
|
||||||
if n.Item.CreatedAt.Before(x) {
|
|
||||||
log.Printf("ignoring old mention %d\n", n.Item.Id)
|
log.Printf("ignoring old mention %d\n", n.Item.Id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil {
|
if handled, err := alreadyHandled(n.Item.Id); err != nil {
|
||||||
log.Printf("failed to check for existing reply to game start in item %d: %v\n", n.Item.Id, err)
|
log.Printf("failed to check for existing reply to game start in item %d: %v\n", n.Item.Id, err)
|
||||||
continue
|
continue
|
||||||
} else if exists {
|
} else if handled {
|
||||||
// TODO: check if move changed
|
// TODO: check if move changed
|
||||||
log.Printf("reply to game start in item %d already exists\n", n.Item.Id)
|
log.Printf("reply to game start in item %d already exists\n", n.Item.Id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = handleGameStart(&n.Item); err != nil {
|
if err = handleGameStart(&n.Item); err != nil {
|
||||||
|
handleError(&n.Item, err)
|
||||||
// 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 {
|
} else {
|
||||||
log.Printf("started new game via item %d\n", n.Item.Id)
|
log.Printf("started new game via item %d\n", n.Item.Id)
|
||||||
}
|
}
|
||||||
@ -95,34 +114,30 @@ func tickGameProgress(c *sn.Client) {
|
|||||||
|
|
||||||
for _, n := range replies {
|
for _, n := range replies {
|
||||||
|
|
||||||
// we only care about current notifications
|
if !isRecent(n.Item.CreatedAt) {
|
||||||
x := time.Now().Add(-30 * time.Second)
|
|
||||||
if n.Item.CreatedAt.Before(x) {
|
|
||||||
log.Printf("ignoring old reply %d\n", n.Item.Id)
|
log.Printf("ignoring old reply %d\n", n.Item.Id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists, err := db.ItemHasReply(n.Item.Id, meId); err != nil {
|
if handled, err := alreadyHandled(n.Item.Id); err != nil {
|
||||||
log.Printf("failed to check for existing reply to game update in item %d: %v\n", n.Item.Id, err)
|
log.Printf("failed to check for existing reply to game update in item %d: %v\n", n.Item.Id, err)
|
||||||
continue
|
continue
|
||||||
} else if exists {
|
} else if handled {
|
||||||
// TODO: check if move changed
|
// TODO: check if move changed
|
||||||
log.Printf("reply to game update in item %d already exists\n", n.Item.Id)
|
log.Printf("reply to game update in item %d already exists\n", n.Item.Id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parent, err := c.Item(n.Item.ParentId); err != nil {
|
||||||
|
log.Printf("failed to fetch parent %d of %d\n", n.Item.ParentId, n.Item.Id)
|
||||||
|
continue
|
||||||
|
} else if parent.User.Id != me.Id {
|
||||||
|
log.Printf("ignoring nested reply %d\n", n.Item.Id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err = handleGameProgress(&n.Item); err != nil {
|
if err = handleGameProgress(&n.Item); err != nil {
|
||||||
|
handleError(&n.Item, err)
|
||||||
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 {
|
} else {
|
||||||
log.Printf("updated game via item %d\n", n.Item.Id)
|
log.Printf("updated game via item %d\n", n.Item.Id)
|
||||||
}
|
}
|
||||||
@ -164,8 +179,15 @@ func handleGameStart(req *sn.Item) error {
|
|||||||
return fmt.Errorf("failed to upload image for item %d: %v\n", req.Id, err)
|
return fmt.Errorf("failed to upload image for item %d: %v\n", req.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reply with algebraic notation and image
|
// reply with algebraic notation, image and info
|
||||||
res = strings.Trim(fmt.Sprintf("%s\n\n%s", b.AlgebraicNotation(), imgUrl), " ")
|
infoMove := "e4"
|
||||||
|
if len(b.Moves) > 0 {
|
||||||
|
infoMove = "e5"
|
||||||
|
}
|
||||||
|
info := fmt.Sprintf("_A new chess game has been started!_\n\n"+
|
||||||
|
"_Reply with a move like `%s` to continue the game. "+
|
||||||
|
"See [here](https://stacker.news/chess#how-to-continue) for details._", infoMove)
|
||||||
|
res = strings.Trim(fmt.Sprintf("%s\n\n%s\n\n%s", b.AlgebraicNotation(), imgUrl, info), " ")
|
||||||
if _, err = createComment(req.Id, res); err != nil {
|
if _, err = createComment(req.Id, res); err != nil {
|
||||||
return fmt.Errorf("failed to reply to item %d: %v\n", req.Id, err)
|
return fmt.Errorf("failed to reply to item %d: %v\n", req.Id, err)
|
||||||
}
|
}
|
||||||
@ -194,7 +216,7 @@ func handleGameProgress(req *sn.Item) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range thread {
|
for _, item := range thread {
|
||||||
if item.User.Id == meId {
|
if item.User.Id == me.Id {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +259,27 @@ func handleGameProgress(req *sn.Item) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleError(req *sn.Item, err error) {
|
||||||
|
|
||||||
|
// 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", req.Id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() == "failed to parse game update" {
|
||||||
|
log.Printf("ignoring error for item %d: %v\n", req.Id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err2 := createComment(req.Id, fmt.Sprintf("`%v`", err)); err2 != nil {
|
||||||
|
log.Printf("failed to reply with error to item %d: %v\n", req.Id, err2)
|
||||||
|
} else {
|
||||||
|
log.Printf("replied to game start in item %d with error: %v\n", req.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createComment(parentId int, text string) (*sn.Item, error) {
|
func createComment(parentId int, text string) (*sn.Item, error) {
|
||||||
var (
|
var (
|
||||||
commentId int
|
commentId int
|
||||||
@ -275,6 +318,8 @@ func parseGameStart(input string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseGameProgress(input string) (string, error) {
|
func parseGameProgress(input string) (string, error) {
|
||||||
|
input = strings.Trim(input, " ")
|
||||||
|
|
||||||
lines := strings.Split(input, "\n")
|
lines := strings.Split(input, "\n")
|
||||||
words := strings.Split(input, " ")
|
words := strings.Split(input, " ")
|
||||||
|
|
||||||
@ -295,3 +340,12 @@ func parseGameProgress(input string) (string, error) {
|
|||||||
|
|
||||||
return "", errors.New("failed to parse game update")
|
return "", errors.New("failed to parse game update")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isRecent(t time.Time) bool {
|
||||||
|
x := time.Now().Add(-30 * time.Second)
|
||||||
|
return t.After(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func alreadyHandled(id int) (bool, error) {
|
||||||
|
return db.ItemHasReply(id, me.Id)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user