Skip to content

Commit 0817bca

Browse files
authored
Merge pull request #3 from fathzer-games/quiesce
First preview
2 parents e360354 + a21b944 commit 0817bca

File tree

117 files changed

+8270
-1
lines changed

Some content is hidden

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

117 files changed

+8270
-1
lines changed

.github/workflows/build.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Build
2+
on:
3+
push:
4+
branches:
5+
- "**"
6+
pull_request:
7+
types: [opened, synchronize, reopened]
8+
jobs:
9+
build:
10+
name: Build
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
16+
- name: Set up JDK 17
17+
uses: actions/setup-java@v4
18+
with:
19+
java-version: 17
20+
distribution: 'zulu' # Alternative distribution options are available.
21+
- name: Cache SonarCloud packages
22+
uses: actions/cache@v4
23+
with:
24+
path: ~/.sonar/cache
25+
key: ${{ runner.os }}-sonar
26+
restore-keys: ${{ runner.os }}-sonar
27+
- name: Cache Maven packages
28+
uses: actions/cache@v4
29+
with:
30+
path: ~/.m2
31+
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
32+
restore-keys: ${{ runner.os }}-m2
33+
- name: Build and analyze
34+
env:
35+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
36+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
37+
run: mvn -B org.jacoco:jacoco-maven-plugin:prepare-agent test org.jacoco:jacoco-maven-plugin:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=fathzer-games_games-core

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target/
2+
/.settings/
3+
.classpath

.project

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>games-core</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
<buildCommand>
14+
<name>org.eclipse.m2e.core.maven2Builder</name>
15+
<arguments>
16+
</arguments>
17+
</buildCommand>
18+
</buildSpec>
19+
<natures>
20+
<nature>org.eclipse.jdt.core.javanature</nature>
21+
<nature>org.eclipse.m2e.core.maven2Nature</nature>
22+
</natures>
23+
</projectDescription>

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,29 @@
1+
[![Maven Central](https://img.shields.io/maven-central/v/com.fathzer/games-core)](https://central.sonatype.com/artifact/com.fathzer/games-core)
2+
[![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/fathzer-games/games-core/blob/master/LICENSE)
3+
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=fathzer_games-core&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=fathzer-games_games-core)
4+
[![javadoc](https://javadoc.io/badge2/com.fathzer/games-core/javadoc.svg)](https://javadoc.io/doc/com.fathzer/games-core)
5+
16
# games-core
2-
A core library to help implement two players games
7+
A core library to help implement two players game engines.
8+
9+
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).
10+
11+
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).
12+
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.
13+
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.
17+
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.
21+
- 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).
22+
- 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
24+
- 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.
25+
- 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 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

pom.xml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE xml><project xmlns="https://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.fathzer</groupId>
8+
<artifactId>parent-pom</artifactId>
9+
<version>1.0.8</version>
10+
</parent>
11+
<artifactId>games-core</artifactId>
12+
<version>0.0.12-SNAPSHOT</version>
13+
14+
<name>games-core</name>
15+
<description>A core library to help implement two players games.</description>
16+
<url>https://github.com/fathzer-games/games-core</url>
17+
18+
<scm>
19+
<url>https://github.com/fathzer-games/games-core.git</url>
20+
<connection>https://github.com/fathzer-games/games-core.git</connection>
21+
</scm>
22+
23+
<properties>
24+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
25+
<sonar.organization>fathzer-games</sonar.organization>
26+
<check-dependencies-java-version>17</check-dependencies-java-version>
27+
<maven.compiler.release>17</maven.compiler.release>
28+
</properties>
29+
30+
<repositories>
31+
<repository>
32+
<id>jitpack.io</id>
33+
<url>https://jitpack.io</url>
34+
</repository>
35+
</repositories>
36+
37+
<dependencies>
38+
<dependency>
39+
<groupId>org.junit.jupiter</groupId>
40+
<artifactId>junit-jupiter</artifactId>
41+
<version>5.10.2</version>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.mockito</groupId>
46+
<artifactId>mockito-junit-jupiter</artifactId>
47+
<version>5.1.1</version>
48+
<scope>test</scope>
49+
</dependency>
50+
<dependency>
51+
<groupId>com.github.bhlangonijr</groupId>
52+
<artifactId>chesslib</artifactId>
53+
<version>1.3.3</version>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.awaitility</groupId>
58+
<artifactId>awaitility</artifactId>
59+
<version>4.2.2</version>
60+
<scope>test</scope>
61+
</dependency>
62+
</dependencies>
63+
</project>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.fathzer.games;
2+
3+
/** A player's color.
4+
* <br>Ok, there's a lot of games with two players and no "white" and "black".
5+
* <br>For instance, the <a href="https://en.wikipedia.org/wiki/Tic-tac-toe">Tic Tac Toe</a>, has "X" and "O" players.
6+
* <br>Ok, ok, so let say "white" means "X" and "black" means "O". The important thing is to identify both players, isn't it?.
7+
*/
8+
public enum Color {
9+
WHITE, BLACK;
10+
11+
static {
12+
WHITE.opposite = BLACK;
13+
BLACK.opposite = WHITE;
14+
}
15+
16+
private Color opposite;
17+
18+
/** Gets the opposite color of this color.
19+
* @return a color.
20+
*/
21+
public Color opposite() {
22+
return opposite;
23+
}
24+
}
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+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.fathzer.games;
2+
3+
/** A class able to compute a hash key (for instance a <a href="https://en.wikipedia.org/wiki/Zobrist_hashing">Zobrist key</a>) for the current game state.
4+
* <br>Typically, a chess {@link MoveGenerator} which supports Zobrist hashing will implement this interface.
5+
*/
6+
@FunctionalInterface
7+
public interface HashProvider {
8+
/** Gets the hash key of the current game state.
9+
* @return a long
10+
*/
11+
long getHashKey();
12+
}

0 commit comments

Comments
 (0)