From f51c184243fb9036db41b65fd6db2c671dc19acc Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Mon, 8 Jul 2019 16:44:21 +0200 Subject: [PATCH] Added multithreading to AIPlayer for better performance + MoveProcessor and ProcessingResult classes for handling multithreaded move calculation + Separate package for AIPlayer and its components --- src/dev/kske/chess/board/Pawn.java | 4 +- src/dev/kske/chess/game/AIPlayer.java | 55 ------------- src/dev/kske/chess/game/ai/AIPlayer.java | 80 +++++++++++++++++++ src/dev/kske/chess/game/ai/MoveProcessor.java | 59 ++++++++++++++ .../kske/chess/game/ai/ProcessingResult.java | 25 ++++++ src/dev/kske/chess/ui/GameModeDialog.java | 8 +- 6 files changed, 171 insertions(+), 60 deletions(-) delete mode 100644 src/dev/kske/chess/game/AIPlayer.java create mode 100644 src/dev/kske/chess/game/ai/AIPlayer.java create mode 100644 src/dev/kske/chess/game/ai/MoveProcessor.java create mode 100644 src/dev/kske/chess/game/ai/ProcessingResult.java diff --git a/src/dev/kske/chess/board/Pawn.java b/src/dev/kske/chess/board/Pawn.java index e43da04..0406409 100644 --- a/src/dev/kske/chess/board/Pawn.java +++ b/src/dev/kske/chess/board/Pawn.java @@ -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)); diff --git a/src/dev/kske/chess/game/AIPlayer.java b/src/dev/kske/chess/game/AIPlayer.java deleted file mode 100644 index fa15216..0000000 --- a/src/dev/kske/chess/game/AIPlayer.java +++ /dev/null @@ -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: Chess
- * File: AIPlayer.java
- * Created: 06.07.2019
- * Author: Kai S. K. Engelbart - */ -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; - } -} diff --git a/src/dev/kske/chess/game/ai/AIPlayer.java b/src/dev/kske/chess/game/ai/AIPlayer.java new file mode 100644 index 0000000..21a1155 --- /dev/null +++ b/src/dev/kske/chess/game/ai/AIPlayer.java @@ -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: Chess
+ * File: AIPlayer.java
+ * Created: 06.07.2019
+ * Author: Kai S. K. Engelbart + */ +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 moves = board.getMoves(color); + + /* + * Define move processors and split the available moves between them. + */ + int numThreads = Math.min(moves.size(), availableProcessors); + List 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 results = new ArrayList<>(numThreads); + try { + List> futures = executor.invokeAll(processors); + for (Future 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(); + } +} diff --git a/src/dev/kske/chess/game/ai/MoveProcessor.java b/src/dev/kske/chess/game/ai/MoveProcessor.java new file mode 100644 index 0000000..e7c5741 --- /dev/null +++ b/src/dev/kske/chess/game/ai/MoveProcessor.java @@ -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: Chess
+ * File: MoveProcessor.java
+ * Created: 08.07.2019
+ * Author: Kai S. K. Engelbart + */ +public class MoveProcessor implements Callable { + + private final Board board; + private final List rootMoves;; + private final Color color; + private final int maxDepth; + + private Move bestMove; + + public MoveProcessor(Board board, List 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 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; + } +} diff --git a/src/dev/kske/chess/game/ai/ProcessingResult.java b/src/dev/kske/chess/game/ai/ProcessingResult.java new file mode 100644 index 0000000..0740f87 --- /dev/null +++ b/src/dev/kske/chess/game/ai/ProcessingResult.java @@ -0,0 +1,25 @@ +package dev.kske.chess.game.ai; + +import dev.kske.chess.board.Move; + +/** + * Project: Chess
+ * File: ProcessingResult.java
+ * Created: 08.07.2019
+ * Author: Kai S. K. Engelbart + */ +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); + } +} diff --git a/src/dev/kske/chess/ui/GameModeDialog.java b/src/dev/kske/chess/ui/GameModeDialog.java index 0db92ee..aee5471 100644 --- a/src/dev/kske/chess/ui/GameModeDialog.java +++ b/src/dev/kske/chess/ui/GameModeDialog.java @@ -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: Chess
@@ -48,7 +48,7 @@ public class GameModeDialog extends JDialog { btnAI.addActionListener((evt) -> { Map 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 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);