Skip to content

Commit

Permalink
Merge pull request #54 from Vadman97/vadim-ai-refactor-time-search
Browse files Browse the repository at this point in the history
Refactor player and implement time-limited search
  • Loading branch information
Devan Adhia authored Apr 10, 2019
2 parents b69e525 + e0532f4 commit cb57cab
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 126 deletions.
36 changes: 36 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
codecov:
notify:
require_ci_to_pass: yes
strict_yaml_branch: master

coverage:
precision: 2
round: down
range: "60...100"

status:
project:
board:
target: 90%
threshold: 0%
paths: "pkg/chessai/board"
default: true
patch:
board:
target: 100%
paths: "pkg/chessai/board"
default: false # TODO(Vadim) add this when we have more tests before going public
changes: yes

parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no

comment:
layout: "header, diff"
behavior: default
require_changes: no
1 change: 0 additions & 1 deletion pkg/chessai/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ var cfg *Configuration
func Get() *Configuration {
if cfg == nil {
dir := path.Join(os.Getenv("GOPATH"), "src", "github.com", "Vadman97", "ChessAI3", FilePath)
print(dir)
file, _ := os.Open(dir)
defer func() { _ = file.Close() }()
decoder := json.NewDecoder(file)
Expand Down
22 changes: 6 additions & 16 deletions pkg/chessai/game/ai_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,28 @@ func TestBoardAI(t *testing.T) {
const TimeToPlay = 2 * time.Minute

rand.Seed(config.Get().TestRandSeed)
aiPlayerSmart := ai.NewAIPlayer(color.Black)
aiPlayerSmart.Algorithm = ai.AlgorithmMTDF
aiPlayerSmart.MaxSearchDepth = 4
aiPlayerDumb := ai.NewAIPlayer(color.White)
aiPlayerDumb.Algorithm = ai.AlgorithmRandom
aiPlayerSmart := ai.NewAIPlayer(color.Black, &ai.MTDf{})
aiPlayerSmart.MaxSearchDepth = 100
aiPlayerSmart.MaxThinkTime = 1 * time.Second
aiPlayerDumb := ai.NewAIPlayer(color.White, &ai.Random{})
aiPlayerDumb.MaxSearchDepth = 2
g := NewGame(aiPlayerDumb, aiPlayerSmart)
g.MoveLimit = MovesToPlay
g.TimeLimit = TimeToPlay

fmt.Println("Before moves:")
fmt.Println(g.CurrentBoard.Print())
start := time.Now()
for i := 0; i < MovesToPlay; i++ {
if i%2 == 0 && time.Now().Sub(start) > TimeToPlay {
fmt.Printf("Aborting - out of time\n")
break
}
fmt.Printf("\nPlayer %s thinking...\n", g.Players[g.CurrentTurnColor].Repr())
active := g.PlayTurn()
fmt.Printf("Move %d by %s\n", g.MovesPlayed, color.Names[g.CurrentTurnColor^1])
fmt.Println(g.CurrentBoard.Print())
fmt.Println(g.Print())
util.PrintMemStats()
if !active {
fmt.Printf("Game Over! Result is: %s\n", StatusStrings[g.GameStatus])
break
}
}

fmt.Println("After moves:")
fmt.Println(g.CurrentBoard.Print())
fmt.Println(g.Print())
// comment out printing inside loop for accurate timing
fmt.Printf("Played %d moves in %d ms.\n", g.MovesPlayed, time.Now().Sub(start)/time.Millisecond)

smartScore := aiPlayerSmart.EvaluateBoard(g.CurrentBoard).TotalScore
Expand Down
69 changes: 45 additions & 24 deletions pkg/chessai/game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/Vadman97/ChessAI3/pkg/chessai/location"
"github.com/Vadman97/ChessAI3/pkg/chessai/player/ai"
"github.com/Vadman97/ChessAI3/pkg/chessai/util"
"math"
"runtime"
"time"
)
Expand All @@ -23,6 +24,8 @@ type Game struct {
PreviousMove *board.LastMove
GameStatus byte
CacheMemoryLimit uint64
MoveLimit int32
TimeLimit time.Duration
PerformanceLogger *ai.PerformanceLogger
}

Expand All @@ -34,32 +37,43 @@ func (g *Game) PlayTurn() bool {
panic("Game is not active!")
}

start := time.Now()
quitTimeUpdates := make(chan bool)
// print think time for slow players, regardless of what's going on
go g.periodicUpdates(quitTimeUpdates, start)
g.PreviousMove = g.Players[g.CurrentTurnColor].MakeMove(g.CurrentBoard, g.PreviousMove, g.PerformanceLogger)
// quit time updates (never prints if quick player)
close(quitTimeUpdates)
g.UpdateTime(start)
g.CurrentTurnColor ^= 1
g.MovesPlayed++
if g.MovesPlayed%2 == 0 && g.GetTotalPlayTime() > g.TimeLimit {
fmt.Printf("Aborting - out of time\n")
g.GameStatus = Aborted
} else {
fmt.Printf("\nPlayer %s thinking...\n", g.Players[g.CurrentTurnColor].Repr())
start := time.Now()
quitTimeUpdates := make(chan bool)
// print think time for slow players, regardless of what's going on
go g.periodicUpdates(quitTimeUpdates, start)
g.PreviousMove = g.Players[g.CurrentTurnColor].MakeMove(g.CurrentBoard, g.PreviousMove, g.PerformanceLogger)
// quit time updates (never prints if quick player)
close(quitTimeUpdates)
g.UpdateTime(start)
g.CurrentTurnColor ^= 1
g.MovesPlayed++

g.CurrentBoard.UpdateDrawCounter(g.PreviousMove)
g.CurrentBoard.UpdateDrawCounter(g.PreviousMove)

if g.CurrentBoard.IsInCheckmate(g.CurrentTurnColor, g.PreviousMove) {
if g.CurrentTurnColor == color.White {
g.GameStatus = BlackWin
if g.CurrentBoard.IsInCheckmate(g.CurrentTurnColor, g.PreviousMove) {
if g.CurrentTurnColor == color.White {
g.GameStatus = BlackWin
} else {
g.GameStatus = WhiteWin
}
} else if g.CurrentBoard.IsStalemate(g.CurrentTurnColor, g.PreviousMove) {
g.GameStatus = Stalemate
} else if g.CurrentBoard.IsStalemate(g.CurrentTurnColor^1, g.PreviousMove) {
g.GameStatus = Stalemate
} else if g.CurrentBoard.MovesSinceNoDraw >= 100 {
// 50 Move Rule (50 moves per color)
g.GameStatus = Stalemate
}
if g.GameStatus == Active {
fmt.Printf("Move #%d by %s\n", g.MovesPlayed, color.Names[g.CurrentTurnColor^1])
} else {
g.GameStatus = WhiteWin
fmt.Printf("Game Over! Result is: %s\n", StatusStrings[g.GameStatus])
}
} else if g.CurrentBoard.IsStalemate(g.CurrentTurnColor, g.PreviousMove) {
g.GameStatus = Stalemate
} else if g.CurrentBoard.IsStalemate(g.CurrentTurnColor^1, g.PreviousMove) {
g.GameStatus = Stalemate
} else if g.CurrentBoard.MovesSinceNoDraw >= 100 {
// 50 Move Rule (50 moves per color)
g.GameStatus = Stalemate
}
if g.GameStatus != Active {
g.PerformanceLogger.CompletePerformanceLog(g.Players[color.White], g.Players[color.Black])
Expand All @@ -69,10 +83,11 @@ func (g *Game) PlayTurn() bool {

func (g *Game) Print() (result string) {
// we just played white if we are now on black, show info for white
result += fmt.Sprintln(g.CurrentBoard.Print())
result += g.PrintThinkTime(g.CurrentTurnColor ^ 1)
if g.MovesPlayed%2 == 0 {
whiteAvg := g.TotalMoveTime[color.White].Seconds() / float64(g.MovesPlayed)
blackAvg := g.TotalMoveTime[color.Black].Seconds() / float64(g.MovesPlayed)
whiteAvg := g.TotalMoveTime[color.White].Seconds() / float64(g.MovesPlayed/2)
blackAvg := g.TotalMoveTime[color.Black].Seconds() / float64(g.MovesPlayed/2)
result += fmt.Sprintf("Average move time:\n")
result += fmt.Sprintf("\t White: %fs\n", whiteAvg)
result += fmt.Sprintf("\t Black: %fs\n", blackAvg)
Expand Down Expand Up @@ -122,6 +137,10 @@ func (g *Game) ClearCaches() {
}
}

func (g *Game) GetTotalPlayTime() time.Duration {
return g.TotalMoveTime[color.White] + g.TotalMoveTime[color.Black]
}

func NewGame(whitePlayer, blackPlayer *ai.Player) *Game {
performanceLogger := ai.CreatePerformanceLogger(config.Get().LogPerformanceToExcel,
config.Get().LogPerformance,
Expand Down Expand Up @@ -151,6 +170,8 @@ func NewGame(whitePlayer, blackPlayer *ai.Player) *Game {
PreviousMove: nil,
GameStatus: Active,
CacheMemoryLimit: config.Get().MemoryLimit,
MoveLimit: math.MaxInt32,
TimeLimit: math.MaxInt64,
PerformanceLogger: performanceLogger,
}
g.CurrentBoard.ResetDefault()
Expand Down
2 changes: 2 additions & 0 deletions pkg/chessai/game/game_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ const (
WhiteWin = byte(iota)
BlackWin = byte(iota)
Stalemate = byte(iota)
Aborted = byte(iota)
)

var StatusStrings = [...]string{
"Active",
"White Win",
"Black Win",
"Stalemate",
"Aborted",
}
55 changes: 24 additions & 31 deletions pkg/chessai/player/ai/ai_player.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"math"
"math/rand"
"os"
"time"
)

const (
Expand All @@ -21,7 +22,7 @@ const (
const (
AlgorithmMiniMax = "MiniMax"
AlgorithmAlphaBetaWithMemory = "AlphaBetaMemory"
AlgorithmMTDF = "MTDF"
AlgorithmMTDf = "MTDf"
AlgorithmRandom = "Random"
)

Expand Down Expand Up @@ -67,12 +68,17 @@ type ScoredMove struct {
Score int
}

type Algorithm interface {
GetName() string
GetBestMove(*Player, *board.Board, *board.LastMove) *ScoredMove
}

type Player struct {
Algorithm string
Algorithm Algorithm
TranspositionTableEnabled bool
PlayerColor byte
MaxSearchDepth int
CurrentSearchDepth int
MaxThinkTime time.Duration
TurnCount int
Opening int
Metrics *Metrics
Expand All @@ -82,9 +88,9 @@ type Player struct {
Debug bool
}

func NewAIPlayer(c byte) *Player {
func NewAIPlayer(c byte, algorithm Algorithm) *Player {
p := &Player{
Algorithm: AlgorithmAlphaBetaWithMemory,
Algorithm: algorithm,
TranspositionTableEnabled: config.Get().TranspositionTableEnabled,
PlayerColor: c,
TurnCount: 0,
Expand Down Expand Up @@ -123,29 +129,20 @@ func (p *Player) GetBestMove(b *board.Board, previousMove *board.LastMove, logge
// reset metrics for each move
p.Metrics = &Metrics{}

var m = &ScoredMove{
Score: 0,
}
if p.Algorithm == AlgorithmMiniMax {
m = p.MiniMax(b, p.MaxSearchDepth, p.PlayerColor, previousMove)
} else if p.Algorithm == AlgorithmAlphaBetaWithMemory {
m = p.AlphaBetaWithMemory(b, p.MaxSearchDepth, NegInf, PosInf, p.PlayerColor, previousMove)
} else if p.Algorithm == AlgorithmMTDF {
m = p.IterativeMTDF(b, m, previousMove)
} else if p.Algorithm == AlgorithmRandom {
m = p.RandomMove(b, previousMove)
if p.Algorithm != nil {
scoredMove := p.Algorithm.GetBestMove(p, b, previousMove)
if p.Debug {
p.printMoveDebug(b, scoredMove)
}
logger.MarkPerformance(b, scoredMove, p)
if scoredMove.Move.Start.Equals(scoredMove.Move.End) {
log.Printf("%s resigns, no best move available. Picking random.\n", p.Repr())
return &p.RandomMove(b, previousMove).Move
}
return &scoredMove.Move
} else {
panic("invalid ai algorithm")
}
if p.Debug {
p.printMoveDebug(b, m)
}
logger.MarkPerformance(b, m, p)
if m.Move.Start.Equals(m.Move.End) {
log.Printf("%s resigns, no best move available. Picking random.\n", p.Repr())
return &p.RandomMove(b, previousMove).Move
}
return &m.Move
}
}

Expand All @@ -156,11 +153,8 @@ func (p *Player) MakeMove(b *board.Board, previousMove *board.LastMove, logger *
}

func (p *Player) Repr() string {
c := "Black"
if p.PlayerColor == color.White {
c = "White"
}
return fmt.Sprintf("AI (%s,depth:%d - %s)", p.Algorithm, p.MaxSearchDepth, c)
return fmt.Sprintf("AI (%s - %s)",
p.Algorithm.GetName(), color.Names[p.PlayerColor])
}

func (p *Player) printMoveDebug(b *board.Board, m *ScoredMove) {
Expand All @@ -184,7 +178,6 @@ func (p *Player) printMoveDebug(b *board.Board, m *ScoredMove) {
result += fmt.Sprintf("\t\t%s\n", move.Print())
board.MakeMove(&move, debugBoard)
}
result += fmt.Sprintf("\nAI %s best move leads to score %d\n", p.Repr(), m.Score)
result += fmt.Sprintf("%s\n", p.Metrics.Print())
result += fmt.Sprintf("%s best move leads to score %d\n", p.Repr(), m.Score)
fmt.Print(result)
Expand Down
Loading

0 comments on commit cb57cab

Please sign in to comment.