Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

elo calculation, competition mode for comparing AI #69

Merged
merged 14 commits into from
Apr 11, 2019
7 changes: 7 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"github.com/Vadman97/ChessAI3/pkg/chessai/competition"
"github.com/gorilla/mux"
"log"
"net/http"
Expand All @@ -11,6 +12,12 @@ import (
)

func main() {
if len(os.Args) > 1 && os.Args[1] == "competition" {
comp := competition.NewCompetition()
comp.RunAICompetition()
return
}

// Setup HTTP Routes
r := mux.NewRouter()
r.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/")))
Expand Down
5 changes: 4 additions & 1 deletion conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"LogPerformance": true,
"PerformanceLogFileName": "performance.log",
"LogPerformanceToExcel": true,
"ExcelPerformanceFileName": "./performance.xlsx"
"ExcelPerformanceFileName": "./performance.xlsx",
"PrintPlayerInfo": true,
"NumberOfCompetitionGames": 100,
"StartingElo": 1200
}
35 changes: 34 additions & 1 deletion pkg/chessai/board/board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"log"
"math/rand"
"path"
"reflect"
"testing"
"time"
Expand Down Expand Up @@ -151,7 +152,39 @@ func TestBoardColorFromChar(t *testing.T) {
assert.Equal(t, byte(0xFF), ColorFromChar('a'))
}

// TODO(Alex) Stalemate, Checkmate Tests
const boardsDirectory = "board_test"

func TestBoard_IsInCheckmateBlack(t *testing.T) {
b := Board{}
lines, _ := util.LoadBoardFile(path.Join(boardsDirectory, "black_is_in_checkmate.txt"))
b.LoadBoardFromText(lines)
assert.False(t, b.IsInCheckmate(color.White, nil))
assert.True(t, b.IsInCheckmate(color.Black, nil))
}

func TestBoard_IsInCheckmateWhite(t *testing.T) {
b := Board{}
lines, _ := util.LoadBoardFile(path.Join(boardsDirectory, "white_is_in_checkmate.txt"))
b.LoadBoardFromText(lines)
assert.True(t, b.IsInCheckmate(color.White, nil))
assert.False(t, b.IsInCheckmate(color.Black, nil))
}

func TestBoard_IsStalemateBlack(t *testing.T) {
b := Board{}
lines, _ := util.LoadBoardFile(path.Join(boardsDirectory, "black_is_stalemate.txt"))
b.LoadBoardFromText(lines)
assert.False(t, b.IsStalemate(color.White, nil))
assert.True(t, b.IsStalemate(color.Black, nil))
}

func TestBoard_IsStalemateWhite(t *testing.T) {
b := Board{}
lines, _ := util.LoadBoardFile(path.Join(boardsDirectory, "white_is_stalemate.txt"))
b.LoadBoardFromText(lines)
assert.True(t, b.IsStalemate(color.White, nil))
assert.False(t, b.IsStalemate(color.Black, nil))
}

func simulateGameMove(move *location.Move, b *Board) {
lastMove := MakeMove(move, b)
Expand Down
8 changes: 8 additions & 0 deletions pkg/chessai/board/board_test/black_is_in_checkmate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
| |B_K|W_Q|W_N| | |
B_P|B_P| | |W_B| | |B_R
| | | | | | |
| | | |W_N| | |B_P
| |B_P| | | | |
W_P| |W_N|W_P| | | |
|W_P|W_P| | |W_P|W_P|W_P
| |W_K|W_R| | | |W_R
8 changes: 8 additions & 0 deletions pkg/chessai/board/board_test/black_is_stalemate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
| | | |B_K| | |
| | | | | | |
| | | | |W_R| |
B_P| |B_P| |W_B|W_N|W_P|
W_P| |W_P| | | | |
| | | | |W_P| |W_P
| | |W_R| | | |
| | | | | |W_K|
8 changes: 8 additions & 0 deletions pkg/chessai/board/board_test/white_is_in_checkmate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
B_R|B_N|B_B| |B_K| | |B_R
B_P|B_P|B_P|B_P| | |B_P|B_P
| | | | |B_N| |
| | | | |B_P| |
W_P|B_B|W_P| | |B_P| |
W_R|W_K| |W_P| |W_P| |
| | | |W_P| | |
|B_Q| | | | | |
8 changes: 8 additions & 0 deletions pkg/chessai/board/board_test/white_is_stalemate.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
| | | | |B_R| |
| | |B_N|B_N| |B_K|
|B_P|B_P|B_P|B_B|B_P| |B_P
| | | | | | |B_P
|B_P| | | | | |W_P
| | |B_Q| | | |
B_R| | | | | | |
| |W_K| | | | |
70 changes: 59 additions & 11 deletions pkg/chessai/board/game_board.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/Vadman97/ChessAI3/pkg/chessai/util"
"log"
"math/rand"
"strings"
"time"
)

