diff --git a/Chess.iml b/Chess.iml
index a1d78f435..f3c545329 100644
--- a/Chess.iml
+++ b/Chess.iml
@@ -21,6 +21,7 @@
+
diff --git a/pom.xml b/pom.xml
index cdad349b3..e4be67de9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,13 @@
test
+
+ com.github.stefanbirkner
+ system-rules
+ 1.19.0
+ test
+
+
diff --git a/src/main/bat/runTwoEngines.bat b/src/main/bat/runTwoEngines.bat
index 5a7995dbd..874fa1a9d 100644
--- a/src/main/bat/runTwoEngines.bat
+++ b/src/main/bat/runTwoEngines.bat
@@ -5,7 +5,8 @@ call variables.bat
SET RUNNABLE_JAR_DIRECTORY_2=%WINBOARD_INSTALLATION_PATH%\LeokomChessTest
SET RUN_JAR_PATH_2=%RUNNABLE_JAR_DIRECTORY_2%\Chess.jar
-rem you may specify -Dblack=Simple
+rem you may specify -Dblack.engine=Simple (or -Dblack=Simple if the second instance is Chess <= 0.3)
+rem for LegalPlayer you may specify -Dblack.depth=2 (if the second instance is Chess >= 0.4)
SET RUN_OPTIONS_2=
SET ENGINE_2=%JAVA_PATH% %RUN_OPTIONS_2% -jar %RUN_JAR_PATH_2%
diff --git a/src/main/bat/variables.bat b/src/main/bat/variables.bat
index 57bc30d38..11ec517c5 100644
--- a/src/main/bat/variables.bat
+++ b/src/main/bat/variables.bat
@@ -6,7 +6,8 @@ rem it should be equal to 'project.deployDirectory' property in pom.xml
SET RUNNABLE_JAR_DIRECTORY=%WINBOARD_INSTALLATION_PATH%\LeokomChess
SET JAVA_PATH=Q:\Program Files\Java\jdk1.8.0_162\bin\java.exe
SET RUN_JAR_PATH=%RUNNABLE_JAR_DIRECTORY%\Chess.jar
-rem you may pass -Dblack=Simple to choose a different engine for blacks
-SET RUN_OPTIONS=
+rem you may pass -Dblack.engine=Simple to choose a different engine for blacks
+rem for LegalPlayer you may specify -Dblack.depth (1 or 2)
+SET RUN_OPTIONS=-Dblack.depth=2
SET ENGINE=%JAVA_PATH% %RUN_OPTIONS% -jar %RUN_JAR_PATH%
\ No newline at end of file
diff --git a/src/main/java/com/leokom/chess/MainRunner.java b/src/main/java/com/leokom/chess/MainRunner.java
index 52f3c79d3..85bace134 100644
--- a/src/main/java/com/leokom/chess/MainRunner.java
+++ b/src/main/java/com/leokom/chess/MainRunner.java
@@ -22,10 +22,10 @@ private MainRunner() {
*
* The parameters are provided via easier-to-use Java system properties way.
*
- * Supported parameters:
+ * General parameters:
*
- *
-Dwhite=engineName
- *
-Dblack=engineName
+ *
-Dwhite.engine=engineName
+ *
-Dblack.engine=engineName
*
*
* engineName could be any of:
@@ -34,11 +34,25 @@ private MainRunner() {
*
Simple
*
Legal
*
+ *
+ * Default players:
+ *
+ *
-Dwhite.engine=Winboard
+ *
-Dblack.engine=Legal
+ *
*
- * Default players:
+ *
+ *
+ * Optional parameters for LegalPlayer
+ *
+ *
-Dwhite.depth=depth in plies
+ *
-Dblack.depth=depth in plies
+ *
+ *
+ * depth in plies can be any of:
*
- *
-Dwhite=Winboard
- *
-Dblack=Legal
+ *
1
+ *
2
*
*
* For Winboard opponents always specify them as Black
diff --git a/src/main/java/com/leokom/chess/PlayerFactory.java b/src/main/java/com/leokom/chess/PlayerFactory.java
index 96b6c6681..744547de3 100644
--- a/src/main/java/com/leokom/chess/PlayerFactory.java
+++ b/src/main/java/com/leokom/chess/PlayerFactory.java
@@ -1,16 +1,14 @@
package com.leokom.chess;
-import com.google.common.collect.ImmutableMap;
import com.leokom.chess.engine.Side;
import com.leokom.chess.player.Player;
-import com.leokom.chess.player.legal.LegalPlayer;
-import com.leokom.chess.player.legal.brain.simple.SimpleBrain;
-import com.leokom.chess.player.winboard.WinboardPlayer;
+import com.leokom.chess.player.legal.LegalPlayerSupplier;
+import com.leokom.chess.player.legal.brain.simple.SimplePlayerSupplier;
+import com.leokom.chess.player.winboard.WinboardPlayerSupplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import java.util.Map;
-import java.util.function.Supplier;
+import java.util.Optional;
/**
* Create players for the chess game
@@ -18,14 +16,30 @@
* Author: Leonid
* Date-time: 06.05.14 22:45
*/
-public final class PlayerFactory {
+final class PlayerFactory {
private PlayerFactory() {}
private static Logger logger = LogManager.getLogger( PlayerFactory.class );
- //side -> name of system property that specifies player for the side
- private static final Map< Side, String > SYSTEM_PROPERTIES =
- ImmutableMap.of( Side.WHITE, "white", Side.BLACK, "black" );
+ /**
+ * Chess system properties.
+ * Represent properties in format 'side.property' (like 'white.depth' or 'black.engine')
+ */
+ static class ChessSystemProperty {
+ private final String propertyName;
+
+ ChessSystemProperty( String propertyName ) {
+ this.propertyName = propertyName;
+ }
+
+ Optional getFor( Side side ) {
+ return Optional.ofNullable(
+ System.getProperty(
+ String.format( "%s.%s", side.name().toLowerCase(), propertyName )
+ )
+ );
+ }
+ }
/**
* Create player for the side
@@ -40,56 +54,36 @@ private PlayerFactory() {}
* Winboard vs any other engine that uses System.out has no practical use (UCI?)
*
* LegalPlayer vs LegalPlayer is possible but can lead to StackOverflow due to
- * no limits on move amount and single-threaded model of execution
+ * no limits on move amount and single-threaded model of execution.
+ *
+ * LegalPlayer supports optional depth parameter.
*
* @param side side to create
* @return new instance of a player
*/
static Player createPlayer( Side side ) {
- final String engineName = System.getProperty( SYSTEM_PROPERTIES.get( side ) );
-
- logger.info( "Engine from system properties: " + engineName + ". Side = " + side );
-
- return selectPlayer( side, engineName ).create();
- }
-
- private static PlayerSelection selectPlayer( Side side, String engineName ) {
- if ( engineName == null ) {
- logger.info( "No selection done. Selecting default player" );
- return getDefaultPlayer( side );
- }
-
- switch ( engineName ) {
- case "Legal":
- return PlayerSelection.LEGAL;
- case "Simple":
- return PlayerSelection.SIMPLE;
- case "Winboard":
- return PlayerSelection.WINBOARD;
- default:
- logger.warn( "Unsupported option specified. Selecting default player" );
- return getDefaultPlayer( side );
- }
- }
-
- public enum PlayerSelection {
- LEGAL( LegalPlayer::new ),
- SIMPLE( () -> new LegalPlayer( new SimpleBrain() ) ),
- WINBOARD( WinboardPlayer::create );
-
- private final Supplier< Player > playerCreator;
-
- PlayerSelection( Supplier< Player > playerCreator ) {
- this.playerCreator = playerCreator;
- }
-
- public Player create() {
- return playerCreator.get();
- }
+ return new ChessSystemProperty("engine").getFor(side).map(engineName -> {
+ logger.info("Selecting an engine for Side = " + side + " by engine name = " + engineName);
+ switch (engineName) {
+ case "Legal":
+ return getLegalPlayerSupplier( side );
+ case "Simple":
+ return new SimplePlayerSupplier();
+ case "Winboard":
+ return new WinboardPlayerSupplier();
+ default:
+ throw new IllegalArgumentException( "The engine is not supported: " + engineName);
+ }
+ }).orElseGet(() -> {
+ logger.info( "Selecting a default engine for Side = " + side );
+ return side == Side.WHITE ? new WinboardPlayerSupplier() : getLegalPlayerSupplier( side );
+ }).get();
}
- private static PlayerSelection getDefaultPlayer( Side side ) {
- logger.info( "Selecting default engine for Side = " + side );
- return side == Side.WHITE ? PlayerSelection.WINBOARD : PlayerSelection.LEGAL;
+ private static LegalPlayerSupplier getLegalPlayerSupplier( Side side ) {
+ return new ChessSystemProperty("depth").getFor(side)
+ .map(Integer::valueOf)
+ .map(LegalPlayerSupplier::new) //takes depth parameter
+ .orElseGet(LegalPlayerSupplier::new); //without parameters, default constructor
}
}
diff --git a/src/main/java/com/leokom/chess/engine/GameState.java b/src/main/java/com/leokom/chess/engine/GameState.java
new file mode 100644
index 000000000..75c5cebc1
--- /dev/null
+++ b/src/main/java/com/leokom/chess/engine/GameState.java
@@ -0,0 +1,19 @@
+package com.leokom.chess.engine;
+
+import java.util.Set;
+
+/**
+ * The notion of game state is very generic and can be extracted to something chess-independent
+ * @param type of transitions
+ * @param current type (state)
+ */
+/*
+ Rather complex recursive generic to S class is introduced in order to support return of exactly
+ our class in the move method.
+ Inspired by https://www.sitepoint.com/self-types-with-javas-generics/
+ */
+public interface GameState< T extends GameTransition, S extends GameState > {
+ S move(T move);
+
+ Set getMoves();
+}
diff --git a/src/main/java/com/leokom/chess/engine/GameTransition.java b/src/main/java/com/leokom/chess/engine/GameTransition.java
new file mode 100644
index 000000000..968dc89b7
--- /dev/null
+++ b/src/main/java/com/leokom/chess/engine/GameTransition.java
@@ -0,0 +1,9 @@
+package com.leokom.chess.engine;
+
+/**
+ * The notion of game transition is an attempt to represent the game
+ * as a state automate.
+ * This notion is so generic that can be extracted to something chess-independent
+ */
+public interface GameTransition {
+}
diff --git a/src/main/java/com/leokom/chess/engine/Move.java b/src/main/java/com/leokom/chess/engine/Move.java
index a0a1243ab..607ce1730 100644
--- a/src/main/java/com/leokom/chess/engine/Move.java
+++ b/src/main/java/com/leokom/chess/engine/Move.java
@@ -13,7 +13,7 @@
*/
//REFACTOR: use this class on all applicable layers
//where pair of 'squareFrom, to' are used
-public final class Move {
+public final class Move implements GameTransition {
/**
* Size of promotion move (e.g. "h1Q")
*/
diff --git a/src/main/java/com/leokom/chess/engine/Position.java b/src/main/java/com/leokom/chess/engine/Position.java
index 650ad610f..cca28985b 100644
--- a/src/main/java/com/leokom/chess/engine/Position.java
+++ b/src/main/java/com/leokom/chess/engine/Position.java
@@ -33,7 +33,7 @@
* Author: Leonid
* Date-time: 21.08.12 15:55
*/
-public class Position {
+public class Position implements GameState< Move, Position > {
/**
* Chess rules mention moves counter must be calculated
* for both players
@@ -641,6 +641,7 @@ public Position move( String squareFrom, String move ) {
* @param move act of movement
* @return new position, which is received from current by making 1 move
*/
+ @Override
public Position move( Move move ) {
return new PositionGenerator( this ).generate( move );
}
@@ -803,6 +804,7 @@ void moveUnconditionally( String from, String to ) {
* for #getSideToMove()
* @return set of possible legal moves
*/
+ @Override
public Set< Move > getMoves() {
if ( terminal ) {
return new HashSet<>();
diff --git a/src/main/java/com/leokom/chess/engine/PositionBuilder.java b/src/main/java/com/leokom/chess/engine/PositionBuilder.java
index 2e1ee34e8..6452f8de8 100644
--- a/src/main/java/com/leokom/chess/engine/PositionBuilder.java
+++ b/src/main/java/com/leokom/chess/engine/PositionBuilder.java
@@ -38,6 +38,10 @@ public PositionBuilder setSide( Side side ) {
return this;
}
+ public Side getSideToMove() {
+ return position.getSideToMove();
+ }
+
public Position build() {
return position;
}
diff --git a/src/main/java/com/leokom/chess/player/legal/LegalPlayer.java b/src/main/java/com/leokom/chess/player/legal/LegalPlayer.java
index 26e40e8b5..f25a21d93 100644
--- a/src/main/java/com/leokom/chess/player/legal/LegalPlayer.java
+++ b/src/main/java/com/leokom/chess/player/legal/LegalPlayer.java
@@ -4,7 +4,7 @@
import com.leokom.chess.engine.Position;
import com.leokom.chess.engine.Side;
import com.leokom.chess.player.Player;
-import com.leokom.chess.player.legal.brain.common.Brain;
+import com.leokom.chess.player.legal.brain.common.GenericBrain;
import com.leokom.chess.player.legal.brain.common.Evaluator;
import com.leokom.chess.player.legal.brain.denormalized.DenormalizedBrain;
import com.leokom.chess.player.legal.brain.normalized.NormalizedBrain;
@@ -22,7 +22,7 @@
public class LegalPlayer implements Player {
private Player opponent;
private Position position = Position.getInitialPosition();
- private final Brain brain;
+ private final GenericBrain< Position, Move > brain;
private boolean recordingMode;
private Side ourSide;
@@ -34,16 +34,16 @@ public LegalPlayer() {
this( new DenormalizedBrain() );
}
- public LegalPlayer( Brain brain ) {
+ public LegalPlayer( GenericBrain< Position, Move > brain ) {
this.brain = brain;
}
/**
- * Create a player with standard decision maker and injected evaluators
- * @param brains brains to evaluate moves
+ * Create a player with standard decision maker and injected evaluator
+ * @param evaluator evaluator to evaluate moves
*/
- public LegalPlayer( Evaluator brains ) {
- this.brain = new NormalizedBrain( brains );
+ public LegalPlayer( Evaluator evaluator ) {
+ this.brain = new NormalizedBrain<>(evaluator);
}
@Override
@@ -110,10 +110,10 @@ void executeOurMove() {
final List< Move > bestMoves = brain.findBestMove( position );
if ( bestMoves == null ) {
- throw new IllegalStateException( "Brain should never return null but it did that" );
+ throw new IllegalStateException( "GenericBrain should never return null but it did that" );
}
if ( bestMoves.isEmpty() ) {
- throw new IllegalStateException( "Brain doesn't want to move while the position is not terminal! It's a bug in the brain" );
+ throw new IllegalStateException( "GenericBrain doesn't want to move while the position is not terminal! It's a bug in the brain" );
}
executeMoves( bestMoves );
diff --git a/src/main/java/com/leokom/chess/player/legal/LegalPlayerSupplier.java b/src/main/java/com/leokom/chess/player/legal/LegalPlayerSupplier.java
new file mode 100644
index 000000000..5e44dcce3
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/legal/LegalPlayerSupplier.java
@@ -0,0 +1,25 @@
+package com.leokom.chess.player.legal;
+
+import com.leokom.chess.player.Player;
+import com.leokom.chess.player.legal.brain.normalized.MasterEvaluator;
+import com.leokom.chess.player.legal.brain.normalized.NormalizedBrain;
+
+import java.util.function.Supplier;
+
+public class LegalPlayerSupplier implements Supplier {
+ //this depth has been used for years
+ private static final int DEFAULT_DEPTH = 1;
+ private final int depth;
+
+ public LegalPlayerSupplier() {
+ this(DEFAULT_DEPTH);
+ }
+
+ public LegalPlayerSupplier( int depth ) {
+ this.depth = depth;
+ }
+
+ public Player get() {
+ return new LegalPlayer( new NormalizedBrain<>( new MasterEvaluator(), depth ) );
+ }
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/common/Brain.java b/src/main/java/com/leokom/chess/player/legal/brain/common/Brain.java
index 4126e88a3..4ca56602a 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/common/Brain.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/common/Brain.java
@@ -3,48 +3,8 @@
import com.leokom.chess.engine.Move;
import com.leokom.chess.engine.Position;
-import java.util.List;
/**
- * The brain.
- *
- * Makes decision :
- * this is the move(s) to execute in current position.
- *
- * A valid implementation of the decision maker must be
- * stateless.
- *
- * Author: Leonid
- * Date-time: 23.08.16 22:53
+ * Chess-specific brain
*/
-public interface Brain {
-
- /**
- * Finds the best move(s) in the current position.
- * 2 moves can be returned if a player is allowed to execute
- * something extra after his move, e.g. OFFER DRAW
- * @param position position to analyze
- * @return best move according to current strategy, absence of moves means:
- * no moves are legal - we reached a terminal position
- */
- List< Move > findBestMove( Position position );
-
- /**
- * Get the best move to execute when it's not our
- * turn to move
- * @param position position to analyze
- * @return best move in not our turn
- * null if we don't want to move)
- */
- /*
- That's exactly why default methods were introduced:
- to allow evolving interface while not forcing existing
- implementations to increase complexity
- */
- default Move findBestMoveForOpponent( Position position ) {
- return null;
- }
-
- default String name() {
- return this.getClass().getSimpleName();
- }
-}
\ No newline at end of file
+public interface Brain extends GenericBrain{
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/common/Evaluator.java b/src/main/java/com/leokom/chess/player/legal/brain/common/Evaluator.java
index 0da6126ba..5549da884 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/common/Evaluator.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/common/Evaluator.java
@@ -11,7 +11,7 @@
* Date-time: 14.07.14 22:57
*/
@FunctionalInterface
-public interface Evaluator {
+public interface Evaluator extends GenericEvaluator< Position, Move > {
/**
* Get 'rating' of a move
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/common/EvaluatorType.java b/src/main/java/com/leokom/chess/player/legal/brain/common/EvaluatorType.java
index 06e129910..82afebec8 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/common/EvaluatorType.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/common/EvaluatorType.java
@@ -8,7 +8,7 @@
* Date-time: 19.04.16 22:46
*/
public enum EvaluatorType {
- CHECKMATE,
+ TERMINAL,
CASTLING_SAFETY,
CENTER_CONTROL,
MATERIAL,
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/common/GenericBrain.java b/src/main/java/com/leokom/chess/player/legal/brain/common/GenericBrain.java
new file mode 100644
index 000000000..90b428b5b
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/legal/brain/common/GenericBrain.java
@@ -0,0 +1,53 @@
+package com.leokom.chess.player.legal.brain.common;
+
+import com.leokom.chess.engine.GameState;
+import com.leokom.chess.engine.GameTransition;
+
+import java.util.List;
+/**
+ * The brain.
+ *
+ * Makes decision :
+ * this is the move(s) to execute in current position.
+ *
+ * A valid implementation of the decision maker must be
+ * stateless.
+ *
+ * @param game state
+ * @param transition type
+ *
+ * Author: Leonid
+ * Date-time: 23.08.16 22:53
+ */
+public interface GenericBrain< S extends GameState, T extends GameTransition> {
+
+ /**
+ * Finds the best move(s) in the current position.
+ * 2 moves can be returned if a player is allowed to execute
+ * something extra after his move, e.g. OFFER DRAW
+ * @param position position to analyze
+ * @return best move according to current strategy, absence of moves means:
+ * no moves are legal - we reached a terminal position
+ */
+ List findBestMove( S position );
+
+ /**
+ * Get the best move to execute when it's not our
+ * turn to move
+ * @param position position to analyze
+ * @return best move in not our turn
+ * null if we don't want to move)
+ */
+ /*
+ That's exactly why default methods were introduced:
+ to allow evolving interface while not forcing existing
+ implementations to increase complexity
+ */
+ default T findBestMoveForOpponent(S position ) {
+ return null;
+ }
+
+ default String name() {
+ return this.getClass().getSimpleName();
+ }
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/common/GenericEvaluator.java b/src/main/java/com/leokom/chess/player/legal/brain/common/GenericEvaluator.java
new file mode 100644
index 000000000..d2ab8b737
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/legal/brain/common/GenericEvaluator.java
@@ -0,0 +1,13 @@
+package com.leokom.chess.player.legal.brain.common;
+
+import com.leokom.chess.engine.GameState;
+import com.leokom.chess.engine.GameTransition;
+
+/**
+ * Generic evaluator, actually game-agnostic
+ * @param game state
+ * @param transition type
+ */
+public interface GenericEvaluator< S extends GameState, T extends GameTransition > {
+ double evaluateMove( S position, T move );
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/denormalized/CheckmateEvaluator.java b/src/main/java/com/leokom/chess/player/legal/brain/denormalized/CheckmateEvaluator.java
deleted file mode 100644
index 5aa67380e..000000000
--- a/src/main/java/com/leokom/chess/player/legal/brain/denormalized/CheckmateEvaluator.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.leokom.chess.player.legal.brain.denormalized;
-
-import com.leokom.chess.engine.Move;
-import com.leokom.chess.engine.Position;
-import com.leokom.chess.player.legal.brain.common.Evaluator;
-
-/**
- * Author: Leonid
- * Date-time: 01.03.15 22:32
- *
- * Checkmate is the highest goal of the whole game
- */
-class CheckmateEvaluator implements Evaluator {
-
- private static final int BEST_MOVE = 1;
- private static final int WORST_MOVE = 0;
-
- /**
- *
- * {@inheritDoc}
- * @return 0 or 1
- */
- @Override
- public double evaluateMove( Position position, Move move ) {
- final Position result = position.move( move );
- return result.isTerminal() &&
- result.getWinningSide() != null && //this excludes draws
- position.getSide( move.getFrom() ) == result.getWinningSide() ?
- BEST_MOVE : WORST_MOVE;
- }
-}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrain.java b/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrain.java
index b3304d4e5..9e32b2d41 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrain.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrain.java
@@ -4,10 +4,7 @@
import com.google.common.collect.Table;
import com.leokom.chess.engine.Move;
import com.leokom.chess.engine.Position;
-import com.leokom.chess.player.legal.brain.common.Brain;
-import com.leokom.chess.player.legal.brain.common.Evaluator;
-import com.leokom.chess.player.legal.brain.common.EvaluatorFactory;
-import com.leokom.chess.player.legal.brain.common.EvaluatorType;
+import com.leokom.chess.player.legal.brain.common.*;
import com.leokom.chess.player.legal.brain.internal.common.EvaluatorWeights;
import com.leokom.chess.player.legal.brain.normalized.NormalizedBrain;
import org.apache.logging.log4j.LogManager;
@@ -25,7 +22,16 @@ public class DenormalizedBrain implements Brain {
private static final Logger LOG = LogManager.getLogger();
private static final double DEFAULT_FOR_EQUAL_NOT_IN_RANGE = 0.5;
- private EvaluatorFactory evaluatorFactory = new DenormalizedEvaluatorFactory();
+ private final EvaluatorFactory evaluatorFactory = new DenormalizedEvaluatorFactory();
+ private final EvaluatorWeights evaluatorWeights;
+
+ public DenormalizedBrain() {
+ this( new EvaluatorWeights() );
+ }
+
+ DenormalizedBrain( EvaluatorWeights evaluatorWeights ) {
+ this.evaluatorWeights = evaluatorWeights;
+ }
@Override
public List< Move > findBestMove( Position position ) {
@@ -44,13 +50,7 @@ public List< Move > findBestMove( Position position ) {
Table weightedTable = generateWithWeights( normalizedTable );
logTable( weightedTable, "WEIGHTED" );
- return new NormalizedBrain( getEvaluator( weightedTable ) ).findBestMove( position );
- }
-
- private Evaluator getEvaluator(Table weightedTable) {
- return ( position, move ) ->
- //summing without converting to DoubleStream http://stackoverflow.com/q/24421140
- weightedTable.column( move ).values().stream().reduce( 0.0, Double::sum );
+ return new NormalizedBrain<>( new DenormalizedMasterEvaluator( weightedTable ) ).findBestMove( position );
}
private void logTable( Table weightedTable, String prefix ) {
@@ -60,7 +60,7 @@ private void logTable( Table weightedTable, String
}
private Table generateWithWeights( Table normalizedTable ) {
- final Map standardWeights = EvaluatorWeights.getStandardWeights();
+ final Map standardWeights = evaluatorWeights.asMap();
Table< EvaluatorType, Move, Double > result = HashBasedTable.create();
@@ -136,4 +136,25 @@ private double standardizedValueFormula( double maxValue, double minValue, Doubl
private interface Formula {
double accept( double maxValue, double minValue, Double value );
}
+
+ //this class is an initial attempt to introduce symmetry between denormalized and normalized brains
+ //as a matter of fact, the denormalized brain has mixed responsibilities of brain and master evaluator
+ private class DenormalizedMasterEvaluator implements Evaluator {
+ private final Table weightedTable;
+
+ DenormalizedMasterEvaluator(Table weightedTable) {
+ this.weightedTable = weightedTable;
+ }
+
+ @Override
+ public double evaluateMove(Position position, Move move) {
+ //the division by evaluators count is done to ensure that the result will be in [ 0, 1 ] measures which is needed for correct input to NormalizedBrain
+ //this division is very similar to that one done in MasterEvaluator
+ //for example if we had 2 evaluators with result eval1: 1, eval2: 0.5 plain sum would be 1.5 but the normalized one is 0.75
+
+ //summing without converting to DoubleStream http://stackoverflow.com/q/24421140
+ return weightedTable.column(move).values().stream().reduce(0.0, Double::sum) /
+ evaluatorWeights.size();
+ }
+ }
}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedEvaluatorFactory.java b/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedEvaluatorFactory.java
index bf5f44b21..9e0f2e4b5 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedEvaluatorFactory.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedEvaluatorFactory.java
@@ -4,6 +4,7 @@
import com.leokom.chess.player.legal.brain.common.Evaluator;
import com.leokom.chess.player.legal.brain.common.EvaluatorFactory;
import com.leokom.chess.player.legal.brain.common.EvaluatorType;
+import com.leokom.chess.player.legal.brain.normalized.TerminalEvaluator;
import java.util.EnumMap;
import java.util.Map;
@@ -23,7 +24,7 @@ public class DenormalizedEvaluatorFactory implements EvaluatorFactory {
evaluatorsMutable.put( EvaluatorType.ATTACK, new AttackEvaluator() );
evaluatorsMutable.put( EvaluatorType.CASTLING_SAFETY, new CastlingSafetyEvaluator() );
evaluatorsMutable.put( EvaluatorType.CENTER_CONTROL, new CenterControlEvaluator() );
- evaluatorsMutable.put( EvaluatorType.CHECKMATE, new CheckmateEvaluator() );
+ evaluatorsMutable.put( EvaluatorType.TERMINAL, new TerminalEvaluator() );
evaluatorsMutable.put( EvaluatorType.MATERIAL, new MaterialEvaluator() );
evaluatorsMutable.put( EvaluatorType.MOBILITY, new MobilityEvaluator() );
evaluatorsMutable.put( EvaluatorType.PROTECTION, new ProtectionEvaluator() );
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeights.java b/src/main/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeights.java
index cbb0dc924..d5b4b62a2 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeights.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeights.java
@@ -2,8 +2,11 @@
import com.leokom.chess.player.legal.brain.common.EvaluatorType;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Stream;
import static com.leokom.chess.player.legal.brain.common.EvaluatorType.*;
@@ -12,21 +15,43 @@
* Date-time: 27.08.16 21:54
*/
public final class EvaluatorWeights {
- private EvaluatorWeights() {}
-
- private static final double HIGHEST_PRIORITY = 100.0;
- private static final double INCREASED_PRIORITY = 3.0;
- private static final double NORMAL_PRIORITY = 1.0;
+ private static final double HIGHEST_PRIORITY = 1.0;
+ //historically it was a minimal multiplier that causes GOOD results for DenormalizedBrain, it may be irrelevant now
+ private static final double INCREASED_PRIORITY = 0.03;
+ private static final double NORMAL_PRIORITY = 0.01;
private static final double LOWEST_POSSIBLE = 0.0;
- public static Map getStandardWeights() {
+ private final Map weights;
+
+ public EvaluatorWeights() {
+ this( getStandardWeights() );
+ }
+
+ /**
+ * Create weights object, ensure constraint: every weight is in [ 0,1 ] range
+ * @param weights weights
+ */
+ public EvaluatorWeights(Map weights) {
+ verifyRange( weights );
+ this.weights = Collections.unmodifiableMap( new HashMap<>( weights ) );
+ }
+
+ //alternative would be: normalize during processing
+ //decided not to do that for simplicity
+ private void verifyRange(Map weights) {
+ if ( weights.values().stream().anyMatch( weight -> weight < 0.0 || weight > 1.0 ) ) {
+ throw new IllegalArgumentException( String.format( "Illegal weight outside of allowed range detected. Map: %s", weights ) );
+ }
+ }
+
+ private static Map getStandardWeights() {
//TODO: refactor to constant immutable map
Map result = new HashMap<>();
- result.put( CHECKMATE, HIGHEST_PRIORITY );
+ //terminal evaluator is still here till https://github.com/lrozenblyum/chess/issues/290
+ result.put( TERMINAL, HIGHEST_PRIORITY );
result.put( CASTLING_SAFETY, NORMAL_PRIORITY );
result.put( CENTER_CONTROL, NORMAL_PRIORITY );
result.put( MOBILITY, NORMAL_PRIORITY );
- //empirically found minimal multiplier that causes GOOD results for DenormalizedBrain
result.put( MATERIAL, INCREASED_PRIORITY );
result.put( PROTECTION, NORMAL_PRIORITY );
result.put( ATTACK, NORMAL_PRIORITY );
@@ -35,4 +60,16 @@ public static Map getStandardWeights() {
result.put( SPECIAL_MOVE, LOWEST_POSSIBLE );
return result;
}
+
+ public Stream> stream() {
+ return this.weights.entrySet().stream();
+ }
+
+ public int size() {
+ return this.weights.size();
+ }
+
+ public Map asMap() {
+ return this.weights;
+ }
}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluator.java b/src/main/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluator.java
index 7c5f9a3c5..01f0eac9d 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluator.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluator.java
@@ -3,17 +3,19 @@
import com.leokom.chess.engine.Move;
import com.leokom.chess.engine.Position;
import com.leokom.chess.player.legal.brain.common.Evaluator;
+import com.leokom.chess.player.legal.brain.common.EvaluatorFactory;
import com.leokom.chess.player.legal.brain.common.EvaluatorType;
import com.leokom.chess.player.legal.brain.internal.common.EvaluatorWeights;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Map;
+import java.util.stream.Stream;
/**
- * Central brain of a move ('brains')
+ * Main normalized evaluator. Delegates evaluation to other normalized evaluators.
*/
-class MasterEvaluator implements Evaluator {
+public class MasterEvaluator implements Evaluator {
private static final Logger LOG = LogManager.getLogger();
//we don't need to know that we can execute other moves
@@ -22,27 +24,58 @@ class MasterEvaluator implements Evaluator {
//among 2 'equal' moves we would like to select according to some
//compare 1-to-another logic
- private final Map evaluatorWeights;
+ private final EvaluatorWeights evaluatorWeights;
+ private final EvaluatorFactory evaluatorFactory;
- MasterEvaluator() {
- this( EvaluatorWeights.getStandardWeights() );
+ public MasterEvaluator() {
+ //standard weights
+ this( new EvaluatorWeights() );
}
- MasterEvaluator( Map weights ) {
- this.evaluatorWeights = weights;
+ /**
+ * create evaluator with custom weights
+ * @param weights evaluator -> weight
+ */
+ MasterEvaluator( EvaluatorWeights weights ) {
+ //standard evaluator factory
+ this( weights, new NormalizedEvaluatorFactory() );
+ }
+
+ MasterEvaluator( EvaluatorWeights evaluatorWeights, EvaluatorFactory evaluatorFactory ) {
+ this.evaluatorWeights = evaluatorWeights;
+ this.evaluatorFactory = evaluatorFactory;
}
@Override
public double evaluateMove( Position position, Move move ) {
- double result = evaluatorWeights.entrySet().stream().mapToDouble(evaluatorEntry -> {
- final Evaluator evaluator = new NormalizedEvaluatorFactory().get(evaluatorEntry.getKey());
+ if ( position.move( move ).isTerminal() ) {
+ double result = evaluatorFactory.get( EvaluatorType.TERMINAL ).evaluateMove(position, move);
+ LOG.info( "{} ===> {}", move, result );
+ return result;
+ }
+
+ // Terminal evaluator excluded because it's used above.
+ // NOTE: it's still in evaluatorWeights until DenormalizedBrain uses it (till https://github.com/lrozenblyum/chess/issues/290)
+ double result = evaluatorsExceptTerminal().mapToDouble(evaluatorEntry -> {
+ final Evaluator evaluator = evaluatorFactory.get(evaluatorEntry.getKey());
final double weight = evaluatorEntry.getValue();
final double evaluatorResponse = evaluator.evaluateMove(position, move);
final double moveEstimate = weight * evaluatorResponse;
LOG.debug( "{} [{}] : {} * {} = {}", move, evaluatorEntry.getKey(), weight, evaluatorResponse, moveEstimate );
return moveEstimate;
}).sum();
- LOG.info("{} ===> {}", move, result);
- return result;
+
+ //result that is in [ 0, 1 ] range
+ //depends on the fact that the weights themselves are in [ 0, 1 ]
+ double normalizedResult = result / evaluatorsExceptTerminal().count();
+
+ LOG.info("{} ===> {} ===> {}", move, result, normalizedResult);
+ return normalizedResult;
+ }
+
+ private Stream> evaluatorsExceptTerminal() {
+ return evaluatorWeights.stream().filter( evaluatorEntry ->
+ evaluatorEntry.getKey() != EvaluatorType.TERMINAL
+ );
}
}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrain.java b/src/main/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrain.java
index 488a36421..a57c25198 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrain.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrain.java
@@ -1,29 +1,72 @@
package com.leokom.chess.player.legal.brain.normalized;
+import com.leokom.chess.engine.GameState;
+import com.leokom.chess.engine.GameTransition;
import com.leokom.chess.engine.Move;
-import com.leokom.chess.engine.Position;
-import com.leokom.chess.player.legal.brain.common.Brain;
-import com.leokom.chess.player.legal.brain.common.Evaluator;
+import com.leokom.chess.player.legal.brain.common.GenericBrain;
+import com.leokom.chess.player.legal.brain.common.GenericEvaluator;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.ThreadContext;
-import java.util.*;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.ToDoubleBiFunction;
+import java.util.stream.Stream;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
/**
- * Initial decision maker based on MasterEvaluator.
- * independent evaluation of each move is delegated to MasterEvaluator.
- * You can inject any custom brains instead of MasterEvaluator via constructor.
+ * Initial decision maker.
+ *
+ * Historically it was based on MasterEvaluator.
+ * Now it has become generic (actually even not depending on chess-related notions).
+ * You can inject any custom evaluator that acts as a normalized one via constructor.
+ *
+ * We assume that the evaluator always evaluates the move from the side to move the next ply
+ * It was a logical assumption when we developed a 1-ply engine.
+ * It can still be kept.
+ * The alternative could be: stable evaluator that returns positive/negative result depending on color of the side to move
+ *
+ * @param game state
+ * @param transition type
*
* Author: Leonid
* Date-time: 23.08.16 22:54
*/
-public class NormalizedBrain implements Brain {
- private Evaluator brains;
+public class NormalizedBrain < S extends GameState, T extends GameTransition> implements GenericBrain {
+ //this constant will increase with chess evolution
+ private static final int MAXIMAL_SUPPORTED_DEPTH = 2;
+ //this is an absolute constant
+ private static final int MINIMAL_POSSIBLE_DEPTH = 1;
+ private final GenericEvaluator evaluator;
+ private final int pliesDepth;
- public NormalizedBrain() {
- this( new MasterEvaluator() );
+ /**
+ * Create normalized brain
+ * @param evaluator evaluator with results in [ 0, 1 ] range
+ */
+ public NormalizedBrain( GenericEvaluator evaluator ) {
+ this( evaluator, 1 );
}
- public NormalizedBrain(Evaluator brains ) {
- this.brains = brains;
+ /**
+ * Create brain with custom plies depth
+ * @param evaluator evaluator with results in [ 0, 1 ] range
+ * @param pliesDepth depth to think (1 or 2 are supported)
+ */
+ public NormalizedBrain(GenericEvaluator evaluator, int pliesDepth ) {
+ if ( pliesDepth < MINIMAL_POSSIBLE_DEPTH) {
+ throw new IllegalArgumentException( String.format( "This depth is wrong: %s", pliesDepth ) );
+ }
+
+ if ( pliesDepth > MAXIMAL_SUPPORTED_DEPTH) {
+ throw new IllegalArgumentException( String.format( "This depth is not supported yet: %s", pliesDepth ) );
+ }
+
+ this.evaluator = new ValidatingNormalizedEvaluator<>(evaluator);
+ this.pliesDepth = pliesDepth;
}
/**
@@ -45,21 +88,69 @@ public NormalizedBrain(Evaluator brains ) {
*
*/
@Override
- public List findBestMove(Position position ) {
- Map< Move, Double > moveRatings = new HashMap<>();
+ public List findBestMove( S position ) {
+ //just 1 or 2 is supported now
+ ToDoubleBiFunction< S, T > moveEvaluator = pliesDepth == 1 ?
+ this::evaluateMoveViaSinglePly :
+ this::evaluateMoveViaTwoPlies;
- //filtering Draw offers till #161 is solved
+ //1. filtering Draw offers till #161 is solved
//this looks safe since Offer draw cannot be a single legal move in a position.
-
//the best place to filter is this decision maker because it's used both by Normalized and Denormalized branches
- position.getMoves().stream().filter( move -> move != Move.OFFER_DRAW ).forEach( move ->
- moveRatings.put( move, brains.evaluateMove( position, move ) )
- );
- return getMoveWithMaxRating( moveRatings );
+ //2. in future we may even not materialize the map and continue the Stream API chain to find the best move
+ Map moveRatings =
+ getMovesWithoutDrawOffer( position ).collect(
+ toMap(
+ identity(),
+ move -> moveEvaluator.applyAsDouble( position, move )
+ )
+ );
+ List bestMove = getMoveWithMaxRating( moveRatings );
+ LogManager.getLogger().info( "Best move(s): {}", bestMove );
+ return bestMove;
+ }
+
+ private double evaluateMoveViaTwoPlies( S position, T move ) {
+ ThreadContext.put( "moveBeingAnalyzed", move.toString() );
+
+ S target = position.move( move );
+ List bestMove = new NormalizedBrain<>(this.evaluator, 1).findBestMove(target);
+
+ //can be empty in case of terminal position
+ if ( bestMove.isEmpty() ) {
+ LogManager.getLogger().info( "Evaluating just the current level" );
+ }
+
+ double moveRating = bestMove.isEmpty() ?
+ //falling back to the 1'st level
+ //trick: moving our evaluation results from [ 0, 1 ] to [ -1, 0 ] range
+ //where all the second level moves exist
+ // highly depends on evaluator range [ 0, 1 ] which is guaranteed by ValidatingNormalizedEvaluator
+ evaluator.evaluateMove(position, move) - 1 :
+ //negating because bigger for the opponents means worse for the current player
+ //composite moves handling split to https://github.com/lrozenblyum/chess/issues/291
+ -evaluator.evaluateMove(target, bestMove.get(0));
+
+ LogManager.getLogger().info( "result = {}", moveRating );
+ ThreadContext.clearAll();
+ return moveRating;
+ }
+
+ private double evaluateMoveViaSinglePly( S position, T move ) {
+ return evaluator.evaluateMove( position, move );
+ }
+
+ @Override
+ public String name() {
+ return String.format( "NormalizedBrain: %s depth", pliesDepth );
+ }
+
+ private Stream getMovesWithoutDrawOffer(S position) {
+ return position.getMoves().stream().filter(move -> move != Move.OFFER_DRAW);
}
- private List getMoveWithMaxRating( Map< Move, Double > moveValues ) {
+ private List getMoveWithMaxRating( Map moveValues ) {
return moveValues.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/normalized/TerminalEvaluator.java b/src/main/java/com/leokom/chess/player/legal/brain/normalized/TerminalEvaluator.java
new file mode 100644
index 000000000..65a09d769
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/legal/brain/normalized/TerminalEvaluator.java
@@ -0,0 +1,40 @@
+package com.leokom.chess.player.legal.brain.normalized;
+
+import com.leokom.chess.engine.Move;
+import com.leokom.chess.engine.Position;
+import com.leokom.chess.player.legal.brain.common.Evaluator;
+
+/**
+ * Author: Leonid
+ * Date-time: 01.03.15 22:32
+ *
+ * Evaluate final game state.
+ * Checkmate is the highest goal of the whole game and will mean win.
+ */
+public class TerminalEvaluator implements Evaluator {
+
+ private static final double BEST_MOVE = 1;
+ private static final double AVERAGE_MOVE = 0.5;
+ private static final double WORST_MOVE = 0;
+
+ /**
+ *
+ * {@inheritDoc}
+ * @return 0 or 1
+ */
+ @Override
+ public double evaluateMove( Position position, Move move ) {
+ final Position result = position.move( move );
+
+ if ( !result.isTerminal() ) {
+ return WORST_MOVE;
+ }
+
+ //assuming it's draw
+ if ( result.getWinningSide() == null ) {
+ return AVERAGE_MOVE;
+ }
+
+ return position.getSide( move.getFrom() ) == result.getWinningSide() ? BEST_MOVE : WORST_MOVE;
+ }
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/normalized/ValidatingNormalizedEvaluator.java b/src/main/java/com/leokom/chess/player/legal/brain/normalized/ValidatingNormalizedEvaluator.java
new file mode 100644
index 000000000..3f20f5181
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/legal/brain/normalized/ValidatingNormalizedEvaluator.java
@@ -0,0 +1,27 @@
+package com.leokom.chess.player.legal.brain.normalized;
+
+import com.leokom.chess.engine.GameState;
+import com.leokom.chess.engine.GameTransition;
+import com.leokom.chess.player.legal.brain.common.GenericEvaluator;
+
+/**
+ * Evaluator delegate that ensures [ 0, 1 ] constraint for the move
+ * @param game state
+ * @param transition type
+ */
+class ValidatingNormalizedEvaluator < S extends GameState, T extends GameTransition> implements GenericEvaluator {
+ private final GenericEvaluator delegate;
+
+ ValidatingNormalizedEvaluator(GenericEvaluator delegate ) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public double evaluateMove(S position, T move) {
+ double result = delegate.evaluateMove(position, move);
+ if ( result < 0.0 || result > 1.0 ) {
+ throw new IllegalArgumentException( String.format( "The value is outside of supported range: %s", result ) );
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/normalized/package-info.java b/src/main/java/com/leokom/chess/player/legal/brain/normalized/package-info.java
index 36487e07c..cbf186830 100644
--- a/src/main/java/com/leokom/chess/player/legal/brain/normalized/package-info.java
+++ b/src/main/java/com/leokom/chess/player/legal/brain/normalized/package-info.java
@@ -1,5 +1,5 @@
/**
- * Brain based on normalized evaluators.
+ * GenericBrain based on normalized evaluators.
*
* All evaluators in this package must return value
*
diff --git a/src/main/java/com/leokom/chess/player/legal/brain/simple/SimplePlayerSupplier.java b/src/main/java/com/leokom/chess/player/legal/brain/simple/SimplePlayerSupplier.java
new file mode 100644
index 000000000..c537c93f0
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/legal/brain/simple/SimplePlayerSupplier.java
@@ -0,0 +1,14 @@
+package com.leokom.chess.player.legal.brain.simple;
+
+import com.leokom.chess.player.Player;
+import com.leokom.chess.player.legal.LegalPlayer;
+
+import java.util.function.Supplier;
+
+public class SimplePlayerSupplier implements Supplier {
+
+ @Override
+ public Player get() {
+ return new LegalPlayer( new SimpleBrain() );
+ }
+}
diff --git a/src/main/java/com/leokom/chess/player/legal/package-info.java b/src/main/java/com/leokom/chess/player/legal/package-info.java
index 690068006..1ae6393ad 100644
--- a/src/main/java/com/leokom/chess/player/legal/package-info.java
+++ b/src/main/java/com/leokom/chess/player/legal/package-info.java
@@ -2,7 +2,7 @@
* Implementation of a player
* who can execute the legal moves according to chess rules.
*
- * Different strategies can be injected via Evaluator or Brain.
+ * Different strategies can be injected via Evaluator or GenericBrain.
*
* @see com.leokom.chess.player.legal.LegalPlayer
*/
diff --git a/src/main/java/com/leokom/chess/player/winboard/WinboardPlayer.java b/src/main/java/com/leokom/chess/player/winboard/WinboardPlayer.java
index e99ae6138..4ed8a4278 100644
--- a/src/main/java/com/leokom/chess/player/winboard/WinboardPlayer.java
+++ b/src/main/java/com/leokom/chess/player/winboard/WinboardPlayer.java
@@ -162,7 +162,7 @@ private boolean canClaimDrawBeExecutedNow() {
* @return instance of properly initialized Player against WinBoard-powered player
*
*/
- public static Player create() {
+ static Player create() {
//TODO: implement some singleton policy?
final WinboardCommunicator communicator = new WinboardCommunicator();
return new WinboardPlayer( new WinboardCommanderImpl( communicator ) );
diff --git a/src/main/java/com/leokom/chess/player/winboard/WinboardPlayerSupplier.java b/src/main/java/com/leokom/chess/player/winboard/WinboardPlayerSupplier.java
new file mode 100644
index 000000000..49d42c2f9
--- /dev/null
+++ b/src/main/java/com/leokom/chess/player/winboard/WinboardPlayerSupplier.java
@@ -0,0 +1,13 @@
+package com.leokom.chess.player.winboard;
+
+import com.leokom.chess.player.Player;
+
+import java.util.function.Supplier;
+
+public class WinboardPlayerSupplier implements Supplier {
+
+ @Override
+ public Player get() {
+ return WinboardPlayer.create();
+ }
+}
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
index 5ecfb0661..71b000ce3 100644
--- a/src/main/resources/log4j2.xml
+++ b/src/main/resources/log4j2.xml
@@ -1,13 +1,23 @@
+
+
+
+
+
-
+
+
+
+
+
+
diff --git a/src/test/java/com/leokom/chess/PlayerFactoryTest.java b/src/test/java/com/leokom/chess/PlayerFactoryTest.java
index 5a6bf96bf..a26a5b92a 100644
--- a/src/test/java/com/leokom/chess/PlayerFactoryTest.java
+++ b/src/test/java/com/leokom/chess/PlayerFactoryTest.java
@@ -3,41 +3,16 @@
import com.leokom.chess.engine.Side;
import com.leokom.chess.player.Player;
import com.leokom.chess.player.winboard.WinboardPlayer;
+import org.hamcrest.CoreMatchers;
import org.junit.*;
+import org.junit.contrib.java.lang.system.RestoreSystemProperties;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
public class PlayerFactoryTest {
- private static String whiteProperty;
- private static String blackProperty;
-
- @BeforeClass
- public static void preserveSystemProperties() {
- whiteProperty = System.getProperty( "white" );
- blackProperty = System.getProperty( "black" );
- }
-
- @AfterClass
- public static void restoreSystemProperties() {
- //if any of them is null, @After method already cleared it.
- //setting null value of system property causes NPE
- if ( whiteProperty != null ) {
- System.setProperty( "white", whiteProperty );
- }
-
- if ( blackProperty != null ) {
- System.setProperty( "black", blackProperty );
- }
- }
-
- //ensure one test has no influence on another
- @Before
- @After
- public void clearSystemProperties() {
- System.clearProperty( "black" );
- System.clearProperty( "white" );
- }
+ //snapshots all system properties before a test, restores after it
+ @Rule
+ public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
@Test
public void noSystemPropertiesDefaultPlayerBlack() {
@@ -47,19 +22,26 @@ public void noSystemPropertiesDefaultPlayerBlack() {
@Test
public void canSelectSimpleEngineForWhite() {
- System.setProperty( "white", "Simple" );
+ System.setProperty( "white.engine", "Simple" );
final Player player = PlayerFactory.createPlayer( Side.WHITE );
assertIsSimple( player );
}
+ @Test( expected = IllegalArgumentException.class )
+ public void failFastOnUnsupportedEngine() {
+ System.setProperty( "white.engine", "Unsupported" );
+
+ PlayerFactory.createPlayer( Side.WHITE );
+ }
+
private void assertIsSimple(Player player) {
assertEquals( "LegalPlayer : SimpleBrain", player.name() );
}
@Test
public void canSelectWinboardForBlack() {
- System.setProperty( "black", "Winboard" );
+ System.setProperty( "black.engine", "Winboard" );
final Player player = PlayerFactory.createPlayer( Side.BLACK );
assertTrue( player instanceof WinboardPlayer );
@@ -80,14 +62,72 @@ public void legalSelected() {
}
private void assertIsLegal( Player player ) {
- assertEquals( "LegalPlayer : DenormalizedBrain", player.name() );
+ assertThat( player.name(), CoreMatchers.startsWith( "LegalPlayer" ) );
}
@Test
public void legalSelectedWhite() {
- System.setProperty( "white", "Legal" );
+ System.setProperty( "white.engine", "Legal" );
final Player player = PlayerFactory.createPlayer( Side.WHITE );
assertIsLegal( player );
}
+
+ @Test
+ public void depth2FromCommandLineRespectedForWhite() {
+ System.setProperty( "white.engine", "Legal" );
+ System.setProperty( "white.depth", "2" );
+
+ final Player player = PlayerFactory.createPlayer( Side.WHITE );
+ assertDepth( player, 2 );
+ }
+
+ @Test
+ public void depth1FromCommandLineRespectedForWhite() {
+ System.setProperty( "white.engine", "Legal" );
+ System.setProperty( "white.depth", "1" );
+
+ final Player player = PlayerFactory.createPlayer( Side.WHITE );
+ assertDepth( player, 1 );
+ }
+
+ @Test
+ public void depth1FromCommandLineRespectedForBlack() {
+ System.setProperty( "black.engine", "Legal" );
+ System.setProperty( "black.depth", "1" );
+
+ final Player player = PlayerFactory.createPlayer( Side.BLACK );
+ assertDepth( player, 1 );
+ }
+
+ @Test
+ public void depth2FromCommandLineRespectedForBlack() {
+ System.setProperty( "black.engine", "Legal" );
+ System.setProperty( "black.depth", "2" );
+
+ final Player player = PlayerFactory.createPlayer( Side.BLACK );
+ assertDepth( player, 2 );
+ }
+
+ @Test
+ public void legalPlayerDepthCanBeProvidedEvenIfEngineIsNotProvided() {
+ //because legal is default one
+ System.setProperty( "black.depth", "2" );
+
+ final Player player = PlayerFactory.createPlayer( Side.BLACK );
+ assertDepth( player, 2 );
+ }
+
+ @Test
+ public void defaultDepthIs1() {
+ System.setProperty( "black.engine", "Legal" );
+
+ final Player player = PlayerFactory.createPlayer( Side.BLACK );
+ assertDepth( player, 1 );
+ }
+
+ private void assertDepth( Player player, int expectedDepth ) {
+ //shallow yet good enough check
+ assertThat( player.name(), CoreMatchers.containsString( String.valueOf( expectedDepth ) ) );
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/leokom/chess/Simulator.java b/src/test/java/com/leokom/chess/Simulator.java
index 87970577b..636739826 100644
--- a/src/test/java/com/leokom/chess/Simulator.java
+++ b/src/test/java/com/leokom/chess/Simulator.java
@@ -1,10 +1,13 @@
package com.leokom.chess;
import com.leokom.chess.player.Player;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.function.Supplier;
import java.util.stream.IntStream;
/**
@@ -21,16 +24,18 @@ class Simulator {
private static final int GAMES_IN_SINGLE_ITERATION = 2;
private final Player first;
private final Player second;
+ private final Logger logger;
private int timesToRun = 1;
- Simulator( PlayerFactory.PlayerSelection first, PlayerFactory.PlayerSelection second ) {
- this( first.create(), second.create() );
+ Simulator( Supplier< Player > first, Supplier< Player > second ) {
+ this( first.get(), second.get() );
}
Simulator( Player first, Player second ) {
this.first = first;
this.second = second;
+ this.logger = LogManager.getLogger();
}
/**
@@ -43,16 +48,21 @@ class Simulator {
* @return statistics about game results
*/
SimulatorStatistics run() {
+ logger.info("Starting simulation for {} and {}", first.name(), second.name());
List< Player > winners = new ArrayList<>();
IntStream.rangeClosed( 1, timesToRun ).forEach( iteration -> {
+ logger.info( "Simulation # {} of {}: starting...", iteration, timesToRun );
winners.add( createGame( first, second ).run() );
winners.add( createGame( second, first ).run() );
+ logger.info( "Simulation # {} of {}: done", iteration, timesToRun );
} );
final long firstWins = countWinsOf( winners, first );
final long secondWins = countWinsOf( winners, second );
final long totalGames = timesToRun * GAMES_IN_SINGLE_ITERATION;
- return new SimulatorStatistics( totalGames, firstWins, secondWins );
+ SimulatorStatistics simulatorStatistics = new SimulatorStatistics(totalGames, firstWins, secondWins);
+ logger.info("The simulation has been finished. Stats: {}", simulatorStatistics);
+ return simulatorStatistics;
}
private static long countWinsOf( List< Player > winners, Player player ) {
diff --git a/src/test/java/com/leokom/chess/SimulatorIT.java b/src/test/java/com/leokom/chess/SimulatorIT.java
index 10729d83f..3d3bec05e 100644
--- a/src/test/java/com/leokom/chess/SimulatorIT.java
+++ b/src/test/java/com/leokom/chess/SimulatorIT.java
@@ -5,16 +5,17 @@
import com.leokom.chess.engine.Side;
import com.leokom.chess.player.Player;
import com.leokom.chess.player.legal.LegalPlayer;
+import com.leokom.chess.player.legal.LegalPlayerSupplier;
import com.leokom.chess.player.legal.brain.common.Evaluator;
import com.leokom.chess.player.legal.brain.common.EvaluatorType;
import com.leokom.chess.player.legal.brain.denormalized.DenormalizedBrain;
+import com.leokom.chess.player.legal.brain.normalized.MasterEvaluator;
import com.leokom.chess.player.legal.brain.normalized.MasterEvaluatorBuilder;
import com.leokom.chess.player.legal.brain.normalized.NormalizedBrain;
+import com.leokom.chess.player.legal.brain.simple.SimplePlayerSupplier;
import org.junit.Ignore;
import org.junit.Test;
-import static com.leokom.chess.PlayerFactory.PlayerSelection.LEGAL;
-import static com.leokom.chess.PlayerFactory.PlayerSelection.SIMPLE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -132,25 +133,25 @@ private void programPlayers( Position position, Position ... positions ) {
@Test
public void legalVsSimpleNoCrash() {
- new Simulator( LEGAL, SIMPLE ).run();
+ new Simulator( new LegalPlayerSupplier(), new SimplePlayerSupplier() ).run();
}
//we expect the default brain of the legal player is much smarter than the simple one
@Test
public void legalVsSimpleStatistics() {
- final SimulatorStatistics statistics = new Simulator( LEGAL, SIMPLE ).run();
+ final SimulatorStatistics statistics = new Simulator( new LegalPlayerSupplier(), new SimplePlayerSupplier() ).run();
assertEquals( new SimulatorStatistics( 2, 2, 0 ), statistics );
}
@Test
public void simpleVsSimpleNoCrash() {
- new Simulator( SIMPLE, SIMPLE ).run();
+ new Simulator( new SimplePlayerSupplier(), new SimplePlayerSupplier() ).run();
}
@Test
public void simpleVsSimpleStatistics() {
- final SimulatorStatistics statistics = new Simulator( SIMPLE, SIMPLE ).run();
+ final SimulatorStatistics statistics = new Simulator( new SimplePlayerSupplier(), new SimplePlayerSupplier() ).run();
//now simple vs simple correctly draws at the second move
assertEquals( new SimulatorStatistics( 2, 0, 0 ), statistics );
@@ -159,7 +160,7 @@ public void simpleVsSimpleStatistics() {
//non-deterministic, it's not a business-requirement
@Test
public void legalVsLegalCustomEvaluator() {
- final Evaluator brainLikesToEatPieces = new MasterEvaluatorBuilder().weight( EvaluatorType.MATERIAL, 100.0 ).build();
+ final Evaluator brainLikesToEatPieces = new MasterEvaluatorBuilder().weight( EvaluatorType.MATERIAL, 1.0 ).build();
final SimulatorStatistics statistics =
new Simulator( new LegalPlayer(), new LegalPlayer( brainLikesToEatPieces ) ).run();
@@ -180,7 +181,7 @@ public void legalPlayerEqualProbableDraw() {
@Test
public void newBrainShouldBeBetter() {
final LegalPlayer withNewSkills = new LegalPlayer( new DenormalizedBrain() );
- final LegalPlayer classicPlayer = new LegalPlayer( new NormalizedBrain() );
+ final LegalPlayer classicPlayer = new LegalPlayer( new NormalizedBrain<>( new MasterEvaluator() ) );
final SimulatorStatistics statistics = new Simulator( withNewSkills, classicPlayer )
.gamePairs( 5 )
.run();
@@ -188,4 +189,16 @@ public void newBrainShouldBeBetter() {
assertTrue( statistics + " should prove advantage of the first player",
statistics.getFirstWins() > statistics.getSecondWins() );
}
+
+ @Test
+ public void normalizedPlayerWithDepth2IsBetterThanDepth1() {
+ final LegalPlayer deeperThinker = new LegalPlayer( new NormalizedBrain<>( new MasterEvaluator(), 2 ) );
+ final LegalPlayer classicPlayer = new LegalPlayer( new NormalizedBrain<>( new MasterEvaluator(), 1 ) );
+ final SimulatorStatistics statistics = new Simulator( deeperThinker, classicPlayer )
+ .gamePairs( 5 )
+ .run();
+
+ assertTrue( statistics + " should prove advantage of the first player",
+ statistics.getFirstWins() > statistics.getSecondWins() );
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/leokom/chess/SimulatorMultiDeltaIT.java b/src/test/java/com/leokom/chess/SimulatorMultiDeltaIT.java
index 50f16a880..2155e464c 100644
--- a/src/test/java/com/leokom/chess/SimulatorMultiDeltaIT.java
+++ b/src/test/java/com/leokom/chess/SimulatorMultiDeltaIT.java
@@ -24,7 +24,7 @@ public class SimulatorMultiDeltaIT {
/*
* Protection property.
- * Coefficient :: [ 0 .. 90 ] delta 10 ==> 10 coefficients probed
+ * Coefficient :: [ 0 .. 0.9 ] delta 0.1 ==> 10 coefficients probed
* Each probe :: 10 games (5 pairs of games with colours switched)
*
*/
@@ -38,31 +38,32 @@ public void attackingDeltas() {
simulateDeltas( this::createAttacker, 1 );
}
- private LegalPlayer createAttacker( Integer coefficient ) {
+ private LegalPlayer createAttacker( double coefficient ) {
return new LegalPlayer( new MasterEvaluatorBuilder().weight( EvaluatorType.ATTACK, coefficient ).build() );
}
- private void simulateDeltas( Function< Integer, LegalPlayer > whitePlayerGenerator, int gamePairsPerIteration ) {
- Map< Integer, SimulatorStatistics > statisticsMap = new TreeMap<>();
+ private void simulateDeltas( Function< Double, LegalPlayer > whitePlayerGenerator, int gamePairsPerIteration ) {
+ Map< Double, SimulatorStatistics > statisticsMap = new TreeMap<>();
for ( int coefficient = 0; coefficient <= 90; coefficient += 10 ) {
- final LegalPlayer protectionBasedPlayer = whitePlayerGenerator.apply( coefficient );
+ double doubleCoefficient = coefficient / 100.0; //normalizing
+ final LegalPlayer protectionBasedPlayer = whitePlayerGenerator.apply( doubleCoefficient );
final LegalPlayer classicPlayer = new LegalPlayer();
final SimulatorStatistics stats = new Simulator( protectionBasedPlayer, classicPlayer )
.gamePairs( gamePairsPerIteration ).run();
- statisticsMap.put( coefficient, stats );
+ statisticsMap.put( doubleCoefficient, stats );
}
printResults( statisticsMap );
}
- private LegalPlayer createProtector( int coefficient ) {
+ private LegalPlayer createProtector( double coefficient ) {
return new LegalPlayer( new MasterEvaluatorBuilder().weight( EvaluatorType.PROTECTION, coefficient ).build() );
}
- private void printResults( Map statisticsMap ) {
+ private void printResults( Map statisticsMap ) {
final String statsPrettyPrinted = statisticsMap.entrySet().stream()
.map( entry -> entry.getKey() + " ==> " + entry.getValue() )
.collect( Collectors.joining( "\n" ) );
diff --git a/src/test/java/com/leokom/chess/SimulatorMultiRunnerIT.java b/src/test/java/com/leokom/chess/SimulatorMultiRunnerIT.java
index 9d7bde6b9..43f1471d3 100644
--- a/src/test/java/com/leokom/chess/SimulatorMultiRunnerIT.java
+++ b/src/test/java/com/leokom/chess/SimulatorMultiRunnerIT.java
@@ -50,7 +50,7 @@ public static Iterable