commit 2b1d67f3cd7790f241a4dad2849fb3b9706c32d6 Author: Gregory Rudolph Date: Fri Oct 30 11:17:33 2020 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cb8a56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +games/* +.bashrc +chessbot.log +chessbot \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dabf0d9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Required for `go build` + export CGO_CFLAGS_ALLOW='-l.*' + export CGO_LDFLAGS_ALLOW='-I.*' diff --git a/chess.go b/chess.go new file mode 100644 index 0000000..db4def1 --- /dev/null +++ b/chess.go @@ -0,0 +1,140 @@ +package main + +import ( + "fmt" + "image/color" + "os" + "strings" + "time" + + "github.com/notnil/chess" + "github.com/notnil/chess/image" + "samhofi.us/x/keybase/v2/types/chat1" +) + +func chessCommand(m chat1.MsgSummary) { + defer log.PanicSafe() + defer saveConfig() + convID := string(m.ConvID) + parts := strings.Split(m.Content.Text.Body, " ") + if g, ok := config.Games[convID]; ok { + if len(parts) != 2 { + return + } + if parts[1] == "show" { + go showBoard(m, &g) + return + } + + submitMove(m, &g) + return + } + if len(parts) < 3 { + return + } + coin := r.Intn(100) + log.LogInfo(fmt.Sprintf("Coin toss resulted in %+v\n", coin)) + var game Game + if coin%2 == 0 { + if len(parts) == 4 && parts[3] == "heads" { + game.White = m.Sender.Username + game.Black = strings.Replace(parts[2], "@", "", -1) + + } else { + game.Black = m.Sender.Username + game.White = strings.Replace(parts[2], "@", "", -1) + } + } else { + if len(parts) == 4 && parts[3] == "heads" { + game.Black = m.Sender.Username + game.White = strings.Replace(parts[2], "@", "", -1) + } else { + game.White = m.Sender.Username + game.Black = strings.Replace(parts[2], "@", "", -1) + } + } + log.LogDebug("There are currently %+v games.\n", len(config.Games)) + g := chess.NewGame() + game.Game = g + game.ConvID = convID + game.Move = true + game.StartTime = time.Now() + config.Games[convID] = game + k.SendMessageByConvID(m.ConvID, fmt.Sprintf("I have created a new game for this conversation @%+v is playing as White, @%+v is playing as Black.", game.White, game.Black)) + k.SendMessageByConvID(m.ConvID, fmt.Sprintf("Possible commands are `@chessbot show` to show the board, or `@chessbot forfeit`. Otherwise I will be expecting a move in Algebraic Notation.")) + +} + +func showBoard(m chat1.MsgSummary, g *Game) { + defer log.PanicSafe() + // create file + f, err := os.Create(fmt.Sprintf("/home/chessbot/chessbot/games/%+v.svg", m.ConvID)) + if err != nil { + log.LogError(fmt.Sprintf("Error in ShowBoard os.Create: %+v\n", err)) + } + + // create board position + fenStr := g.Game.Position().String() + pos := &chess.Position{} + if err := pos.UnmarshalText([]byte(fenStr)); err != nil { + log.LogError(fmt.Sprintf("Error in ShowBoard pos.UnmasrhalText: %+v\n", err)) + } + if len(g.Game.Moves()) != 0 { + // write board SVG to file + yellow := color.RGBA{255, 255, 0, 1} + lastMove := g.Game.Moves()[len(g.Game.Moves())-1] + + mark := image.MarkSquares(yellow, lastMove.S1(), lastMove.S2()) + if err := image.SVG(f, pos.Board(), mark); err != nil { + log.LogError("Error in ShowBoard image.SVG Mark: %+v\n", err) + } + } else { + if err := image.SVG(f, pos.Board()); err != nil { + log.LogError("Error in ShowBoard image.SVG: %+v\n", err) + } + + } + f.Close() + if svgToPNG(fmt.Sprintf("/home/chessbot/chessbot/games/%+v", m.ConvID)) { + k.UploadToConversation(m.ConvID, "", fmt.Sprintf("/home/chessbot/chessbot/games/%+v.png", m.ConvID)) + } else { + k.SendMessageByConvID(m.ConvID, fmt.Sprintf("```%+v```", g.Game.Position().Board().Draw())) + } +} + +func submitMove(m chat1.MsgSummary, g *Game) { + defer log.PanicSafe() + parts := strings.Split(m.Content.Text.Body, " ") + if g.Move { + if m.Sender.Username != g.White { + k.SendMessageByConvID(m.ConvID, "Wait your turn.") + return + } + } else { + if m.Sender.Username != g.Black { + k.SendMessageByConvID(m.ConvID, "Wait your turn.") + return + } + } + if parts[1] == "forfeit" || parts[1] == "surrender" { + if g.Move { + g.Game.Resign(chess.White) + } else { + g.Game.Resign(chess.Black) + } + } else { + err := g.Game.MoveStr(parts[1]) + if err != nil { + k.SendMessageByConvID(m.ConvID, "There was an error with your move.") + log.LogDebug("%+v\n", err) + return + } + } + g.Move = !g.Move + + if g.Game.Outcome() != chess.NoOutcome { + k.SendMessageByConvID(m.ConvID, fmt.Sprintf("Game completed in %+v. %s by %s.\n", time.Since(g.StartTime), g.Game.Outcome(), g.Game.Method())) + delete(config.Games, g.ConvID) + } + go showBoard(m, g) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..6b03ba4 --- /dev/null +++ b/config.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/json" + "io/ioutil" +) + +func loadConfig() { + var c Config + confFile, _ := ioutil.ReadFile(configFile) + err := json.Unmarshal([]byte(confFile), &c) + if err != nil { + log.LogErrorType(err) + return + } + config = c + log.LogInfo("Setup completed using config file.") +} + +func saveConfig() { + defer log.PanicSafe() + file, err := json.Marshal(config) + if err != nil { + log.LogErrorType(err) + } + err = ioutil.WriteFile(configFile, file, 0600) + if err != nil { + log.LogErrorType(err) + } +} diff --git a/config.json.sample b/config.json.sample new file mode 100644 index 0000000..395e78f --- /dev/null +++ b/config.json.sample @@ -0,0 +1,11 @@ +{ + "Games": {}, + "LogOpts": { + "OutFile": "", + "KBTeam": "", + "KBChann": "", + "Level": 5, + "ProgName": "chessbot", + "UseStdout": true + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4c7cdfb --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.nightmare.haus/rudi/chessbot + +go 1.15 + +require ( + github.com/bwmarrin/discordgo v0.22.0 + github.com/llgcode/draw2d v0.0.0-20200930101115-bfaf5d914d1e + github.com/notnil/chess v1.2.0 + github.com/rogpeppe/misc v0.0.0-20200516092017-43a33a8f4c44 + github.com/rudi9719/loggy v0.0.0-20201030144506-35d9b032ab93 + samhofi.us/x/keybase/v2 v2.0.6 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4c363 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA= +github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM= +github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/llgcode/draw2d v0.0.0-20200930101115-bfaf5d914d1e h1:YRRazju3DMGuZTSWEj0nE2SCRcK3DW/qdHQ4UQx7sgs= +github.com/llgcode/draw2d v0.0.0-20200930101115-bfaf5d914d1e/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA= +github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= +github.com/notnil/chess v1.2.0 h1:5We6movgg1yzqBa68xsi0gWBM9XMIRfiaNc0Kcq6guk= +github.com/notnil/chess v1.2.0/go.mod h1:cRuJUIBFq9Xki05TWHJxHYkC+fFpq45IWwk94DdlCrA= +github.com/rogpeppe/misc v0.0.0-20200516092017-43a33a8f4c44 h1:osBnWuJgXxjdv/zUO9sm32B1Peew1cPGGyonHrqXd5M= +github.com/rogpeppe/misc v0.0.0-20200516092017-43a33a8f4c44/go.mod h1:hT7K6J2LNwUaaq7rtwF5VMB9kQ9yf7HgYFNVOOf41OI= +github.com/rudi9719/loggy v0.0.0-20201030144506-35d9b032ab93 h1:GjCI3CXdGt2PysVpObVzmKqMgKf3RlsKLBnaSOqPpUk= +github.com/rudi9719/loggy v0.0.0-20201030144506-35d9b032ab93/go.mod h1:s1ANCN8bF6HwwTpJLR458MFVGua9oqKKDbph/2jptL4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +samhofi.us/x/keybase v0.0.0-20200129212102-e05e93be9f3f h1:MHSEiuiRFrFi7BTw46lC22PMk3Fit8IvVRM4xANTt20= +samhofi.us/x/keybase v0.0.0-20200129212102-e05e93be9f3f/go.mod h1:fcva80IUFyWcHtV4bBSzgKg07K6Rvuvi3GtGCLNGkyE= +samhofi.us/x/keybase v0.0.0-20200409184719-3e5afcc7f988 h1:ZQDHB2AP5ouuMnANibyBJQBbxwi+tCMQHa4ttp15ac0= +samhofi.us/x/keybase/v2 v2.0.6 h1:gLluTcyjbwckQxSarF1ig2klL4Li7O/THdxsgo1dUvw= +samhofi.us/x/keybase/v2 v2.0.6/go.mod h1:lJivwhzMSV+WUg+XUbatszStjjFVcuLGl+xcQpqQ5GQ= diff --git a/main.go b/main.go new file mode 100755 index 0000000..f6a3e48 --- /dev/null +++ b/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "image" + "math/rand" + "os" + "strings" + "time" + + "github.com/llgcode/draw2d/draw2dimg" + "github.com/rogpeppe/misc/svg" + "github.com/rudi9719/loggy" + "samhofi.us/x/keybase/v2" + "samhofi.us/x/keybase/v2/types/chat1" +) + +var ( + k = keybase.NewKeybase() + + configFile = "config.json" + config Config + log = loggy.NewLogger(config.LogOpts) + r = rand.New(rand.NewSource(time.Now().UnixNano())) +) + +func printChat(m chat1.MsgSummary) { + defer log.PanicSafe() + if m.Sender.Username == k.Username { + return + } + if strings.HasPrefix(m.Content.Text.Body, "@chessbot") { + chessCommand(m) + return + } + log.LogInfo("%s: %s\n", m.Sender.Username, m.Content.Text.Body) +} +func main() { + loadConfig() + log = loggy.NewLogger(config.LogOpts) + defer log.PanicSafe() + chat := printChat + err := log.LogErrorType + handlers := keybase.Handlers{ + ChatHandler: &chat, + ErrorHandler: &err, + } + log.LogCritical("Starting Chessbot") + k.Run(handlers, &keybase.RunOptions{}) +} +func svgToPNG(path string) bool { + defer log.PanicSafe() + file, err := os.Open(fmt.Sprintf("%+v.svg", path)) + if err != nil { + return false + } + defer file.Close() + size := image.Point{512, 512} + dest, err := svg.Render(file, size) + if err != nil { + return false + } + err = draw2dimg.SaveToPngFile(fmt.Sprintf("%+v.png", path), dest) + if err != nil { + return false + } + return true + +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..56b6a46 --- /dev/null +++ b/types.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/notnil/chess" + "github.com/rudi9719/loggy" + "time" +) + +// Game is just a struct of type Game +type Game struct { + Game *chess.Game + StartTime time.Time + ConvID string + White string + Black string + Move bool +} + +// Config struct for restarting game +type Config struct { + Games map[string]Game + LogOpts loggy.LogOpts +} \ No newline at end of file