├── .gitignore ├── Makefile ├── cmd └── connect-four │ └── main.go └── connectfour.go /.gitignore: -------------------------------------------------------------------------------- 1 | /connect-four -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES := $(shell find . -name "*.go") 2 | 3 | 4 | connect-four: $(SOURCES) 5 | go build -o connect-four ./cmd/connect-four/main.go 6 | -------------------------------------------------------------------------------- /cmd/connect-four/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "time" 9 | 10 | "github.com/Sirupsen/logrus" 11 | "github.com/gin-gonic/gin" 12 | "github.com/moul/bolosseum/bots" 13 | "github.com/moul/connect-four" 14 | ) 15 | 16 | func init() { 17 | rand.Seed(time.Now().UTC().UnixNano()) 18 | gin.DisableBindValidation() 19 | } 20 | 21 | func main() { 22 | if len(os.Args) == 1 { 23 | // web mode 24 | logrus.Warnf("You ran this program without argument, it will then start a web server") 25 | logrus.Warnf("usage: ") 26 | logrus.Warnf("- %s # web mode", os.Args[0]) 27 | logrus.Warnf("- %s some-json # cli mode", os.Args[0]) 28 | 29 | r := gin.Default() 30 | 31 | r.POST("/", func(c *gin.Context) { 32 | var question bots.QuestionMessage 33 | if err := c.BindJSON(&question); err != nil { 34 | fmt.Println(err) 35 | c.JSON(404, fmt.Errorf("Invalid POST data: %v", err)) 36 | return 37 | } 38 | 39 | bot := connectfour.NewConnectfourBot() 40 | reply := &bots.ReplyMessage{} 41 | switch question.Action { 42 | case "init": 43 | reply = bot.Init(question) 44 | case "play-turn": 45 | reply = bot.PlayTurn(question) 46 | default: 47 | // FIXME: reply message error 48 | c.JSON(500, gin.H{"Error": fmt.Errorf("Unknown action: %q", question.Action)}) 49 | return 50 | } 51 | 52 | c.JSON(200, reply) 53 | }) 54 | 55 | r.GET("/", func(c *gin.Context) { 56 | if message := c.Query("message"); message != "" { 57 | bot := connectfour.NewConnectfourBot() 58 | 59 | logrus.Warnf("<< %s", message) 60 | var question bots.QuestionMessage 61 | if err := json.Unmarshal([]byte(message), &question); err != nil { 62 | c.JSON(500, gin.H{"Error": err}) 63 | return 64 | } 65 | 66 | reply := &bots.ReplyMessage{} 67 | switch question.Action { 68 | case "init": 69 | reply = bot.Init(question) 70 | case "play-turn": 71 | reply = bot.PlayTurn(question) 72 | default: 73 | // FIXME: reply message error 74 | c.JSON(500, gin.H{"Error": fmt.Errorf("Unknown action: %q", question.Action)}) 75 | return 76 | } 77 | 78 | c.JSON(200, reply) 79 | } else { 80 | c.String(404, "This server is a bot for bolosseum.") 81 | } 82 | }) 83 | r.Run(":8080") 84 | } else { 85 | // cli mode 86 | logrus.Warnf("%s << %v", os.Args[0], os.Args[1]) 87 | 88 | var question bots.QuestionMessage 89 | if err := json.Unmarshal([]byte(os.Args[1]), &question); err != nil { 90 | logrus.Fatalf("%s XX err: %v", err) 91 | } 92 | 93 | bot := connectfour.NewConnectfourBot() 94 | 95 | reply := &bots.ReplyMessage{} 96 | switch question.Action { 97 | case "init": 98 | reply = bot.Init(question) 99 | case "play-turn": 100 | reply = bot.PlayTurn(question) 101 | default: 102 | // FIXME: reply message error 103 | logrus.Fatalf("Unknown action: %q", question.Action) 104 | } 105 | 106 | jsonString, err := json.Marshal(reply) 107 | if err != nil { 108 | logrus.Fatalf("Failed to marshal json: %v", err) 109 | } 110 | 111 | fmt.Println(string(jsonString)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /connectfour.go: -------------------------------------------------------------------------------- 1 | package connectfour 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "gopkg.in/redis.v3" 13 | 14 | "github.com/Sirupsen/logrus" 15 | "github.com/moul/bolosseum/bots" 16 | "github.com/robfig/go-cache" 17 | ) 18 | 19 | var Rows = 6 20 | var Cols = 7 21 | var MaxDeepness = 6 22 | 23 | var rc *redis.Client 24 | var c *cache.Cache 25 | 26 | func init() { 27 | // initialize cache 28 | c = cache.New(5*time.Minute, 30*time.Second) 29 | 30 | // initialize redis 31 | if os.Getenv("REDIS_HOSTNAME") != "" { 32 | rc = redis.NewClient(&redis.Options{ 33 | Addr: os.Getenv("REDIS_HOSTNAME"), 34 | Password: os.Getenv("REDIS_PASSWORD"), 35 | DB: 0, 36 | }) 37 | pong, err := rc.Ping().Result() 38 | logrus.Warnf("Redis ping: %v, %v", pong, err) 39 | } 40 | } 41 | 42 | func NewConnectfourBot() *ConnectfourBot { 43 | return &ConnectfourBot{} 44 | } 45 | 46 | type ConnectfourBot struct{} 47 | 48 | func (b *ConnectfourBot) Init(message bots.QuestionMessage) *bots.ReplyMessage { 49 | // FIXME: init ttt here 50 | return &bots.ReplyMessage{ 51 | Name: "moul-connectfour", 52 | } 53 | } 54 | 55 | func (b *ConnectfourBot) PlayTurn(question bots.QuestionMessage) *bots.ReplyMessage { 56 | bot := NewConnectFour(question.You.(string)) 57 | 58 | doneMoves := 0 59 | board := question.Board 60 | for y := 0; y < Rows; y++ { 61 | row := board.([]interface{})[y] 62 | for x := 0; x < Cols; x++ { 63 | val := row.([]interface{})[x] 64 | if val.(string) != "" { 65 | bot.Board[y][x] = val.(string) 66 | doneMoves++ 67 | } 68 | } 69 | } 70 | 71 | // first move is random 72 | if doneMoves == 0 { 73 | play := rand.Intn(Cols) 74 | logrus.Warnf("the first move is always random, playing %d", play) 75 | return &bots.ReplyMessage{ 76 | Play: play, 77 | } 78 | } 79 | 80 | // get movements 81 | moves := bot.BestMovements() 82 | if len(moves) == 0 { 83 | return &bots.ReplyMessage{ 84 | Error: "no available movement", 85 | } 86 | } 87 | 88 | // pick one 89 | picked := moves[rand.Intn(len(moves))] 90 | 91 | logrus.Warnf("Playing %d with score %f, %d best moves", picked.Play, picked.Score, len(moves)) 92 | return &bots.ReplyMessage{ 93 | Play: picked.Play, 94 | } 95 | } 96 | 97 | func (b *ConnectFour) Hash(currentPlayer string) string { 98 | hash := "" 99 | hash += fmt.Sprintf("%d", MaxDeepness) 100 | for y := 0; y < Rows; y++ { 101 | for x := 0; x < Cols; x++ { 102 | if b.Board[y][x] != "" { 103 | hash += b.Board[y][x] 104 | } else { 105 | hash += "." 106 | } 107 | } 108 | } 109 | 110 | hash += currentPlayer 111 | return hash 112 | } 113 | 114 | type ConnectFour struct { 115 | Board [][]string 116 | Player string 117 | } 118 | 119 | type Movement struct { 120 | Play int 121 | Score float64 122 | } 123 | 124 | func (b *ConnectFour) PrintMap() { 125 | for y := 0; y < Rows; y++ { 126 | line := "|" 127 | for x := 0; x < Cols; x++ { 128 | if b.Board[y][x] != "" { 129 | line += b.Board[y][x] + "|" 130 | } else { 131 | line += " |" 132 | } 133 | } 134 | logrus.Warnf(line) 135 | } 136 | } 137 | 138 | func (b *ConnectFour) Winner() string { 139 | pieces := []string{"X", "O"} 140 | 141 | // horizontal 142 | for _, piece := range pieces { 143 | for y := 0; y < Rows; y++ { 144 | continuous := 0 145 | for x := 0; x < Cols; x++ { 146 | if b.Board[y][x] == piece { 147 | continuous++ 148 | if continuous == 4 { 149 | return piece 150 | } 151 | } else { 152 | continuous = 0 153 | } 154 | } 155 | } 156 | } 157 | 158 | //vertical 159 | for _, piece := range pieces { 160 | for x := 0; x < Cols; x++ { 161 | continuous := 0 162 | for y := 0; y < Rows; y++ { 163 | if b.Board[y][x] == piece { 164 | continuous++ 165 | if continuous == 4 { 166 | return piece 167 | } 168 | } else { 169 | continuous = 0 170 | } 171 | } 172 | } 173 | } 174 | 175 | // diagnoals 176 | for _, piece := range pieces { 177 | for x := 0; x < Cols-4; x++ { 178 | for y := 0; y < Rows-4; y++ { 179 | continuous := 0 180 | for i := 0; i < 4; i++ { 181 | if b.Board[y+i][x+i] == piece { 182 | continuous++ 183 | if continuous == 4 { 184 | return piece 185 | } 186 | } else { 187 | continuous = 0 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | return "" 195 | } 196 | 197 | func (b *ConnectFour) BestMovements() []Movement { 198 | hash := b.Hash(b.Player) 199 | if cachedMoves, found := c.Get(hash); found { 200 | return cachedMoves.([]Movement) 201 | } 202 | 203 | if rc != nil { 204 | cachedMoves, err := rc.Get(hash).Result() 205 | if err == nil { 206 | moves := []Movement{} 207 | for _, playStr := range strings.Split(cachedMoves, ",") { 208 | play, _ := strconv.Atoi(playStr) 209 | moves = append(moves, Movement{ 210 | Play: play, 211 | }) 212 | } 213 | c.Set(hash, moves, -1) 214 | return moves 215 | } 216 | if err != redis.Nil { 217 | logrus.Errorf("Redis: failed to get value for hash=%q: %v", hash, err) 218 | } 219 | } 220 | 221 | logrus.Warnf("bot: %v", b) 222 | moves := b.ScoreMovements(b.Player, 1) 223 | logrus.Warnf("score-moves: %v", moves) 224 | b.PrintMap() 225 | 226 | if len(moves) == 0 { 227 | return moves 228 | } 229 | 230 | // take the best score 231 | maxScore := moves[0].Score 232 | for _, move := range moves { 233 | if move.Score > maxScore { 234 | maxScore = move.Score 235 | } 236 | } 237 | bestMoves := []Movement{} 238 | for _, move := range moves { 239 | if move.Score == maxScore { 240 | bestMoves = append(bestMoves, move) 241 | } 242 | } 243 | 244 | c.Set(hash, bestMoves, -1) 245 | if rc != nil { 246 | bestMovesStr := "" 247 | if len(bestMoves) > 0 { 248 | bestMovesStr = fmt.Sprintf("%d", bestMoves[0].Play) 249 | for _, move := range bestMoves[1:] { 250 | bestMovesStr += fmt.Sprintf(",%d", move.Play) 251 | } 252 | } 253 | if err := rc.Set(hash, bestMovesStr, 0).Err(); err != nil { 254 | logrus.Errorf("Redis: failed to write value for hash=%q", hash) 255 | } 256 | } 257 | return bestMoves 258 | } 259 | 260 | func (b *ConnectFour) ScoreMovements(currentPlayer string, deepness int) []Movement { 261 | // check if board is already finished 262 | if b.Winner() != "" { 263 | return []Movement{} 264 | } 265 | 266 | // get available moves 267 | moves := b.AvailableMovements() 268 | 269 | // useless to go too deep 270 | if deepness > MaxDeepness { 271 | return moves 272 | } 273 | 274 | //size := Cols * Rows 275 | value := math.Pow(float64(MaxDeepness+1), float64(MaxDeepness-deepness)) 276 | if deepness == 1 { 277 | logrus.Warnf("score=%q deepness=%d moves=%v winner=%q value=%f", currentPlayer, deepness, moves, b.Winner(), value) 278 | } 279 | 280 | for idx, move := range moves { 281 | b.Play(move.Play, currentPlayer) 282 | switch b.Winner() { 283 | case b.Player: 284 | moves[idx].Score = value 285 | case b.Opponent(): 286 | moves[idx].Score = -value 287 | default: 288 | for _, subMove := range b.ScoreMovements(b.NextPlayer(currentPlayer), deepness+1) { 289 | moves[idx].Score += subMove.Score 290 | } 291 | } 292 | b.Play(move.Play, "") 293 | } 294 | 295 | return moves 296 | } 297 | 298 | func (b *ConnectFour) Opponent() string { 299 | return b.NextPlayer(b.Player) 300 | } 301 | 302 | func (b *ConnectFour) NextPlayer(currentPlayer string) string { 303 | switch currentPlayer { 304 | case "X": 305 | return "O" 306 | case "O": 307 | return "X" 308 | } 309 | return "" 310 | } 311 | 312 | func (b *ConnectFour) AvailableMovements() []Movement { 313 | movements := []Movement{} 314 | for x := 0; x < Cols; x++ { 315 | for y := 0; y < Rows; y++ { 316 | if b.Board[y][x] == "" { 317 | movement := Movement{ 318 | Play: x, 319 | Score: 0, 320 | } 321 | movements = append(movements, movement) 322 | break 323 | } 324 | } 325 | } 326 | return movements 327 | } 328 | 329 | func (b *ConnectFour) Play(col int, piece string) { 330 | if piece != "" { // place a piece 331 | for y := 0; y < Rows; y++ { 332 | if b.Board[y][col] == "" { 333 | b.Board[y][col] = piece 334 | return 335 | } 336 | } 337 | } else { // remove a piece 338 | for y := Rows - 1; y >= 0; y-- { 339 | if b.Board[y][col] != "" { 340 | b.Board[y][col] = "" 341 | return 342 | } 343 | } 344 | } 345 | } 346 | 347 | func NewConnectFour(player string) ConnectFour { 348 | cf := ConnectFour{ 349 | Board: make([][]string, Rows), 350 | Player: player, 351 | } 352 | for i := 0; i < Rows; i++ { 353 | cf.Board[i] = make([]string, Cols) 354 | } 355 | return cf 356 | } 357 | --------------------------------------------------------------------------------