Skip to content

Commit

Permalink
Merge pull request #69 from Vadman97/vadim-elo
Browse files Browse the repository at this point in the history
elo calculation, competition mode for comparing AI
  • Loading branch information
Devan Adhia authored Apr 11, 2019
2 parents f358316 + 8240d14 commit 684a7e1
Show file tree
Hide file tree
Showing 30 changed files with 630 additions and 147 deletions.
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
}
5 changes: 4 additions & 1 deletion pkg/chessai/board/bishop.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (r *Bishop) GetPosition() location.Location {
return r.Location
}

func (r *Bishop) GetMoves(board *Board) *[]location.Move {
func (r *Bishop) GetMoves(board *Board, onlyFirstMove bool) *[]location.Move {
var moves []location.Move
for i := 0; i < 4; i++ {
loc := r.GetPosition()
Expand All @@ -55,6 +55,9 @@ func (r *Bishop) GetMoves(board *Board) *[]location.Move {
validMove, checkNext := CheckLocationForPiece(r.GetColor(), loc, board)
if validMove {
moves = append(moves, location.Move{Start: r.GetPosition(), End: loc})
if onlyFirstMove {
return &moves
}
}
if !checkNext {
break
Expand Down
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| | | | |
75 changes: 63 additions & 12 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 {
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 All @@ -323,10 +349,13 @@ func (b *Board) getAllMoves(c byte) *[]location.Move {
if !b.IsEmpty(l) {
p := b.GetPiece(l)
if p.GetColor() == c {
additionalMoves := *p.GetMoves(b)
additionalMoves := *p.GetMoves(b, onlyFirstMove)
for _, nextMove := range additionalMoves {
if !b.willMoveLeaveKingInCheck(c, nextMove) {
moves = append(moves, nextMove)
if onlyFirstMove {
return &moves
}
}
}
}
Expand Down Expand Up @@ -453,16 +482,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 +504,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
25 changes: 16 additions & 9 deletions pkg/chessai/board/king.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,33 @@ func (r *King) GetPosition() location.Location {
/**
* Gets all possible moves for the King.
*/
func (r *King) GetMoves(board *Board) *[]location.Move {
func (r *King) GetMoves(board *Board, onlyFirstMove bool) *[]location.Move {
var moves []location.Move
moves = append(moves, *r.GetNormalMoves(board)...)
moves = append(moves, *r.GetCastleMoves(board)...)
moves = append(moves, *r.GetNormalMoves(board, onlyFirstMove)...)
if onlyFirstMove && len(moves) > 0 {
return &moves
}
moves = append(moves, *r.GetCastleMoves(board, onlyFirstMove)...)
return &moves
}

/*
* Determines possible "normal" moves for a king (move in any direction a distance of one).
*/
func (r *King) GetNormalMoves(board *Board) *[]location.Move {
func (r *King) GetNormalMoves(board *Board, onlyFirstMove bool) *[]location.Move {
var moves []location.Move
for i := int8(-1); i <= 1; i++ {
for j := int8(-1); j <= 1; j++ {
if i != 0 || j != 0 {
l := r.GetPosition()
l, inBounds := l.AddRelative(location.RelativeLocation{i, j})
l, inBounds := l.AddRelative(location.RelativeLocation{Row: i, Col: j})
if inBounds {
pieceOnLocation := board.GetPiece(l)
if (pieceOnLocation == nil) || (pieceOnLocation.GetColor() != r.Color) {
moves = append(moves, location.Move{r.GetPosition(), l})
moves = append(moves, location.Move{Start: r.GetPosition(), End: l})
if onlyFirstMove {
return &moves
}
}
}
}
Expand All @@ -70,7 +76,7 @@ func (r *King) GetNormalMoves(board *Board) *[]location.Move {
/**
* Determines if the king is able to left castle or right castle.
*/
func (r *King) GetCastleMoves(board *Board) *[]location.Move {
func (r *King) GetCastleMoves(board *Board, onlyFirstMove bool) *[]location.Move {
var moves []location.Move
if !board.GetFlag(FlagCastled, r.GetColor()) && !board.GetFlag(FlagKingMoved, r.GetColor()) {
right, left := r.GetPosition(), r.GetPosition()
Expand All @@ -81,7 +87,8 @@ func (r *King) GetCastleMoves(board *Board) *[]location.Move {
left, lin = left.AddRelative(location.LeftMove)
rightIn, leftIn = rightIn || rin, leftIn || lin
}
rightM, leftM := location.Move{r.GetPosition(), right}, location.Move{r.GetPosition(), left}
rightM, leftM := location.Move{Start: r.GetPosition(), End: right},
location.Move{Start: r.GetPosition(), End: left}
if rightIn && r.canCastle(&rightM, board) && !board.GetFlag(FlagRightRookMoved, r.GetColor()) {
moves = append(moves, rightM)
}
Expand All @@ -101,7 +108,7 @@ func (r *King) GetAttackableMoves(board *Board) AttackableBoard {
for j := int8(-1); j <= 1; j++ {
if i != 0 || j != 0 {
loc := r.GetPosition()
loc, inBounds := loc.AddRelative(location.RelativeLocation{i, j})
loc, inBounds := loc.AddRelative(location.RelativeLocation{Row: i, Col: j})
if inBounds {
SetLocationAttackable(attackableBoard, loc)
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/chessai/board/knight.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ func (r *Knight) getNextLocations(board *Board) *[]location.Location {
/**
* Calculates all valid moves that a knight can make.
*/
func (r *Knight) GetMoves(board *Board) *[]location.Move {
func (r *Knight) GetMoves(board *Board, onlyFirstMove bool) *[]location.Move {
var moves []location.Move
locations := r.getNextLocations(board)
for _, loc := range *locations {
pieceOnLocation := board.GetPiece(loc)
if pieceOnLocation == nil || pieceOnLocation.GetColor() != r.Color {
moves = append(moves, location.Move{Start: r.GetPosition(), End: loc})
if onlyFirstMove {
return &moves
}
}
}
return &moves
Expand Down
Loading

0 comments on commit 684a7e1

Please sign in to comment.