Added multithreading to AIPlayer for better performance

+ MoveProcessor and ProcessingResult classes for handling multithreaded
move calculation
+ Separate package for AIPlayer and its components
This commit is contained in:
Kai S. K. Engelbart 2019-07-08 16:44:21 +02:00
parent 0a8a8d481a
commit f51c184243
6 changed files with 171 additions and 60 deletions

View File

@ -17,7 +17,7 @@ public class Pawn extends Piece {
@Override
public boolean isValidMove(Move move) {
// TODO: en passant
// TODO: en passant, pawn promotion
boolean step = move.isVertical() && move.yDist == 1;
boolean doubleStep = move.isVertical() && move.yDist == 2;
boolean strafe = move.isDiagonal() && move.xDist == 1;
@ -43,6 +43,8 @@ public class Pawn extends Piece {
int sign = getColor() == Color.WHITE ? -1 : 1;
if (sign == -1 && pos.y == 1 || sign == 1 && pos.y == 7) return moves;
// Strafe left
if (pos.x > 0) {
Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign));

View File

@ -1,55 +0,0 @@
package dev.kske.chess.game;
import javax.swing.SwingUtilities;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>AIPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class AIPlayer extends Player {
private Move bestMove;
private int count;
public AIPlayer(Board board, Color color) {
super(board, color);
}
@Override
public void requestMove() {
new Thread(() -> {
count = 0;
findBestMove((Board) board.clone(), color, 0);
System.out.println("Moves processed: " + count);
SwingUtilities.invokeLater(() -> game.onMove(this, bestMove));
}).start();
}
private int findBestMove(Board board, Color color, int depth) {
int bestValue = Integer.MIN_VALUE;
for (Move move : board.getMoves(color)) {
++count;
Piece capturePiece = board.move(move);
int teamValue = board.evaluate(color);
int enemyValue = board.evaluate(color.opposite());
int valueChange = teamValue - enemyValue;
if (depth < 4) valueChange -= findBestMove(board, color.opposite(), depth + 1);
if (valueChange > bestValue) {
bestValue = valueChange;
if (depth == 0) bestMove = move;
}
board.revert(move, capturePiece);
}
return bestValue;
}
}

View File

@ -0,0 +1,80 @@
package dev.kske.chess.game.ai;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.SwingUtilities;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.Player;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>AIPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class AIPlayer extends Player {
private int availableProcessors;
private int maxDepth;
public AIPlayer(Board board, Color color, int maxDepth) {
super(board, color);
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth;
}
@Override
public void requestMove() {
/*
* Define some processing threads, split the available moves between them and
* retrieve the result after their execution.
*/
new Thread(() -> {
/*
* Get a copy of the board and the available moves.
*/
Board board = (Board) AIPlayer.this.board.clone();
List<Move> moves = board.getMoves(color);
/*
* Define move processors and split the available moves between them.
*/
int numThreads = Math.min(moves.size(), availableProcessors);
List<MoveProcessor> processors = new ArrayList<>(numThreads);
final int step = moves.size() / numThreads;
int rem = moves.size() % numThreads;
int beginIndex = 0, endIndex = 0;
for (int i = 0; i < numThreads; i++) {
if (rem-- > 0) ++endIndex;
endIndex += step;
processors.add(
new MoveProcessor((Board) board.clone(), moves.subList(beginIndex, endIndex), color, maxDepth));
beginIndex = endIndex;
}
/*
* Execute processors, get the best result and pass it back to the Game class
*/
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
try {
List<Future<ProcessingResult>> futures = executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures)
results.add(f.get());
executor.shutdown();
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score));
SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move));
}, "AIPlayer calculation setup").start();
}
}

View File

@ -0,0 +1,59 @@
package dev.kske.chess.game.ai;
import java.util.List;
import java.util.concurrent.Callable;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MoveProcessor.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class MoveProcessor implements Callable<ProcessingResult> {
private final Board board;
private final List<Move> rootMoves;;
private final Color color;
private final int maxDepth;
private Move bestMove;
public MoveProcessor(Board board, List<Move> rootMoves, Color color, int maxDepth) {
this.board = board;
this.rootMoves = rootMoves;
this.color = color;
this.maxDepth = maxDepth;
}
@Override
public ProcessingResult call() throws Exception {
int score = miniMax(board, rootMoves, color, 0);
return new ProcessingResult(bestMove, score);
}
private int miniMax(Board board, List<Move> moves, Color color, int depth) {
int bestValue = Integer.MIN_VALUE;
for (Move move : moves) {
Piece capturePiece = board.move(move);
int teamValue = board.evaluate(color);
int enemyValue = board.evaluate(color.opposite());
int valueChange = teamValue - enemyValue;
if (depth < maxDepth && valueChange >= 0)
valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1);
if (valueChange > bestValue) {
bestValue = valueChange;
if (depth == 0) bestMove = move;
}
board.revert(move, capturePiece);
}
return bestValue;
}
}

View File

@ -0,0 +1,25 @@
package dev.kske.chess.game.ai;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>ProcessingResult.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class ProcessingResult {
public final Move move;
public final int score;
public ProcessingResult(Move move, int score) {
this.move = move;
this.score = score;
}
@Override
public String toString() {
return String.format("ProcessingResult[Move = %s, Score = %d]", move, score);
}
}

View File

@ -8,10 +8,10 @@ import javax.swing.JButton;
import javax.swing.JDialog;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.AIPlayer;
import dev.kske.chess.game.Game;
import dev.kske.chess.game.NaturalPlayer;
import dev.kske.chess.game.Player;
import dev.kske.chess.game.ai.AIPlayer;
/**
* Project: <strong>Chess</strong><br>
@ -48,7 +48,7 @@ public class GameModeDialog extends JDialog {
btnAI.addActionListener((evt) -> {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new NaturalPlayer(boardPanel.getBoard(), Color.WHITE, boardPanel));
players.put(Color.BLACK, new AIPlayer(boardPanel.getBoard(), Color.BLACK));
players.put(Color.BLACK, new AIPlayer(boardPanel.getBoard(), Color.BLACK, 4));
startGame(players, boardPanel);
});
getContentPane().add(btnAI);
@ -56,8 +56,8 @@ public class GameModeDialog extends JDialog {
JButton btnAI2 = new JButton("AI against AI");
btnAI2.addActionListener((evt) -> {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new AIPlayer(boardPanel.getBoard(), Color.WHITE));
players.put(Color.BLACK, new AIPlayer(boardPanel.getBoard(), Color.BLACK));
players.put(Color.WHITE, new AIPlayer(boardPanel.getBoard(), Color.WHITE, 4));
players.put(Color.BLACK, new AIPlayer(boardPanel.getBoard(), Color.BLACK, 3));
startGame(players, boardPanel);
});
getContentPane().add(btnAI2);