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 data() { @Test public void runGamePair() { final Evaluator brainLikesToProtectItself = new MasterEvaluatorBuilder() - .weight( EvaluatorType.PROTECTION, 1000.0 ).build(); + .weight( EvaluatorType.PROTECTION, 1.0 ).build(); final SimulatorStatistics statistics = new Simulator( new LegalPlayer(), new LegalPlayer( brainLikesToProtectItself ) ) diff --git a/src/test/java/com/leokom/chess/engine/GameStateImpl.java b/src/test/java/com/leokom/chess/engine/GameStateImpl.java new file mode 100644 index 000000000..27bcf895d --- /dev/null +++ b/src/test/java/com/leokom/chess/engine/GameStateImpl.java @@ -0,0 +1,42 @@ +package com.leokom.chess.engine; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.Set; + +//fake implementation for test purposes +public class GameStateImpl implements GameState< GameTransitionImpl, GameStateImpl > { + private final Map tree; + + //a few constructors for simplicity + public GameStateImpl() { + this(ImmutableMap.of()); + } + + public GameStateImpl(GameTransitionImpl gameTransition, GameStateImpl gameState) { + this( ImmutableMap.of( gameTransition, gameState ) ); + } + + public GameStateImpl(GameTransitionImpl gameTransition, GameStateImpl gameState, GameTransitionImpl gameTransition2, GameStateImpl gameState2) { + this( ImmutableMap.of( gameTransition, gameState, gameTransition2, gameState2 ) ); + } + + public GameStateImpl(GameTransitionImpl gameTransition, GameStateImpl gameState, GameTransitionImpl gameTransition2, GameStateImpl gameState2, + GameTransitionImpl gameTransition3, GameStateImpl gameState3 ) { + this( ImmutableMap.of( gameTransition, gameState, gameTransition2, gameState2, gameTransition3, gameState3 ) ); + } + + private GameStateImpl(Map tree ) { + this.tree = tree; + } + + @Override + public GameStateImpl move(GameTransitionImpl move) { + return tree.get(move); + } + + public Set getMoves() { + return tree.keySet(); + } +} diff --git a/src/test/java/com/leokom/chess/engine/GameTransitionImpl.java b/src/test/java/com/leokom/chess/engine/GameTransitionImpl.java new file mode 100644 index 000000000..e62ee9ab3 --- /dev/null +++ b/src/test/java/com/leokom/chess/engine/GameTransitionImpl.java @@ -0,0 +1,14 @@ +package com.leokom.chess.engine; + +//fake implementation for test purposes +public class GameTransitionImpl implements com.leokom.chess.engine.GameTransition { + private final long id; + + public GameTransitionImpl(long id ) { + this.id = id; + } + + public long getId() { + return this.id; + } +} diff --git a/src/test/java/com/leokom/chess/player/legal/LegalPlayerNameTest.java b/src/test/java/com/leokom/chess/player/legal/LegalPlayerNameTest.java index 75c455e5a..749b2c987 100644 --- a/src/test/java/com/leokom/chess/player/legal/LegalPlayerNameTest.java +++ b/src/test/java/com/leokom/chess/player/legal/LegalPlayerNameTest.java @@ -1,19 +1,26 @@ package com.leokom.chess.player.legal; +import com.leokom.chess.player.legal.brain.simple.SimplePlayerSupplier; +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.NormalizedBrain; 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; public class LegalPlayerNameTest { @Test - public void defaultName() { - assertEquals( "LegalPlayer : DenormalizedBrain", LEGAL.create().name() ); + public void denormalizedBrain() { + assertEquals( "LegalPlayer : DenormalizedBrain", new LegalPlayer( new DenormalizedBrain() ).name() ); } @Test - public void customNameWithCustomBrain() { - assertEquals( "LegalPlayer : SimpleBrain", SIMPLE.create().name() ); + public void normalizedBrain() { + assertEquals( "LegalPlayer : NormalizedBrain: 1 depth", new LegalPlayer( new NormalizedBrain<>( new MasterEvaluator()) ).name() ); + } + + @Test + public void simpleBrain() { + assertEquals( "LegalPlayer : SimpleBrain", new SimplePlayerSupplier().get().name() ); } } diff --git a/src/test/java/com/leokom/chess/player/legal/brain/common/EvaluatorAsserts.java b/src/test/java/com/leokom/chess/player/legal/brain/common/EvaluatorAsserts.java index 2e410bdc1..67537b072 100644 --- a/src/test/java/com/leokom/chess/player/legal/brain/common/EvaluatorAsserts.java +++ b/src/test/java/com/leokom/chess/player/legal/brain/common/EvaluatorAsserts.java @@ -17,7 +17,13 @@ public EvaluatorAsserts( Evaluator evaluator ) { } public void assertFirstBetter( PositionBuilder position, Move expectedBetter, Move expectedWorse ) { - assertFirstBetter( position.setSideOf( expectedBetter.getFrom() ).build(), expectedBetter, expectedWorse ); + if ( ! expectedBetter.isSpecial() ) { + position.setSideOf( expectedBetter.getFrom() ); + } + else if ( position.getSideToMove() == null ) { + throw new IllegalArgumentException( "We are unable autodetect the side to move from a special move, you should have set up it in the builder" ); + } + assertFirstBetter( position.build(), expectedBetter, expectedWorse ); } public void assertFirstBetter( Position position, Move expectedBetter, Move expectedWorse ) { diff --git a/src/test/java/com/leokom/chess/player/legal/brain/common/CheckmateEvaluatorTest.java b/src/test/java/com/leokom/chess/player/legal/brain/common/TerminalEvaluatorTest.java similarity index 82% rename from src/test/java/com/leokom/chess/player/legal/brain/common/CheckmateEvaluatorTest.java rename to src/test/java/com/leokom/chess/player/legal/brain/common/TerminalEvaluatorTest.java index 9a62b07ec..ace28cc5c 100644 --- a/src/test/java/com/leokom/chess/player/legal/brain/common/CheckmateEvaluatorTest.java +++ b/src/test/java/com/leokom/chess/player/legal/brain/common/TerminalEvaluatorTest.java @@ -7,7 +7,7 @@ * Author: Leonid * Date-time: 01.03.15 22:31 */ -public class CheckmateEvaluatorTest extends EvaluatorTestCase { +public class TerminalEvaluatorTest extends EvaluatorTestCase { @Test public void checkmateBetterThanNot() { PositionBuilder builder = new PositionBuilder() @@ -66,8 +66,19 @@ public void drawClaimIsWorseThanCheckmate() { asserts.assertFirstBetter( builder, checkmateMove, claimDrawMove ); } + @Test + public void drawClaimIsBetterThanResign() { + PositionBuilder builder = new PositionBuilder() + .add( Side.WHITE, "c1", PieceType.ROOK ) + .add( Side.WHITE, "b7", PieceType.ROOK ) + .add( Side.BLACK, "h8", PieceType.KING ); + + + asserts.assertFirstBetter( builder, Move.CLAIM_DRAW, Move.RESIGN ); + } + @Override EvaluatorType getEvaluatorType() { - return EvaluatorType.CHECKMATE; + return EvaluatorType.TERMINAL; } } diff --git a/src/test/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrainTest.java b/src/test/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrainTest.java index e872bc042..9dab75b8c 100644 --- a/src/test/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrainTest.java +++ b/src/test/java/com/leokom/chess/player/legal/brain/denormalized/DenormalizedBrainTest.java @@ -2,13 +2,18 @@ import com.leokom.chess.engine.*; import com.leokom.chess.player.legal.brain.common.Brain; +import com.leokom.chess.player.legal.brain.common.EvaluatorType; +import com.leokom.chess.player.legal.brain.internal.common.EvaluatorWeights; import org.junit.Before; import org.junit.Test; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.*; /** * Author: Leonid @@ -49,4 +54,12 @@ public void beSmartALittle() { assertFalse( movesFound.isEmpty() ); assertEquals( captureIsSmartest, movesFound.get( 0 ) ); } + + @Test + public void nonStandardEvaluatorWeightsMustNotCrash() { + //maximal legal 1.0 weight set up + Map weights = Arrays.stream(EvaluatorType.values()).collect(Collectors.toMap(Function.identity(), type -> 1.0)); + EvaluatorWeights evaluatorWeights = new EvaluatorWeights(weights); + assertNotNull( new DenormalizedBrain( evaluatorWeights ).findBestMove( Position.getInitialPosition() ) ); + } } \ No newline at end of file diff --git a/src/test/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeightsTest.java b/src/test/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeightsTest.java new file mode 100644 index 000000000..2e1ea6f78 --- /dev/null +++ b/src/test/java/com/leokom/chess/player/legal/brain/internal/common/EvaluatorWeightsTest.java @@ -0,0 +1,32 @@ +package com.leokom.chess.player.legal.brain.internal.common; + +import com.leokom.chess.player.legal.brain.common.EvaluatorType; +import org.junit.Test; +import org.mutabilitydetector.unittesting.MutabilityAssert; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class EvaluatorWeightsTest { + @Test + public void weightsAreImmutable() { + MutabilityAssert.assertImmutable( EvaluatorWeights.class ); + } + + @Test( expected = IllegalArgumentException.class ) + public void evaluatorWeightBigger1NotAccepted() { + Map weights = new HashMap<>(); + weights.put( EvaluatorType.PROTECTION, 1.1 ); + new EvaluatorWeights( weights ); + } + + + @Test( expected = IllegalArgumentException.class ) + public void evaluatorWeightLess0NotAccepted() { + Map weights = new HashMap<>(); + weights.put( EvaluatorType.PROTECTION, -0.1 ); + new EvaluatorWeights( weights ); + } +} \ No newline at end of file diff --git a/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorBuilder.java b/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorBuilder.java index 81ce9068c..f13ea898f 100644 --- a/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorBuilder.java +++ b/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorBuilder.java @@ -3,6 +3,7 @@ import com.leokom.chess.player.legal.brain.common.EvaluatorType; import com.leokom.chess.player.legal.brain.internal.common.EvaluatorWeights; +import java.util.HashMap; import java.util.Map; /** @@ -13,7 +14,8 @@ * Date-time: 19.04.16 23:03 */ public class MasterEvaluatorBuilder { - private Map weights = EvaluatorWeights.getStandardWeights(); + //wrapping to a hash map to allow mutability + private Map weights = new HashMap<>( new EvaluatorWeights().asMap() ); public MasterEvaluatorBuilder weight( EvaluatorType evaluatorType, double weight ) { weights.put( evaluatorType, weight ); @@ -21,6 +23,6 @@ public MasterEvaluatorBuilder weight( EvaluatorType evaluatorType, double weight } public MasterEvaluator build() { - return new MasterEvaluator( weights ); + return new MasterEvaluator( new EvaluatorWeights( weights ) ); } } \ No newline at end of file diff --git a/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorTest.java b/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorTest.java index 7fe3e1379..25e1313ea 100644 --- a/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorTest.java +++ b/src/test/java/com/leokom/chess/player/legal/brain/normalized/MasterEvaluatorTest.java @@ -3,8 +3,19 @@ import com.leokom.chess.engine.*; import com.leokom.chess.player.legal.brain.common.Evaluator; import com.leokom.chess.player.legal.brain.common.EvaluatorAsserts; +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.junit.Before; import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class MasterEvaluatorTest { private Evaluator evaluator; @@ -31,4 +42,75 @@ public void beSmartALittle() { new EvaluatorAsserts( evaluator ) .assertFirstBetter( position, simpleMove, captureWithRiskToLoseQueen ); } + + @Test + public void resignIsWeak() { + Position position = Position.getInitialPosition(); + + new EvaluatorAsserts( evaluator ) + .assertFirstBetter( position, new Move( "e2", "e4" ), Move.RESIGN ); + } + + //enforce rules that are valid for the whole normalized package, to MasterEvaluator itself + @Test + public void allMovesMustBeEvaluatedFrom0To1() { + Position position = Position.getInitialPosition(); + + assertAllMovesEvaluatedIn0To1Range(position, evaluator); + } + + private void assertAllMovesEvaluatedIn0To1Range(Position position, Evaluator evaluatorToValidate) { + position.getMoves().forEach( move -> { + double result = evaluatorToValidate.evaluateMove(position, move); + assertTrue( + String.format( "The move %s must be evaluated in range [0,1], actually: %s", move, result ) + ,result >= 0.0 && result <= 1.0 ); + } ); + } + + @Test + public void allMovesMustBeEvaluatedFrom0To1EvenWithCustomWeights() { + Map weights = new HashMap<>(); + Arrays.stream( EvaluatorType.values() ).forEach( type -> weights.put( type, 1.0 ) ); + + MasterEvaluator masterEvaluatorWithCustomWeights = new MasterEvaluator(new EvaluatorWeights(weights)); + assertAllMovesEvaluatedIn0To1Range( Position.getInitialPosition(), masterEvaluatorWithCustomWeights ); + } + + @Test + public void singleNonTerminalEvaluatorWeightMaximalResultIs1() { + Map weights = new HashMap<>(); + weights.put( EvaluatorType.TERMINAL, 1.0 ); + weights.put( EvaluatorType.CASTLING_SAFETY, 1.0 ); //max weight + + EvaluatorFactory factory = Mockito.mock(EvaluatorFactory.class); + Evaluator castlingEvaluatorMock = Mockito.mock(Evaluator.class); + //max estimate + Mockito.when( castlingEvaluatorMock.evaluateMove( Mockito.any(), Mockito.any() ) ).thenReturn( 1.0 ); + Mockito.when( factory.get( EvaluatorType.CASTLING_SAFETY )) .thenReturn( castlingEvaluatorMock ); + + MasterEvaluator masterEvaluator = new MasterEvaluator( new EvaluatorWeights( weights ), factory ); + double evaluation = masterEvaluator.evaluateMove(Position.getInitialPosition(), new Move("e2", "e4")); + assertEquals( 1.0, evaluation, 0 ); + } + + //losing should get the minimal possible value + @Test + public void losingIsEvaluatedTo0() { + Position position = Position.getInitialPosition(); + assertEquals( 0.0, evaluator.evaluateMove(position, Move.RESIGN), 0 ); + } + + @Test + public void winningIsEvaluatedTo1() { + Position position = Position.getInitialPosition() + .move( "f2", "f3" ) + .move( "e7", "e5" ) + .move( "g2", "g4" ); + //fools checkmate is prepared + + + Move checkmateMove = new Move( "d8", "h4" ); + assertEquals( 1, evaluator.evaluateMove( position, checkmateMove ), 0 ); + } } \ No newline at end of file diff --git a/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainMasterEvaluatorTest.java b/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainMasterEvaluatorTest.java new file mode 100644 index 000000000..9a52f2d11 --- /dev/null +++ b/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainMasterEvaluatorTest.java @@ -0,0 +1,51 @@ +package com.leokom.chess.player.legal.brain.normalized; + + +import com.leokom.chess.engine.*; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +//it's move integration than NormalizedBrainPositionMoveTest +public class NormalizedBrainMasterEvaluatorTest { + @Test + public void resignIsNotTheBest() { + List bestMove = new NormalizedBrain<>(new MasterEvaluator(), 2) + .findBestMove(Position.getInitialPosition()); + assertNotEquals( Move.RESIGN, bestMove.get(0) ); + } + + @Test + public void drawOfferIsNotTheBest() { + List bestMove = new NormalizedBrain<>(new MasterEvaluator(), 2) + .findBestMove(Position.getInitialPosition()); + assertNotEquals( Move.OFFER_DRAW, bestMove.get(0) ); + } + + //it's not a strict requirement. I just want to exclude draw offer from the algorithm + //we're not ready for it yet. + //TODO: extract draw offer 2'nd level evaluation to a separate ticket + @Test + public void drawOfferIsNotTheBestResponse() { + List bestMove = new NormalizedBrain<>(new MasterEvaluator(), 2) + .findBestMove(Position.getInitialPosition().move( "e2", "e4" )); + assertNotEquals( Move.OFFER_DRAW, bestMove.get(0) ); + } + + @Test + public void winningMaterialNotBestIdeaIfCanCheckmate() { + Position position = new PositionBuilder() + .add(Side.BLACK, "a1", PieceType.KING) + .add(Side.WHITE, "c1", PieceType.KING) + .add(Side.WHITE, "c3", PieceType.ROOK) + .add(Side.BLACK, "h3", PieceType.BISHOP) + .build(); + + List result = new NormalizedBrain<>(new MasterEvaluator(), 2).findBestMove(position); + //we shouldn't try c3: h3 + assertEquals( new Move( "c3", "a3" ), result.get( 0 ) ); + } +} diff --git a/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainPositionMoveTest.java b/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainPositionMoveTest.java new file mode 100644 index 000000000..c6b6e7a4c --- /dev/null +++ b/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainPositionMoveTest.java @@ -0,0 +1,21 @@ +package com.leokom.chess.player.legal.brain.normalized; + +import com.leokom.chess.engine.Move; +import com.leokom.chess.engine.Position; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertFalse; + +//this test is based on Chess notions (in contrary to NormalizedBrainTest) +//so it can be considered being more integrations with Position, Move +public class NormalizedBrainPositionMoveTest { + + @Test + public void integrationWithPosition() { + List bestMove = new NormalizedBrain(((position, move) -> 0), 2) + .findBestMove(Position.getInitialPosition()); + assertFalse( bestMove.isEmpty() ); + } +} diff --git a/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainTest.java b/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainTest.java new file mode 100644 index 000000000..a14d7497c --- /dev/null +++ b/src/test/java/com/leokom/chess/player/legal/brain/normalized/NormalizedBrainTest.java @@ -0,0 +1,148 @@ +package com.leokom.chess.player.legal.brain.normalized; + +import com.leokom.chess.engine.GameStateImpl; +import com.leokom.chess.engine.GameTransitionImpl; +import com.leokom.chess.player.legal.brain.common.GenericEvaluator; +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class NormalizedBrainTest { + @Test + public void noMovesNoBestMove() { + GameStateImpl gameState = new GameStateImpl(); + + List result = new NormalizedBrain< GameStateImpl, GameTransitionImpl >((state, transition) -> 0).findBestMove(gameState); + assertTrue( result.isEmpty() ); + } + + @Test + public void singlePossibleMoveReturned() { + int moveId = 12345; + GameStateImpl gameState = new GameStateImpl( new GameTransitionImpl(moveId), new GameStateImpl() ); + + List result = new NormalizedBrain((state, transition) -> 0).findBestMove(gameState); + assertEquals( 1, result.size() ); + assertEquals( moveId, result.get(0).getId() ); + } + + @Test + public void betterMoveFound() { + GameStateImpl gameState = new GameStateImpl( new GameTransitionImpl(12), new GameStateImpl(), + new GameTransitionImpl( 20 ), new GameStateImpl() ); + + List result = new NormalizedBrain<>(getSimpleIdEvaluator()).findBestMove(gameState); + assertEquals( 1, result.size() ); + assertEquals( 20, result.get(0).getId() ); + } + + //we must not look to the 2'nd ply if we are limited by the 1'st one + @Test + public void singlePlyThinkingIsLimited() { + GameStateImpl gameState = new GameStateImpl( + new GameTransitionImpl(12), new GameStateImpl( new GameTransitionImpl( 0 ), new GameStateImpl() ), + new GameTransitionImpl( 20 ), new GameStateImpl( new GameTransitionImpl( 100 ), new GameStateImpl() ) ); // bigger means better for the opponent + + List result = new NormalizedBrain<>( getSimpleIdEvaluator()).findBestMove(gameState); + assertEquals( 1, result.size() ); + assertEquals( 20, result.get(0).getId() ); + } + + //we must look to the 2'nd ply and detect a really better move + @Test + public void secondPlyThinkingMustSuggestBetterMove() { + GameStateImpl gameState = new GameStateImpl( + new GameTransitionImpl(12), new GameStateImpl( new GameTransitionImpl( 0 ), new GameStateImpl() ), + new GameTransitionImpl( 20 ), new GameStateImpl( new GameTransitionImpl( 100 ), new GameStateImpl() ) ); + + List result = new NormalizedBrain<>( getSimpleIdEvaluator(),2).findBestMove(gameState); + assertEquals( 1, result.size() ); + assertEquals( 12, result.get(0).getId() ); + } + + // just a simple evaluation - let's say bigger id is better + private GenericEvaluator getSimpleIdEvaluator() { + return (state, transition) -> transition.getId() / 100.0; + } + + @Test + public void ifOpponentCanSelectCoolMoveDetectThat() { + GameStateImpl gameState = new GameStateImpl( + //here the opponent can execute an average move + new GameTransitionImpl(12), new GameStateImpl( new GameTransitionImpl( 50 ), new GameStateImpl() ), + //here he can execute a cool and a bad move. Thinking about him in positive way - that he'll select the best one + new GameTransitionImpl( 20 ), new GameStateImpl( new GameTransitionImpl( 0 ), new GameStateImpl(), new GameTransitionImpl( 100 ), new GameStateImpl() ) ); + + List result = new NormalizedBrain<>( getSimpleIdEvaluator(),2 ).findBestMove(gameState); + assertEquals( 1, result.size() ); + assertEquals( 12, result.get(0).getId() ); + } + + @Test + public void secondPlyThinkingNoCrashOnTerminalPosition() { + GameStateImpl gameState = new GameStateImpl( new GameTransitionImpl(25 ), new GameStateImpl() //terminal + ); + + new NormalizedBrain<>( getSimpleIdEvaluator(),2).findBestMove(gameState); + } + + @Test + public void singleMoveMustBeSelectableWhenNextIsTerminal() { + GameStateImpl gameState = new GameStateImpl( new GameTransitionImpl(0 ), new GameStateImpl() ); + + List bestMove = new NormalizedBrain( + (state, transition) -> transition.getId(), + 2 + ).findBestMove(gameState); + + assertEquals( 1, bestMove.size() ); + assertEquals( 0, bestMove.get(0).getId() ); + } + + @Test + public void movesLeadingToTerminalBetterToSelect() { + GameStateImpl gameState = new GameStateImpl( + new GameTransitionImpl(100 ), new GameStateImpl(), + new GameTransitionImpl(0 ), new GameStateImpl(), + new GameTransitionImpl(50 ), new GameStateImpl() + ); + + List bestMove = new NormalizedBrain<>( getSimpleIdEvaluator(),2 ).findBestMove(gameState); + + assertEquals( 1, bestMove.size() ); + assertEquals( 100, bestMove.get(0).getId() ); + } + + @Test( expected = IllegalArgumentException.class) + public void depthMore2NotSupported() { + new NormalizedBrain( (state, transition) -> transition.getId(), 3 ); + } + + @Test( expected = IllegalArgumentException.class) + public void depthLess1NotSupported() { + new NormalizedBrain( (state, transition) -> transition.getId(), 0 ); + } + + //if evaluator is not providing correct range for the move, we should throw an exception + //TODO: it's semantically questionable, the evaluator was passed in constructor and we'll throw this from the method + @Test( expected = IllegalArgumentException.class) + public void evaluatorWithWrongResultMustBeDetected() { + new NormalizedBrain( ( state, transition ) -> 1.1 ) + .findBestMove( new GameStateImpl( new GameTransitionImpl(1), new GameStateImpl() ) ); + } + + @Test + public void brainNameRespectsDepthOne() { + assertThat( new NormalizedBrain< GameStateImpl, GameTransitionImpl >( + ( state, transition ) -> 0.0, 1 ).name(), CoreMatchers.containsString( "1" )); + } + + @Test + public void brainNameRespectsDepthTwo() { + assertThat( new NormalizedBrain< GameStateImpl, GameTransitionImpl >( + ( state, transition ) -> 0.5, 2 ).name(), CoreMatchers.containsString( "2" )); + } +} \ No newline at end of file diff --git a/src/test/java/com/leokom/chess/player/legal/brain/simple/SimpleBrainTest.java b/src/test/java/com/leokom/chess/player/legal/brain/simple/SimpleBrainTest.java index d675d2ba5..ea44ee660 100644 --- a/src/test/java/com/leokom/chess/player/legal/brain/simple/SimpleBrainTest.java +++ b/src/test/java/com/leokom/chess/player/legal/brain/simple/SimpleBrainTest.java @@ -7,7 +7,6 @@ import org.junit.Before; import org.junit.Test; -import static com.leokom.chess.PlayerFactory.PlayerSelection.SIMPLE; import static org.mockito.Mockito.*; /** @@ -18,7 +17,7 @@ public class SimpleBrainTest { @Before public void prepare() { - simplePlayer = SIMPLE.create(); + simplePlayer = new SimplePlayerSupplier().get(); } @Test