Skip to content

Commit a21b944

Browse files
authored
Merge pull request #2 from fathzer-games/game-classes
Game classes
2 parents bc90723 + 34b2f7f commit a21b944

File tree

12 files changed

+556
-13
lines changed

12 files changed

+556
-13
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![javadoc](https://javadoc.io/badge2/com.fathzer/games-core/javadoc.svg)](https://javadoc.io/doc/com.fathzer/games-core)
55

66
# games-core
7-
A core library to help implement two players game engines
7+
A core library to help implement two players game engines.
88

99
It provides you with a ready to use and highly configurable [iterative deepening](https://www.chessprogramming.org/Iterative_Deepening) [Negamax](https://en.wikipedia.org/wiki/Negamax) implementation using configurable [transposition table](https://en.wikipedia.org/wiki/Transposition_table).
1010

@@ -17,13 +17,13 @@ You can implement your own transposition table and its policy (which positions t
1717

1818
## TODO
1919
- Filter the library moves with candidates moves in IterativeDeepengingEngine.
20-
- The transposition table *newPosition* method should have the board has an argument in order to, for instance, compute a generation number based on board characteristics.
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.
2121
- 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).
2222
- 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.
2323
- EvaluatedMove.compareTo does not sort in natural order which can be confusing
2424
- 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.
2525
- MoveGenerator:
26-
- 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 start 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.
26+
- 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.
2727
- Write tests and Documentation ;-)
2828
- Test with Sonar
2929
- Publish artifact on Maven central

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,11 @@
5353
<version>1.3.3</version>
5454
<scope>test</scope>
5555
</dependency>
56+
<dependency>
57+
<groupId>org.awaitility</groupId>
58+
<artifactId>awaitility</artifactId>
59+
<version>4.2.2</version>
60+
<scope>test</scope>
61+
</dependency>
5662
</dependencies>
5763
</project>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.fathzer.games;
2+
3+
import java.util.LinkedList;
4+
import java.util.List;
5+
6+
import com.fathzer.games.MoveGenerator.MoveConfidence;
7+
8+
/** A game history with its start board, the list of its moves and its termination cause.
9+
* @param <M> The type of the moves
10+
* @param <B> The type of the {@link MoveGenerator} used to represent the board
11+
*/
12+
public class GameHistory<M,B extends MoveGenerator<M>> {
13+
/** The termination cause of a game.
14+
*/
15+
public enum TerminationCause {
16+
/** A player resigns */
17+
ABANDONED,
18+
/** Result determined by third-party adjudication */
19+
ADJUDICATION,
20+
/** A player died (hope he was a computer!) */
21+
DEATH,
22+
/** The game was ended because of an emergency (fire, etc...) */
23+
EMERGENCY,
24+
/** The game ended simply because a player won */
25+
NORMAL,
26+
/** A player was disqualified */
27+
RULES_INFRACTION,
28+
/** A player ran out of time */
29+
TIME_FORFEIT,
30+
/** The game is not terminated */
31+
UNTERMINATED;
32+
}
33+
34+
private final B startBoard;
35+
private final B board;
36+
private final List<M> moves;
37+
private Status extraStatus;
38+
private TerminationCause terminationCause;
39+
40+
@SuppressWarnings("unchecked")
41+
public GameHistory(B board) {
42+
this.startBoard = (B) board.fork();
43+
this.board = (B) board.fork();
44+
this.moves = new LinkedList<>();
45+
this.terminationCause = TerminationCause.UNTERMINATED;
46+
}
47+
48+
/** Adds a move to this history.
49+
* @param move The move to add.
50+
* @return true if the move is legal. False if it is not. In such a case the move is also added to the list of moves.
51+
* <br>Please that if the move is illegal, the player is not automatically disqualified. The developer is free to implement
52+
* the punishment he wants.
53+
* <br>If the move is legal and changes the {@link Status} of the game to anything except {@link Status#PLAYING}, the termination
54+
* cause is set to NORMAL
55+
* @throws IllegalStateException if game is already ended.
56+
*/
57+
public boolean add(M move) {
58+
if (Status.PLAYING!=getStatus()) {
59+
throw new IllegalStateException();
60+
}
61+
this.moves.add(move);
62+
final boolean result = board.makeMove(move, MoveConfidence.UNSAFE);
63+
if (result && Status.PLAYING!=getStatus()) {
64+
this.terminationCause = TerminationCause.NORMAL;
65+
}
66+
return result;
67+
}
68+
69+
public B getStartBoard() {
70+
return startBoard;
71+
}
72+
73+
public B getBoard() {
74+
return board;
75+
}
76+
77+
public List<M> getMoves() {
78+
return moves;
79+
}
80+
81+
/** Declares an early termination for the game.
82+
* <br>For instance, a player resigns or players agree to a draw.
83+
* <br>Any subsequent call to this method or {@link #add(Move)} will result in an IllegalStateException
84+
* @param status The end status
85+
* @param terminationCause The termination cause
86+
* @throws IllegalStateException if game is already ended.
87+
* @throws IllegalArgumentException if termination is null or if status is {@link Status#PLAYING} or null.
88+
*/
89+
public void earlyEnd(Status status, TerminationCause terminationCause) {
90+
if (Status.PLAYING==status || status==null || terminationCause==null || terminationCause==TerminationCause.UNTERMINATED) {
91+
throw new IllegalArgumentException();
92+
}
93+
if (Status.PLAYING!=getStatus()) {
94+
throw new IllegalStateException("Status is "+getStatus());
95+
}
96+
this.extraStatus = status;
97+
this.terminationCause = terminationCause;
98+
}
99+
100+
public Status getStatus() {
101+
if (extraStatus!=null) {
102+
return extraStatus;
103+
}
104+
return getBoardStatus(board);
105+
}
106+
107+
public TerminationCause getTerminationCause() {
108+
return this.terminationCause;
109+
}
110+
111+
protected Status getBoardStatus(B board) {
112+
Status status = board.getContextualStatus();
113+
if (status==Status.PLAYING && board.getLegalMoves().isEmpty()) {
114+
status = board.getEndGameStatus();
115+
}
116+
return status;
117+
}
118+
}

src/main/java/com/fathzer/games/ai/evaluation/QuiesceEvaluator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@
33
import com.fathzer.games.MoveGenerator;
44
import com.fathzer.games.ai.SearchContext;
55

6+
/** An evaluator that performs quiescence search before evaluating.
7+
* @param <M> The type of moves
8+
* @param <B> The type of the evaluator
9+
*/
610
public interface QuiesceEvaluator<M, B extends MoveGenerator<M>> {
11+
/** Gets the position evaluation after performing a quiescence search.
12+
* @param ctx The search context (The position can be found using {@link SearchContext#getGamePosition()}
13+
* @param depth The current depth
14+
* @param alpha The current alpha value
15+
* @param beta The current beta
16+
* @return The evaluation
17+
*/
718
int evaluate(SearchContext<M, B> ctx, int depth, int alpha, int beta);
819
}

src/main/java/com/fathzer/games/ai/iterativedeepening/DeepeningPolicy.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,28 @@ public class DeepeningPolicy extends SearchParameters {
2121
private long start;
2222
private boolean deepenOnForced;
2323

24+
/** Constructor.
25+
* <br>By default there's no time limit to deepening and forced moves are not evaluated.
26+
* @param maxDepth The maximum search depth.
27+
*/
2428
public DeepeningPolicy(int maxDepth) {
2529
super(maxDepth);
2630
this.maxTime = Long.MAX_VALUE;
2731
this.start = -1;
2832
this.deepenOnForced = false;
2933
}
3034

35+
/** Start counting time.
36+
* <br>This method should be called when the search starts in order to have {@link #getSpent()} and {@link #isDeepenOnForced()} works
37+
*/
3138
public void start() {
3239
this.start = System.currentTimeMillis();
3340
}
3441

42+
/** Gets the number of milliseconds elapsed since the call to {@link #start}
43+
* @return a positive long.
44+
* @throws IllegalStateException if {@link #start()} was not called
45+
*/
3546
public long getSpent() {
3647
if (start<0) {
3748
throw new IllegalStateException("Not yet started");
@@ -45,6 +56,7 @@ public long getSpent() {
4556
*/
4657
public void setMaxTime(long maxTime) {
4758
this.maxTime = maxTime;
59+
this.start = -1;
4860
}
4961

5062
/** Gets the maximum time to spend in the search.
@@ -54,13 +66,17 @@ public long getMaxTime() {
5466
return maxTime;
5567
}
5668

69+
/** Check whether the search should be deepened on forced moves.
70+
* @return true it should be deepened.
71+
* @see #setDeepenOnForced(boolean)
72+
*/
5773
public boolean isDeepenOnForced() {
5874
return deepenOnForced;
5975
}
6076

6177
/** Sets if the search should be deepened when only one move remains to evaluate.
62-
* <br>Please note, that if this attribute to false (The default value), searching for two best moves (size=2), and
63-
* you having n possible moves and only one that isn't yet evaluate to win or loose, will stop the evaluation
78+
* <br>Please note, that if this attribute is false (The default value), searching for two best moves (size=2), and
79+
* having n possible moves but only one not yet evaluated to win or loose, will stop the evaluation
6480
* @param deepenOnForced true to force the deepening until max depth or maxtime is reached.
6581
*/
6682
public void setDeepenOnForced(boolean deepenOnForced) {

src/main/java/com/fathzer/games/clock/Clock.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package com.fathzer.games.clock;
22

33
import java.util.concurrent.ConcurrentLinkedQueue;
4-
import java.util.concurrent.Executors;
54
import java.util.concurrent.ScheduledExecutorService;
65
import java.util.concurrent.ScheduledFuture;
76
import java.util.concurrent.ScheduledThreadPoolExecutor;
8-
import java.util.concurrent.ThreadFactory;
97
import java.util.concurrent.TimeUnit;
108
import java.util.function.Consumer;
119

1210
import com.fathzer.games.Color;
1311
import com.fathzer.games.Status;
12+
import com.fathzer.games.util.exec.CustomThreadFactory;
13+
1414
import static com.fathzer.games.clock.ClockState.*;
1515

1616
/** A clock.
@@ -21,12 +21,7 @@
2121
* <br>This class is thread safe.
2222
*/
2323
public class Clock {
24-
private static final ThreadFactory FACTORY = r -> {
25-
Thread t = Executors.defaultThreadFactory().newThread(r);
26-
t.setDaemon(true);
27-
return t;
28-
};
29-
private static final ScheduledExecutorService TIMER = new ScheduledThreadPoolExecutor(1, FACTORY);
24+
private static final ScheduledExecutorService TIMER = new ScheduledThreadPoolExecutor(1, new CustomThreadFactory(new CustomThreadFactory.BasicThreadNameSupplier("Clock"), true));
3025

3126
static {
3227
((ScheduledThreadPoolExecutor)TIMER).setRemoveOnCancelPolicy(true);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.fathzer.games.util.exec;
2+
3+
import java.util.concurrent.Executors;
4+
import java.util.concurrent.ThreadFactory;
5+
import java.util.concurrent.atomic.AtomicLong;
6+
import java.util.function.Supplier;
7+
8+
public class CustomThreadFactory implements ThreadFactory {
9+
10+
public static class BasicThreadNameSupplier implements Supplier<String>{
11+
private final AtomicLong count;
12+
private final String prefix;
13+
14+
public BasicThreadNameSupplier(String prefix) {
15+
super();
16+
this.prefix = prefix;
17+
this.count = new AtomicLong();
18+
}
19+
20+
@Override
21+
public String get() {
22+
return prefix + " " + count.incrementAndGet();
23+
}
24+
}
25+
26+
private final Supplier<String> threadNameSupplier;
27+
private final boolean isDaemon;
28+
29+
public CustomThreadFactory(Supplier<String> threadNameSupplier, boolean isDaemon) {
30+
this.threadNameSupplier = threadNameSupplier;
31+
this.isDaemon = isDaemon;
32+
}
33+
34+
@Override
35+
public Thread newThread(Runnable runnable) {
36+
final Thread thread = Executors.defaultThreadFactory().newThread(runnable);
37+
thread.setName(threadNameSupplier.get());
38+
if (isDaemon) {
39+
thread.setDaemon(true);
40+
}
41+
return thread;
42+
}
43+
44+
}

0 commit comments

Comments
 (0)