Skip to content

Commit 416aedd

Browse files
authored
Merge pull request #4 from fathzer-games/branch-release-0.0.1
0.0.1 release
2 parents 0817bca + ac5862a commit 416aedd

File tree

119 files changed

+3110
-1002
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+3110
-1002
lines changed

.project

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,15 @@
2020
<nature>org.eclipse.jdt.core.javanature</nature>
2121
<nature>org.eclipse.m2e.core.maven2Nature</nature>
2222
</natures>
23+
<filteredResources>
24+
<filter>
25+
<id>1739988148319</id>
26+
<name></name>
27+
<type>30</type>
28+
<matcher>
29+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
30+
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
31+
</matcher>
32+
</filter>
33+
</filteredResources>
2334
</projectDescription>

README.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,24 @@ It provides you with a ready to use and highly configurable [iterative deepening
1111
In order to have a working engine for your favorite game, you have to implement your own MoveGenerator and Evaluator for this game ... and use one of the provided ai (I recommend IterativeDeepeningEngine).
1212
You can implement your own transposition table and its policy (which positions to save or how to reuse them during the search). This library contains a basic implementation, try it to see if it is enough for your needs.
1313

14-
# Known bugs
15-
- The AI always supposes a player that can't move loosed or make a draw ... which is not always the case (typically in [Reversi](https://en.wikipedia.org/wiki/Reversi)).
16-
- The iterative deepening search always stops deepening when it finds a winning move. It prevents finding other deeper winning moves. It's not a problem when playing a game, but for analysis, it is one.
14+
It also provides you with other useful building blocks like a clock, PerfT tests (for testing your move generator), or abstract implementations of move library (typically openings book for chess).
1715

18-
## TODO
19-
- Filter the library moves with candidates moves in IterativeDeepengingEngine.
20-
- The transposition table *newPosition* method should have the board as an argument in order to, for instance, compute a generation number based on board characteristics.
16+
# Known limitations
17+
- The AI always supposes that when a player can't move the game is ended ... which is not always the case (typically in [Reversi](https://en.wikipedia.org/wiki/Reversi)).
18+
This doesn't means that the AI can't be used to play Reversi. Simply, the reversi move generator should have a special '*can't play*' move returned when a player can't move but the game is not finished.
19+
- The Negamax is quite basic, it implements a highly configurable transposition and quiesce move search, but none other advanced algorithm (no PV search, futility pruning, killer or null move, etc ...).
20+
21+
## TODO (probable breaking changes)
22+
- com.fathzer.games.ai.AbstractAI requires an ExecutionContext in its constructor. I think this is not a good approach (even it was mine ;-)) to have this non standard implementation detail exposed outside this public class. Moreover, this disallow building different multi-threading models in AI implementation. Typically it is currently impossible to use a fork/join pool to perform the search.
23+
As it seems, in real life, a new context is created at each AI invocation, a better approach would be to let the AI manage its own threading scheme, and simply pass a SearchContext to the constructor.
24+
- Some methods in `com.fathzer.games.ai/iterativedeepening` implicitly suppose that the move generator implement `HashProvider`. This may be changed for an explicit requirement.
25+
26+
27+
## TODO (maybe)
28+
- Make MoveLibrary implement AI?
2129
- There's probably a bug or something very misleading in ClockSettings: withNext has a number of plies arguments, but it seems that the usage is to deal with number of moves (not plies which are half moves).
2230
- Maybe TTAi scoreToTT and ttToScore would be at a better place in TranspositionTablePolicy. Another, maybe better, approach is to compute fixed (mat) values in Negamax class and have a flag in the table (and in its store method) to explicitly set the stored value as a fixed value. It would allow those values to be used regardless of the depth at which they are recorded.
23-
- EvaluatedMove.compareTo does not sort in natural order which can be confusing
2431
- There's a strange behavior with transposition table. There's some situations where a never replace strategy leads to a faster resolution. It is totally counter intuitive.
2532
- MoveGenerator:
2633
- It would be more efficient to have the moveGenerator generating directly an iterator of moves instead of a list of moves. It would allow to use incremental move generators that starts by returning, what can hope to be the best moves (for instance captures), and other moves if these first moves are consumed. Indeed, in alpha/beta ai, the first moves are often enough to trigger a cut, so generating all the moves is a waste of time.
27-
- Write tests and Documentation ;-)
28-
- Test with Sonar
29-
- Publish artifact on Maven central
34+
- Write more tests ;-)

pom.xml

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<version>1.0.8</version>
1010
</parent>
1111
<artifactId>games-core</artifactId>
12-
<version>0.0.12-SNAPSHOT</version>
12+
<version>0.0.1</version>
1313

