diff --git a/pkg/chessai/player/ai/abdada.go b/pkg/chessai/player/ai/abdada.go new file mode 100644 index 0000000..7cdd5ab --- /dev/null +++ b/pkg/chessai/player/ai/abdada.go @@ -0,0 +1,98 @@ +package ai + +import ( + "fmt" + "github.com/Vadman97/ChessAI3/pkg/chessai/board" + "github.com/Vadman97/ChessAI3/pkg/chessai/color" + "github.com/Vadman97/ChessAI3/pkg/chessai/location" + "github.com/Vadman97/ChessAI3/pkg/chessai/transposition_table" + "time" +) + +func (ab *ABDADA) ABDADA(root *board.Board, depth, alpha, beta int, exclusiveProbe bool, currentPlayer color.Color, previousMove *board.LastMove) *ScoredMove { + if depth == 0 { + return &ScoredMove{ + Score: ab.player.EvaluateBoard(root, ab.player.PlayerColor).TotalScore, + } + } else { + var best ScoredMove + + answerChan := ab.asyncTTRead(root, uint16(depth), alpha, beta, currentPlayer, exclusiveProbe) + // generate moves while waiting for the answer ... + moves := root.GetAllMoves(currentPlayer, previousMove) + + // block and grab the answer + ttAnswer := <-answerChan + + } +} + +type ABDADA struct { + player *AIPlayer + currentSearchDepth int + lastSearchDepth int + lastSearchTime time.Duration +} + +func (ab *ABDADA) GetName() string { + return fmt.Sprintf("%s,[D:%d;T:%s]", AlgorithmABDADA, ab.lastSearchDepth, ab.lastSearchTime) +} + +func (ab *ABDADA) GetBestMove(p *AIPlayer, b *board.Board, previousMove *board.LastMove) *ScoredMove { + ab.player = p +} + +type TTAnswer struct { + alpha, beta, score int + bestMove location.Move + onEvaluation bool +} + +func (ab *ABDADA) asyncTTRead(root *board.Board, depth uint16, alpha, beta int, currentPlayer color.Color, exclusiveProbe bool) chan TTAnswer { + answerChan := make(chan TTAnswer) + go func(answerChan chan TTAnswer) { + answer := TTAnswer{ + alpha: alpha, + beta: beta, + score: NegInf, + } + if ab.player.TranspositionTableEnabled { + // transposition table lookup + h := root.Hash() + if e, ok := ab.player.alphaBetaTable.Read(&h, currentPlayer); ok { + entry := e.(*transposition_table.TranspositionTableEntryABDADA) + entry.Lock.Lock() + + if entry.Depth == depth && exclusiveProbe && entry.NumProcessors > 0 { + answer.onEvaluation = true + } else if entry.Depth >= depth { + if entry.EntryType == transposition_table.TrueScore { + answer.score = entry.Score + answer.alpha = entry.Score + answer.beta = entry.Score + } else if entry.EntryType == transposition_table.UpperBound && entry.Score < beta { + answer.score = entry.Score + answer.beta = entry.Score + } else if entry.EntryType == transposition_table.LowerBound && entry.Score > alpha { + answer.score = entry.Score + answer.alpha = entry.Score + } + + if entry.Depth == depth && answer.alpha < answer.beta { + // Increment the number of processors evaluating this node + entry.NumProcessors++ + } + } else { + // This is the first processor to evaluate this node + entry.Depth = depth + entry.EntryType = transposition_table.Unset + entry.NumProcessors = 1 + } + + entry.Lock.Unlock() + } + } + answerChan <- answer + }(answerChan) + return answerChan +} diff --git a/pkg/chessai/player/ai/ai_player.go b/pkg/chessai/player/ai/ai_player.go index 7e50962..90610ba 100644 --- a/pkg/chessai/player/ai/ai_player.go +++ b/pkg/chessai/player/ai/ai_player.go @@ -6,6 +6,7 @@ import ( "github.com/Vadman97/ChessAI3/pkg/chessai/color" "github.com/Vadman97/ChessAI3/pkg/chessai/config" "github.com/Vadman97/ChessAI3/pkg/chessai/location" + "github.com/Vadman97/ChessAI3/pkg/chessai/transposition_table" "github.com/Vadman97/ChessAI3/pkg/chessai/util" "log" "math" @@ -21,8 +22,9 @@ const ( const ( AlgorithmMiniMax = "MiniMax" - AlgorithmAlphaBetaWithMemory = "AlphaBetaMemory" + AlgorithmAlphaBetaWithMemory = "α/β Memory" AlgorithmMTDf = "MTDf" + AlgorithmABDADA = "ABDADA (α/β Parallel)" AlgorithmRandom = "Random" ) @@ -86,7 +88,7 @@ type AIPlayer struct { Debug bool PrintInfo bool evaluationMap *util.ConcurrentBoardMap - alphaBetaTable *util.TranspositionTable + alphaBetaTable *transposition_table.TranspositionTable printer chan string } @@ -101,7 +103,7 @@ func NewAIPlayer(c byte, algorithm Algorithm) *AIPlayer { Debug: config.Get().LogDebug, PrintInfo: config.Get().PrintPlayerInfo, evaluationMap: util.NewConcurrentBoardMap(), - alphaBetaTable: util.NewTranspositionTable(), + alphaBetaTable: transposition_table.NewTranspositionTable(), printer: make(chan string, 1000000), } if config.Get().UseOpenings { @@ -203,7 +205,7 @@ func (p *AIPlayer) printMoveDebug(b *board.Board, m *ScoredMove) { func (p *AIPlayer) ClearCaches() { // TODO(Vadim) find better way to pick when to clear, based on size #49 p.evaluationMap = util.NewConcurrentBoardMap() - p.alphaBetaTable = util.NewTranspositionTable() + p.alphaBetaTable = transposition_table.NewTranspositionTable() } func (p *AIPlayer) printThread(stop chan bool) { diff --git a/pkg/chessai/player/ai/alpha_beta.go b/pkg/chessai/player/ai/alpha_beta.go index 36c0138..1f5cb04 100644 --- a/pkg/chessai/player/ai/alpha_beta.go +++ b/pkg/chessai/player/ai/alpha_beta.go @@ -3,6 +3,7 @@ package ai import ( "fmt" "github.com/Vadman97/ChessAI3/pkg/chessai/board" + "github.com/Vadman97/ChessAI3/pkg/chessai/transposition_table" "github.com/Vadman97/ChessAI3/pkg/chessai/util" ) @@ -38,27 +39,28 @@ func (ab *AlphaBetaWithMemory) AlphaBetaWithMemory(root *board.Board, depth, alp // transposition table lookup h = root.Hash() if entry, ok := ab.player.alphaBetaTable.Read(&h, currentPlayer); ok { - if entry.Lower > NegInf && entry.Lower >= beta { + abEntry := entry.(*transposition_table.TranspositionTableEntryABMemory) + if abEntry.Lower > NegInf && abEntry.Lower >= beta { ab.player.Metrics.MovesPrunedTransposition++ return &ScoredMove{ - Score: entry.Lower, - Move: entry.BestMove, + Score: abEntry.Lower, + Move: abEntry.BestMove, } - } else if entry.Upper < PosInf && entry.Upper <= alpha { + } else if abEntry.Upper < PosInf && abEntry.Upper <= alpha { ab.player.Metrics.MovesPrunedTransposition++ return &ScoredMove{ - Score: entry.Upper, - Move: entry.BestMove, + Score: abEntry.Upper, + Move: abEntry.BestMove, } } - if entry.Lower > NegInf && entry.Lower > alpha { + if abEntry.Lower > NegInf && abEntry.Lower > alpha { ab.player.Metrics.MovesABImprovedTransposition++ - alpha = entry.Lower - // TODO(Vadim) first in for loop of moves try entry.BestMove, same in other else + alpha = abEntry.Lower + // TODO(Vadim) first in for loop of moves try abEntry.BestMove, same in other else } - if entry.Upper < PosInf && entry.Upper < beta { + if abEntry.Upper < PosInf && abEntry.Upper < beta { ab.player.Metrics.MovesABImprovedTransposition++ - beta = entry.Upper + beta = abEntry.Upper } } } @@ -117,19 +119,19 @@ func (ab *AlphaBetaWithMemory) AlphaBetaWithMemory(root *board.Board, depth, alp if !ab.abort && ab.player.TranspositionTableEnabled { if best.Score >= beta { - ab.player.alphaBetaTable.Store(&h, currentPlayer, &util.TranspositionTableEntry{ + ab.player.alphaBetaTable.Store(&h, currentPlayer, &transposition_table.TranspositionTableEntryABMemory{ Lower: best.Score, Upper: PosInf, BestMove: best.Move, }) } else if best.Score > alpha && best.Score < beta { - ab.player.alphaBetaTable.Store(&h, currentPlayer, &util.TranspositionTableEntry{ + ab.player.alphaBetaTable.Store(&h, currentPlayer, &transposition_table.TranspositionTableEntryABMemory{ Lower: best.Score, Upper: best.Score, BestMove: best.Move, }) } else if best.Score <= alpha { - ab.player.alphaBetaTable.Store(&h, currentPlayer, &util.TranspositionTableEntry{ + ab.player.alphaBetaTable.Store(&h, currentPlayer, &transposition_table.TranspositionTableEntryABMemory{ Lower: NegInf, Upper: best.Score, BestMove: best.Move, diff --git a/pkg/chessai/transposition_table/transposition_table.go b/pkg/chessai/transposition_table/transposition_table.go new file mode 100644 index 0000000..0737efe --- /dev/null +++ b/pkg/chessai/transposition_table/transposition_table.go @@ -0,0 +1,83 @@ +package transposition_table + +import ( + "fmt" + "github.com/Vadman97/ChessAI3/pkg/chessai/location" + "github.com/Vadman97/ChessAI3/pkg/chessai/util" + "sync" +) + +type TranspositionTableEntryABMemory struct { + Lower, Upper int + BestMove location.Move +} + +const ( + Unset = byte(iota) + UpperBound = byte(iota) + LowerBound = byte(iota) + TrueScore = byte(iota) +) + +type TranspositionTableEntryABDADA struct { + Score int + BestMove location.Move + + // Flag that identifies the entry + EntryType byte + + // Length of subtree upon which score is based. + Depth uint16 + + // The number of processors currently evaluating the node related to the transposition table entry + NumProcessors uint16 + + Lock sync.Mutex +} + +type TranspositionTable struct { + entryMap map[util.BoardHash]map[byte]interface{} + numStored int + numReads, numHits int +} + +func NewTranspositionTable() *TranspositionTable { + var m TranspositionTable + if m.entryMap == nil { + m.entryMap = make(map[util.BoardHash]map[byte]interface{}) + } + return &m +} + +/* + Note: Transposition table does not support concurrent read/write at the moment +*/ +func (m *TranspositionTable) Store(hash *util.BoardHash, currentTurn byte, entry interface{}) { + _, ok := m.entryMap[*hash] + if !ok { + m.entryMap[*hash] = make(map[byte]interface{}) + } + m.entryMap[*hash][currentTurn] = entry + m.numStored++ +} + +func (m *TranspositionTable) Read(hash *util.BoardHash, currentTurn byte) (interface{}, bool) { + m.numReads++ + + m1, ok := m.entryMap[*hash] + if ok { + v, ok := m1[currentTurn] + if ok { + m.numHits++ + return v, true + } + } + return nil, false +} + +func (m *TranspositionTable) PrintMetrics() (result string) { + result += fmt.Sprintf("\tTotal entries in transposition table %d\n", m.numStored) + result += fmt.Sprintf("\tHit ratio %f%% (%d/%d)\n", 100.0*float64(m.numHits)/float64(m.numReads), + m.numHits, m.numReads) + return +} diff --git a/pkg/chessai/transposition_table/transposition_table_test.go b/pkg/chessai/transposition_table/transposition_table_test.go new file mode 100644 index 0000000..90bd8bc --- /dev/null +++ b/pkg/chessai/transposition_table/transposition_table_test.go @@ -0,0 +1,43 @@ +package transposition_table + +import ( + "github.com/Vadman97/ChessAI3/pkg/chessai/board" + "github.com/Vadman97/ChessAI3/pkg/chessai/color" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestTranspositionTable_StoreUpdateReadABDADA(t *testing.T) { + tt := NewTranspositionTable() + + b := board.Board{} + b.ResetDefault() + b.RandomizeIllegal() + h := b.Hash() + + ttEntry := TranspositionTableEntryABDADA{} + tt.Store(&h, color.Black, &ttEntry) + ttEntry.NumProcessors++ + + assert.Equal(t, uint16(1), ttEntry.NumProcessors) + + e, _ := tt.Read(&h, color.Black) + readEntry := e.(*TranspositionTableEntryABDADA) + + assert.Equal(t, uint16(1), readEntry.NumProcessors) + readEntry.NumProcessors++ + + assert.Equal(t, uint16(2), readEntry.NumProcessors) + assert.Equal(t, uint16(2), ttEntry.NumProcessors) + + newEntry := TranspositionTableEntryABDADA{} + tt.Store(&h, color.Black, &newEntry) + + assert.Equal(t, uint16(2), readEntry.NumProcessors) + assert.Equal(t, uint16(2), ttEntry.NumProcessors) + + e, _ = tt.Read(&h, color.Black) + readEntry = e.(*TranspositionTableEntryABDADA) + + assert.Equal(t, uint16(0), readEntry.NumProcessors) +} diff --git a/pkg/chessai/util/transposition_table.go b/pkg/chessai/util/transposition_table.go deleted file mode 100644 index 1702d44..0000000 --- a/pkg/chessai/util/transposition_table.go +++ /dev/null @@ -1,58 +0,0 @@ -package util - -import ( - "fmt" - "github.com/Vadman97/ChessAI3/pkg/chessai/location" -) - -type TranspositionTableEntry struct { - Lower, Upper int - BestMove location.Move -} - -type TranspositionTable struct { - entryMap map[BoardHash]map[byte]*TranspositionTableEntry - numStored int - numReads, numHits int -} - -func NewTranspositionTable() *TranspositionTable { - var m TranspositionTable - if m.entryMap == nil { - m.entryMap = make(map[BoardHash]map[byte]*TranspositionTableEntry) - } - return &m -} - -/* - Note: Transposition table does not support concurrent read/write at the moment -*/ -func (m *TranspositionTable) Store(hash *BoardHash, currentTurn byte, entry *TranspositionTableEntry) { - _, ok := m.entryMap[*hash] - if !ok { - m.entryMap[*hash] = make(map[byte]*TranspositionTableEntry) - } - m.entryMap[*hash][currentTurn] = entry - m.numStored++ -} - -func (m *TranspositionTable) Read(hash *BoardHash, currentTurn byte) (*TranspositionTableEntry, bool) { - m.numReads++ - - m1, ok := m.entryMap[*hash] - if ok { - v, ok := m1[currentTurn] - if ok { - m.numHits++ - return v, true - } - } - return nil, false -} - -func (m *TranspositionTable) PrintMetrics() (result string) { - result += fmt.Sprintf("\tTotal entries in transposition table %d\n", m.numStored) - result += fmt.Sprintf("\tHit ratio %f%% (%d/%d)\n", 100.0*float64(m.numHits)/float64(m.numReads), - m.numHits, m.numReads) - return -}