Expand Down Expand Up @@ -161,6 +162,10 @@ func (b *Board) ResetDefault() {
b.MovesSinceNoDraw = 0
b.CacheGetAllMoves = config.Get().CacheGetAllMoves
b.CacheGetAllAttackableMoves = config.Get().CacheGetAllAttackableMoves
b.KingLocations = [color.NumColors]location.Location{
location.NewLocation(7, 4),
location.NewLocation(0, 4),
}
}

func (b *Board) ResetDefaultSlow() {
Expand Down Expand Up @@ -279,11 +284,22 @@ func (b *Board) RandomizeIllegal() {
b.flags = byte(b.TestRandGen.Uint32())
}

/**
* Check if color has at least one legal move, optimized
*/
func (b *Board) HasLegalMove(color byte, previousMove *LastMove) bool {
return len(*b.getAllMovesCached(color, previousMove, true)) > 0
}

func (b *Board) GetAllMoves(color byte, previousMove *LastMove) *[]location.Move {
return b.getAllMovesCached(color, previousMove, false)
}

/*
* Only this is cached and not GetAllAttackableMoves for now because this calls GetAllAttackableMoves
* May need to cache that one too when we use it for CheckMate / Tie evaluation
*/
func (b *Board) GetAllMoves(color byte, previousMove *LastMove) *[]location.Move {
func (b *Board) getAllMovesCached(color byte, previousMove *LastMove, onlyFirstMove bool) *[]location.Move {
entry := &MoveCacheEntry{
moves: make(map[byte]interface{}),
}
Expand All @@ -293,15 +309,21 @@ func (b *Board) GetAllMoves(color byte, previousMove *LastMove) *[]location.Move
entry = cacheEntry.(*MoveCacheEntry)
// we've gotten the other color but not the one we want
if entry.moves[color] == nil {
entry.moves[color] = b.getAllMoves(color)
b.MoveCache.Store(&h, entry)
entry.moves[color] = b.getAllMoves(color, onlyFirstMove)
// store only if we grabbing all moves
if !onlyFirstMove {
b.MoveCache.Store(&h, entry)
}
}
} else {
entry.moves[color] = b.getAllMoves(color)
b.MoveCache.Store(&h, entry)
entry.moves[color] = b.getAllMoves(color, onlyFirstMove)
// store only if we grabbing all moves
if !onlyFirstMove {
b.MoveCache.Store(&h, entry)
}
}
} else {
entry.moves[color] = b.getAllMoves(color)
entry.moves[color] = b.getAllMoves(color, onlyFirstMove)
}
if previousMove != nil {
enPassantMoves := b.GetEnPassantMoves(color, previousMove)
Expand All @@ -311,7 +333,11 @@ func (b *Board) GetAllMoves(color byte, previousMove *LastMove) *[]location.Move
return entry.moves[color].(*[]location.Move)
}

func (b *Board) getAllMoves(c byte) *[]location.Move {
/**
* Get moves for all pieces of color c.
* If onlyFirstMove is set, will only return first move
*/
func (b *Board) getAllMoves(c byte, onlyFirstMove bool) *[]location.Move {
dadhia marked this conversation as resolved.
Show resolved Hide resolved
var moves []location.Move
for row := 0; row < Height; row++ {
// this is just a speedup - if the whole row is empty don't look at pieces
Expand Down Expand Up @@ -453,16 +479,14 @@ func (b *Board) willMoveLeaveKingInCheck(c byte, m location.Move) bool {
* Checks if the king of color c is in checkmate.
*/
func (b *Board) IsInCheckmate(c byte, previousMove *LastMove) bool {
moves := b.GetAllMoves(c, previousMove)
return (len(*moves) == 0) && b.IsKingInCheck(c)
return !b.HasLegalMove(c, previousMove) && b.IsKingInCheck(c)
}

/**
* Checks if the board is in a stalemate based on color c not having any moves and its king is also not in check.
*/
func (b *Board) IsStalemate(c byte, previousMove *LastMove) bool {
moves := b.GetAllMoves(c, previousMove)
return (len(*moves) == 0) && !b.IsKingInCheck(c)
return !b.HasLegalMove(c, previousMove) && !b.IsKingInCheck(c)
}

/**
Expand All @@ -477,6 +501,30 @@ func (b *Board) UpdateDrawCounter(previousMove *LastMove) {
}
}

/**
* Load board from text for tests
*/
func (b *Board) LoadBoardFromText(boardRows []string) {
for r := location.CoordinateType(0); r < Height; r++ {
pieces := strings.Split(boardRows[r], "|")
for c, pStr := range pieces {
l := location.NewLocation(r, location.CoordinateType(c))
var p Piece
if pStr != " " && len(pStr) == 3 {
d := strings.Split(pStr, "_")
cChar, pChar := rune(d[0][0]), rune(d[1][0])
p = PieceFromType(piece.NameToType[pChar])
if p.GetPieceType() == piece.KingType {
b.KingLocations[ColorFromChar(cChar)] = l
}
p.SetColor(ColorFromChar(cChar))
p.SetPosition(l)
}
b.SetPiece(l, p)
}
}
}

func (b *Board) move(m *location.Move) {
// more efficient function than using SetPiece(end, GetPiece(start)) - tested with benchmark

Expand Down
113 changes: 113 additions & 0 deletions pkg/chessai/competition/competition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package competition

import (
"fmt"
"github.com/Vadman97/ChessAI3/pkg/chessai/color"
"github.com/Vadman97/ChessAI3/pkg/chessai/config"
"github.com/Vadman97/ChessAI3/pkg/chessai/game"
"github.com/Vadman97/ChessAI3/pkg/chessai/player/ai"
"math/rand"
"time"
)

type Competition struct {
NumberOfGames int
wins [color.NumColors]int
ties int
players [color.NumColors]*ai.Player
elos [color.NumColors]Elo
gameNumber int
whiteIndex, blackIndex int
competitionRand *rand.Rand
}

func NewCompetition() (c *Competition) {
var StartingElo = Elo(config.Get().StartingElo)
c = &Competition{
NumberOfGames: config.Get().NumberOfCompetitionGames,
players: [2]*ai.Player{
ai.NewAIPlayer(color.White, nil),
ai.NewAIPlayer(color.Black, nil),
},
elos: [2]Elo{StartingElo, StartingElo},
}
return
}

// TODO(Vadim) add reading AI / outputting results to file

func (c *Competition) RunCompetition() {
c.competitionRand = rand.New(rand.NewSource(time.Now().UnixNano()))
for c.gameNumber = 1; c.gameNumber <= c.NumberOfGames; c.gameNumber++ {
fmt.Printf(c.Print())
// randomize color of players each game
c.randomizePlayers()
g := game.NewGame(c.players[c.whiteIndex], c.players[c.blackIndex])
c.disablePrinting(g)
active := true
for active {
active = g.PlayTurn()
}
fmt.Println(g.Print())
g.ClearCaches()
outcome := c.derandomizeGameOutcome(g.GetGameOutcome())
c.elos = CalculateRatings(c.elos, outcome)
c.RecordOutcome(outcome)
}
}

func (c *Competition) Print() (result string) {
result += fmt.Sprintf("\n=== Game %d ===\n", c.gameNumber)
result += fmt.Sprintf("\tWhite Elo: %d\n", c.elos[color.White])
result += fmt.Sprintf("\tBlack Elo: %d\n", c.elos[color.Black])
result += fmt.Sprintf("\tWW:%d,BW:%d,T:%d\n", c.wins[color.White], c.wins[color.Black], c.ties)
return result
}

func (c *Competition) RecordOutcome(outcome game.Outcome) {
if outcome.Win[color.White] {
c.wins[color.White]++
} else if outcome.Win[color.Black] {
c.wins[color.Black]++
} else if outcome.Tie {
c.ties++
}
}

/**
* swap players randomly so the competition white players is swapped
* competition maintains constant perspective of the two players
*/
func (c *Competition) randomizePlayers() {
c.whiteIndex = c.competitionRand.Int() % color.NumColors
c.blackIndex = c.whiteIndex ^ 1
c.players[c.whiteIndex].PlayerColor = color.White
c.players[c.blackIndex].PlayerColor = color.Black
}

/**
* randomizePlayers will swap players in game, which affects outcome.
* swap again to make outcome match our perspective on white/black
*/
func (c *Competition) derandomizeGameOutcome(out game.Outcome) game.Outcome {
out.Win[color.White], out.Win[color.Black] = out.Win[c.whiteIndex], out.Win[c.blackIndex]
return out
}

func (c *Competition) disablePrinting(g *game.Game) {
g.PrintInfo = false
c.players[color.White].PrintInfo = false
c.players[color.Black].PrintInfo = false
}

func (c *Competition) RunAICompetition() {
// TODO(Vadim) output this to file and keep history of AI performance
// TODO(Vadim) load ai from file
rand.Seed(config.Get().TestRandSeed)
c.players[color.White].Algorithm = &ai.MTDf{}
c.players[color.White].MaxSearchDepth = 512
c.players[color.White].MaxThinkTime = 100 * time.Millisecond
c.players[color.Black].Algorithm = &ai.MiniMax{}
c.players[color.Black].MaxSearchDepth = 1
c.RunCompetition()
}
Loading