1414
<name>games-core</name>
1515
<description>A core library to help implement two players games.</description>
@@ -41,16 +41,22 @@
4141
<version>5.10.2</version>
4242
<scope>test</scope>
4343
</dependency>
44+
<dependency>
45+
<groupId>com.fathzer</groupId>
46+
<artifactId>jchess-perft-dataset</artifactId>
47+
<version>2.0.0</version>
48+
<scope>test</scope>
49+
</dependency>
4450
<dependency>
4551
<groupId>org.mockito</groupId>
4652
<artifactId>mockito-junit-jupiter</artifactId>
47-
<version>5.1.1</version>
53+
<version>5.16.0</version>
4854
<scope>test</scope>
4955
</dependency>
5056
<dependency>
5157
<groupId>com.github.bhlangonijr</groupId>
5258
<artifactId>chesslib</artifactId>
53-
<version>1.3.3</version>
59+
<version>1.3.4</version>
5460
<scope>test</scope>
5561
</dependency>
5662
<dependency>
@@ -60,4 +66,43 @@
6066
<scope>test</scope>
6167
</dependency>
6268
</dependencies>
69+
70+
<build>
71+
<plugins>
72+
<plugin>
73+
<groupId>org.jacoco</groupId>
74+
<artifactId>jacoco-maven-plugin</artifactId>
75+
<version>0.8.12</version>
76+
<configuration>
77+
<excludes>
78+
<!-- Exclude the PhysicalCores class from code coverage because it is too tiedly coupled with the runtime environment -->
79+
<exclude>**/PhysicalCores.class</exclude>
80+
<exclude>**/experimental/*</exclude>
81+
</excludes>
82+
</configuration>
83+
</plugin>
84+
<plugin>
85+
<groupId>org.apache.maven.plugins</groupId>
86+
<artifactId>maven-javadoc-plugin</artifactId>
87+
<version>3.6.3</version>
88+
<configuration>
89+
<source>8</source>
90+
<docencoding>UTF-8</docencoding>
91+
<overview>${basedir}/overview.html</overview>
92+
<header>${project.version}</header>
93+
<bottom>${project.version}</bottom>
94+
<excludePackageNames>:*.experimental</excludePackageNames>
95+
</configuration>
96+
<executions>
97+
<execution>
98+
<id>javadoc_generation</id>
99+
<phase>package</phase>
100+
<goals>
101+
<goal>jar</goal>
102+
</goals>
103+
</execution>
104+
</executions>
105+
</plugin>
106+
</plugins>
107+
</build>
63108
</project>

src/main/java/com/fathzer/games/Color.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
* <br>Ok, ok, so let say "white" means "X" and "black" means "O". The important thing is to identify both players, isn't it?.
77
*/
88
public enum Color {
9-
WHITE, BLACK;
9+
/** White player */
10+
WHITE,
11+
/** Black player */
12+
BLACK;
1013

1114
static {
1215
WHITE.opposite = BLACK;

src/main/java/com/fathzer/games/GameHistory.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public enum TerminationCause {
3737
private Status extraStatus;
3838
private TerminationCause terminationCause;
3939

40+
/** Creates a new game history.
41+
* @param board The board of the game at its start
42+
*/
4043
@SuppressWarnings("unchecked")
4144
public GameHistory(B board) {
4245
this.startBoard = (B) board.fork();
@@ -66,14 +69,24 @@ public boolean add(M move) {
6669
return result;
6770
}
6871

72+
/** Gets the start board.
73+
* @return a board instance.
74+
*/
6975
public B getStartBoard() {
7076
return startBoard;
7177
}
7278

79+
/** Gets the current board.
80+
* @return board instance.
81+
*/
7382
public B getBoard() {
7483
return board;
7584
}
7685

86+
/** Gets the list of moves added to this history using the {@link #add(Move)} method.
87+
* @return a list of moves
88+
* @see #add(Move)
89+
*/
7790
public List<M> getMoves() {
7891
return moves;
7992
}
@@ -97,17 +110,30 @@ public void earlyEnd(Status status, TerminationCause terminationCause) {
97110
this.terminationCause = terminationCause;
98111
}
99112

113+
/** Gets the current game status.
114+
* @return A status. The one set with the {@link #earlyEnd(Status, TerminationCause)} method or the one returned by the {@link #getBoardStatus(B)} method.
115+
*/
100116
public Status getStatus() {
101117
if (extraStatus!=null) {
102118
return extraStatus;
103119
}
104120
return getBoardStatus(board);
105121
}
106122

123+
/** Gets the current termination cause.
124+
* @return a non null termination cause
125+
* @see #earlyEnd(Status, TerminationCause)
126+
*/
107127
public TerminationCause getTerminationCause() {
108128
return this.terminationCause;
109129
}
110130

131+
/** Gets the current game status, excluding status set by {@link #earlyEnd(Status, TerminationCause)} method.
132+
* <br>The default implementation returns the status returned by {@link MoveGenerator#getContextualStatus()} if it is not <code>PLAYING</code>.
133+
* If its is <code>PLAYING</code> but there are no legal moves, then the result of {@link MoveGenerator#getEndGameStatus()} is returned.
134+
* @param board The board.
135+
* @return A status.
136+
*/
111137
protected Status getBoardStatus(B board) {
112138
Status status = board.getContextualStatus();
113139
if (status==Status.PLAYING && board.getLegalMoves().isEmpty()) {

src/main/java/com/fathzer/games/MoveGenerator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.fathzer.games;
22

33
import java.util.List;
4-
import java.util.stream.Collectors;
54

65
import com.fathzer.games.util.exec.Forkable;
76

@@ -70,7 +69,7 @@ default List<M> getLegalMoves() {
7069
unmakeMove();
7170
}
7271
return ok;
73-
}).collect(Collectors.toList());
72+
}).toList();
7473
}
7574

7675
/** This method is called before evaluating a position or looking for a previous evaluation in a transposition table.

src/main/java/com/fathzer/games/Status.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
/** The status of a game (playing, draw, white or black won).
44
*/
55
public enum Status {
6-
PLAYING, DRAW, WHITE_WON, BLACK_WON;
6+
/** The game is still playing */
7+
PLAYING,
8+
/** The game ends with a draw */
9+
DRAW,
10+
/** The white player has won */
11+
WHITE_WON,
12+
/** The black player has won */
13+
BLACK_WON;
714

815
/** Gets the winner's color.
916
* @return A color or null if there's no winner

src/main/java/com/fathzer/games/ai/AI.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,25 @@
44

55
/** An AI able to find the best move(s) during a game.
66
* @param <M> Implementation of the Move interface to use
7+
* @param <P> Implementation of the SearchParameters interface to use
78
*/
8-
public interface AI<M> {
9+
public interface AI<M, P extends SearchParameters> {
910

1011
/**
11-
* Gets best moves evaluations at the given search depth
12+
* Gets best moves evaluations with the given search parameters
1213
* <br>This method works on all possible moves for the position. If you want to work on reduced move set, you can use {@link #getBestMoves(List, SearchParameters)} methods
1314
* @param parameters The search parameters
1415
* @return The search result
1516
*/
16-
SearchResult<M> getBestMoves(SearchParameters parameters);
17+
SearchResult<M> getBestMoves(P parameters);
1718

1819
/**
19-
* Gets best moves evaluations at the given search depth
20+
* Gets best moves evaluations at the given search parameters
2021
* <br>This methods evaluates provided moves in the list order. In order to maximize cutoff in some algorithm (like {@link Negamax}),
2122
* you should order the list in from what is estimated to be the best move to the worst one.
2223
* @param possibleMoves A list of moves to evaluate. If one of these moves is impossible, result is not specified (It may crash or return a wrong result, etc...).
2324
* @param parameters The search parameters
2425
* @return The search result.
2526
*/
26-
SearchResult<M> getBestMoves(List<M> possibleMoves, SearchParameters parameters);
27-
28-
public void interrupt();
29-
30-
public boolean isInterrupted();
31-
32-
/** Gets the statistic related to last search call.
33-
* @return The statistics
34-
*/
35-
SearchStatistics getStatistics();
27+
SearchResult<M> getBestMoves(List<M> possibleMoves, P parameters);
3628
}

src/main/java/com/fathzer/games/ai/AbstractAI.java

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@
88
import com.fathzer.games.Status;
99
import com.fathzer.games.ai.evaluation.Evaluator;
1010
import com.fathzer.games.util.exec.ExecutionContext;
11+
import com.fathzer.games.util.exec.Interruptible;
1112

12-
public abstract class AbstractAI<M, B extends MoveGenerator<M>> implements AI<M> {
13+
/** An abstract {@link DepthFirstAI} implementation.
14+
* @param <M> Implementation of the Move interface to use
15+
* @param <B> Implementation of the MoveGenerator interface to use
16+
*/
17+
public abstract class AbstractAI<M, B extends MoveGenerator<M>> implements DepthFirstAI<M, DepthFirstSearchParameters>, Interruptible {
1318
private final ExecutionContext<SearchContext<M,B>> context;
1419
private boolean interrupted;
1520

21+
/** Constructor
22+
* @param context The context to use for the search
23+
*/
1624
protected AbstractAI(ExecutionContext<SearchContext<M,B>> context) {
1725
this.context = context;
1826
this.interrupted = false;
@@ -23,23 +31,34 @@ public SearchStatistics getStatistics() {
2331
return context.getContext().getStatistics();
2432
}
2533

34+
/**
35+
* Gets the context used for the search.
36+
* @return the context
37+
*/
2638
public SearchContext<M, B> getContext() {
2739
return context.getContext();
2840
}
2941

3042
@Override
31-
public SearchResult<M> getBestMoves(SearchParameters params) {
43+
public SearchResult<M> getBestMoves(DepthFirstSearchParameters params) {
3244
getContext().getStatistics().clear();
3345
List<M> moves = getContext().getGamePosition().getMoves();
3446
getStatistics().movesGenerated(moves.size());
3547
return this.getBestMoves(moves, params);
3648
}
3749

3850
@Override
39-
public SearchResult<M> getBestMoves(List<M> moves, SearchParameters params) {
40-
return getBestMoves(moves, params, (m,lowestInterestingScore)->rootEvaluation(m,params.getDepth(),lowestInterestingScore));
51+
public SearchResult<M> getBestMoves(List<M> moves, DepthFirstSearchParameters params) {
52+
return getBestMoves(moves, params, (m,lowestInterestingScore)->rootEvaluation(m,params.getDepth(), lowestInterestingScore));
4153
}
4254

55+
/**
56+
* Evaluates a root move of the search tree.
57+
* @param move The move to evaluate
58+
* @param depth The depth of the search
59+
* @param lowestInterestingScore The lowest interesting score under which the evaluation is not interesting (typically this can be used to cut the tree when this evaluation can't be reached)
60+
* @return The score of the move (the score is computed by the {@link #getRootScore(int, int)} method), or null if the move is not valid
61+
*/
4362
protected Integer rootEvaluation(M move, final int depth, int lowestInterestingScore) {
4463
if (lowestInterestingScore==Integer.MIN_VALUE) {
4564
// WARNING: -Integer.MIN_VALUE is equals to ... Integer.MIN_VALUE
@@ -56,15 +75,29 @@ protected Integer rootEvaluation(M move, final int depth, int lowestInterestingS
5675
}
5776
}
5877

78+
/**
79+
* Gets the score of a root move.
80+
* @param depth The depth of the search
81+
* @param lowestInterestingScore The lowest interesting score under which the evaluation is not interesting (typically this can be used to cut the tree when this evaluation can't be reached)
82+
* @return The score of the move
83+
*/
5984
protected abstract int getRootScore(final int depth, int lowestInterestingScore);
6085

61-
protected SearchResult<M> getBestMoves(List<M> moves, SearchParameters params, BiFunction<M,Integer, Integer> rootEvaluator) {
62-
final SearchResult<M> search = new SearchResult<>(params.getSize(), params.getAccuracy());
63-
context.execute(moves.stream().map(m -> getRootEvaluationTask(params, rootEvaluator, search, m)).toList());
86+
/**
87+
* Performs a search on a list of moves.
88+
* <br>It is called by the {@link #getBestMoves(List, DepthFirstSearchParameters)} method and uses the execution context to process the moves (see {@link ExecutionContext#execute(Collection)}).
89+
* @param moves The moves to evaluate
90+
* @param params The parameters of the search
91+
* @param rootEvaluator A function that evaluates the root moves
92+
* @return The search result
93+
*/
94+
protected SearchResult<M> getBestMoves(List<M> moves, DepthFirstSearchParameters params, BiFunction<M,Integer, Integer> rootEvaluator) {
95+
final SearchResult<M> search = new SearchResult<>(params);
96+
context.execute(moves.stream().map(m -> getRootEvaluationTask(rootEvaluator, search, m)).toList());
6497
return search;
6598
}
6699

67-
private Runnable getRootEvaluationTask(SearchParameters params, BiFunction<M, Integer, Integer> rootEvaluator, final SearchResult<M> search, M m) {
100+
private Runnable getRootEvaluationTask(BiFunction<M, Integer, Integer> rootEvaluator, final SearchResult<M> search, M m) {
68101
return () -> {
69102
final Integer score = rootEvaluator.apply(m, search.getLow());
70103
if (!isInterrupted() && score!=null) {
@@ -84,11 +117,18 @@ public void interrupt() {
84117
interrupted = true;
85118
}
86119

120+
/**
121+
* Gets the score when the game ended during the search (for instance, for chess, when the last move played during the search is a mate).
122+
* @param evaluator The evaluator used (returned by {@link SearchContext#getEvaluator()})
123+
* @param status The status of the game
124+
* @param depth The current search depth
125+
* @param maxDepth The maximum depth of the search
126+
* @return The score. The default implementation returns 0 for a draw, otherwise it considers the player loose and returns -{@link Evaluator#getWinScore(int)}.
127+
*/
87128
protected int getScore(final Evaluator<M,B> evaluator, final Status status, final int depth, int maxDepth) {
88129
if (Status.DRAW==status) {
89130
return 0;
90131
} else {
91-
//FIXME Maybe there's some games where the player wins if it can't move...
92132
return -evaluator.getWinScore(maxDepth-depth);
93133
}
94134
}

0 commit comments

Comments
 (0)