Skip to content

Commit

Permalink
Adds a command line to make tournaments between two engines
Browse files Browse the repository at this point in the history
  • Loading branch information
fathzer committed Nov 4, 2024
1 parent ef3893b commit 1b2d50b
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 42 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ If you specify a value for the `gameCount` system property while starting the ap
- When a engine that is used in player settings hangs, it leaves settings in a wrong state with no possibility to fix it; The application should be restarted.

# TODO
- PGN should contain time settings
- PGN should contain time settings and a [Termination tag](https://github.com/mliebelt/pgn-spec-commented/blob/main/pgn-specification.md#981-tag-termination).
- Implement a way to play again on missclick.
- Implement PGN game loading
- Implement move backward/forward in the game.
Expand Down
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
</properties>

<dependencies>
<dependency>
<groupId>com.fathzer</groupId>
<artifactId>games-core</artifactId>
<version>0.0.12-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.fathzer</groupId>
<artifactId>jchess-core</artifactId>
Expand All @@ -33,7 +38,7 @@
<dependency>
<groupId>com.fathzer</groupId>
<artifactId>jchess-uci-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.fathzer</groupId>
Expand Down
40 changes: 25 additions & 15 deletions src/main/java/com/fathzer/jchess/AbstractGameSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public Score getScore() {
return score;
}

public Settings getSettings() {
return settings;
}

protected AbstractGameSession(T gui, Settings settings) {
this.gui = gui;
this.state = new Observable<>(State.CREATED);
Expand Down Expand Up @@ -67,7 +71,7 @@ private void doRevenge() {
blackEngine = dummy;
onPlayerColorsChanged();
}
initGame();
newGame();
setState(State.PAUSED);
start();
}
Expand All @@ -76,7 +80,7 @@ protected void onPlayerColorsChanged() {
// Allows subclasses to perform extra initialization when players color changes
}

protected void initGame() {
protected void newGame() {
this.game = new Game(settings.getVariant().getRules().apply(settings.getFen()), buildClock());
this.game.setStartClockAfterFirstMove(settings.isStartClockAfterFirstMove());
score.newGame();
Expand Down Expand Up @@ -141,11 +145,14 @@ protected Engine getEngine(Color color) {
public void start() {
if (State.ENDED.equals(getState())) {
this.score.reset();
initGame();
newGame();
}
setEngine(player1Color, getEngine(settings.getPlayer1().getEngine()));
setEngine(player1Color.opposite(), getEngine(settings.getPlayer2().getEngine()));
setState(State.RUNNING);
//TODO With the nextMove here, TournamentGameSession goes (sometime) crazy !!!
//TODO The alternative is having the subclass trap the state change to call nextMove itself ... wich should do exactly the same !!!
nextMove();
}

protected abstract void nextMove();
Expand All @@ -160,15 +167,15 @@ private void setState(State state) {
this.state.setValue(state);
}

protected abstract void onMove(Move move);
private void timeUp(Status status) {
doTimeUp(status);
}

/** This method is called when clock emits a time up event.
* <br>Please note that this method could be invoked on a thread that is not the Swing event thread.
* <br>Please note that this method could be invoked on a thread that is not the Swing event thread.
* <br>One can override this method to ensure the correct thread is used.
* @param status The game status.
*/
protected void timeUp(Status status) {
}

protected void doTimeUp(Status status) {
if (getTournamentGamesCount()==0) {
// If we are not in tournament mode, propose to continue without clock
Expand Down Expand Up @@ -209,25 +216,28 @@ public int getTournamentGamesCount() {
protected void endOfGame(final Status status) {
setState(State.PAUSED);
log.debug("End of game, state: {}", getState());
try {
GameRecorder.commit(this.settings, this.player1Color, this.game.getHistory());
} catch (Exception e) {
log.error("An error occured while writing pgn",e);
}
updateScores(status);
onGameEnded();
if (isMakeRevenge(status)) {
doRevenge();
} else {
setState(State.ENDED);
}
}

/** This method is called when game just finished.
* <br>It does nothing by default but allows subclasses to perform some specific actions (for instance, output a summary of the game in a file).
*/
protected void onGameEnded() {
// Does nothing by default
}

protected boolean isMakeRevenge(final Status status) {
final int toPlay = getTournamentGamesCount();
return toPlay!=0 && score.getGameCount()<toPlay;
}

protected void updateScores(Status status) {
private void updateScores(Status status) {
if (Status.DRAW.equals(status)) {
score.draw();
} else {
Expand All @@ -246,6 +256,6 @@ public void setSettings(Settings settings) {
this.settings = settings;
player1Color = settings.getPlayer1Color().getColor();
score.reset();
initGame();
newGame();
}
}
27 changes: 17 additions & 10 deletions src/main/java/com/fathzer/jchess/GameRecorder.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.time.LocalDate;

import com.fathzer.games.Color;
import com.fathzer.games.clock.PGNTimeControlTagParser;
import com.fathzer.jchess.pgn.PGNHeaders;
import com.fathzer.jchess.pgn.PGNHeaders.Builder;
import com.fathzer.jchess.settings.Settings;
Expand All @@ -19,20 +20,26 @@

@UtilityClass
public class GameRecorder {
public static void commit(Settings settings, Color player1Color, GameHistory history) throws IOException {
public static void print(GameHistory history, Settings settings, Color player1Color, Long round) throws IOException {
try (PrintWriter out=out()) {
final Builder builder = new PGNHeaders.Builder();
builder.setWhiteName(who(settings, player1Color, Color.WHITE));
builder.setBlackName(who(settings, player1Color, Color.BLACK));
if (Variant.STANDARD!=settings.getVariant()) {
builder.setVariant(settings.getVariant().name());
}
// Add white and black names
new PGNWriter().getPGN(builder.build(), history).forEach(out::println);
out.flush();
print(history, settings, player1Color, round, out);
}
}

public static void print(GameHistory history, Settings settings, Color player1Color, Long round, PrintWriter out) {
final Builder builder = new PGNHeaders.Builder();
builder.setWhiteName(who(settings, player1Color, Color.WHITE));
builder.setBlackName(who(settings, player1Color, Color.BLACK));
builder.setRound(round);
builder.setTimeControl(new PGNTimeControlTagParser().toTag(settings.getClock().toClockSettings()));
if (Variant.STANDARD!=settings.getVariant()) {
builder.setVariant(settings.getVariant().name());
}
// Add white and black names
new PGNWriter().getPGN(builder.build(), history).forEach(out::println);
out.flush();
}

private static PrintWriter out() throws IOException {
final Path file = Path.of("./data/pgn", PGNWriter.DATE_FORMAT.format(LocalDate.now())+".pgn");
Files.createDirectories(file.getParent());
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/fathzer/jchess/settings/BasicClockSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,14 @@ public ClockSettings toClockSettings() {
}
return result;
}

public static BasicClockSettings fromClockSettings(ClockSettings cs) {
final BasicClockSettings settings = new BasicClockSettings();
settings.initialTime = cs.getInitialTime();
if (cs.getIncrement()!=0) {
settings.increment = cs.getIncrement();
settings.movesNumberBeforeIncrement = cs.getMovesNumberBeforeIncrement();
}
return settings;
}
}
30 changes: 16 additions & 14 deletions src/main/java/com/fathzer/jchess/swing/GameSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.fathzer.games.Status;
import com.fathzer.jchess.AbstractGameSession;
import com.fathzer.jchess.Game;
import com.fathzer.jchess.GameRecorder;
import com.fathzer.jchess.Move;
import com.fathzer.jchess.ai.evaluator.NaiveEvaluator;
import com.fathzer.jchess.bot.Engine;
Expand Down Expand Up @@ -35,9 +36,7 @@ protected void onStateChanged(State old, State current) {
}

private void swingStateChanged(State current) {
if (State.RUNNING.equals(current)) {
nextMove();
} else {
if (!State.RUNNING.equals(current)) {
gui.getBoard().setManualMoveEnabled(false);
}
}
Expand All @@ -52,8 +51,8 @@ protected void onPlayerColorsChanged() {
}

@Override
protected void initGame() {
super.initGame();
protected void newGame() {
super.newGame();
setEvaluation();
gui.setPlayer1Color(player1Color);
gui.setClock(game.getClock());
Expand Down Expand Up @@ -91,7 +90,6 @@ protected void play(Game game, Move move) {
});
}

@Override
protected void onMove(Move move) {
gui.repaint();
this.game.onMove(move);
Expand Down Expand Up @@ -140,8 +138,8 @@ private void setEvaluation() {
}

@Override
protected void timeUp(Status status) {
SwingUtilities.invokeLater(()->doTimeUp(status));
protected void doTimeUp(Status status) {
SwingUtilities.invokeLater(()->super.doTimeUp(status));
}

@Override
Expand All @@ -160,6 +158,16 @@ private void onEngineError(Engine engine) {
endOfGame(status);
}

@Override
protected void onGameEnded() {
gui.setScore(getScore());
try {
GameRecorder.print(this.game.getHistory(), this.getSettings(), this.player1Color, (long) this.getScore().getGameCount());
} catch (Exception e) {
log.error("An error occured while writing pgn",e);
}
}

@Override
protected boolean isMakeRevenge(final Status status) {
boolean result = super.isMakeRevenge(status);
Expand All @@ -170,12 +178,6 @@ protected boolean isMakeRevenge(final Status status) {
}
return result;
}

@Override
protected void updateScores(Status status) {
super.updateScores(status);
gui.setScore(getScore());
}

private String getMessage(Status status) {
if (Status.DRAW.equals(status)) {
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/fathzer/jchess/swing/JChess.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import org.json.JSONObject;
import org.slf4j.LoggerFactory;

import com.fathzer.jchess.AbstractGameSession;
import com.fathzer.jchess.bot.uci.EngineLoader;
import com.fathzer.jchess.bot.uci.EngineLoader.EngineData;
import com.fathzer.jchess.settings.Context;
import com.fathzer.jchess.settings.Settings;
import com.fathzer.jchess.settings.Settings.PlayerSettings;
import com.fathzer.jchess.swing.settings.SettingsDialog;
import com.fathzer.jchess.tournament.Tournament;
import com.fathzer.jchess.uci.JChessUCI;
import com.fathzer.soft.ajlib.swing.framework.Application;
import com.fathzer.util.TinyJackson;
Expand All @@ -41,6 +43,8 @@ public class JChess extends Application {
public static void main(String[] args) {
if (Boolean.getBoolean("uci")) {
JChessUCI.main(args);
} else if (Boolean.getBoolean("tournament")) {
new Tournament().launch(args);
} else {
new JChess().launch();
}
Expand Down Expand Up @@ -104,7 +108,7 @@ protected boolean onStart() {
fixSettings();
this.game = new GameSession(panel.getGamePanel(), settings);
this.game.addListener((o,n) -> {
if (GameSession.State.ENDED.equals(n)) {
if (AbstractGameSession.State.ENDED.equals(n)) {
this.startAction.setEnabled(true);
this.panel.setMenuVisible(true);
}
Expand Down
75 changes: 75 additions & 0 deletions src/main/java/com/fathzer/jchess/tournament/Tournament.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.fathzer.jchess.tournament;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fathzer.games.clock.ClockSettings;
import com.fathzer.games.clock.PGNTimeControlTagParser;
import com.fathzer.jchess.bot.uci.EngineLoader;
import com.fathzer.jchess.bot.uci.EngineLoader.EngineData;
import com.fathzer.jchess.settings.BasicClockSettings;
import com.fathzer.jchess.settings.Settings;
import com.fathzer.jchess.settings.Settings.EngineSettings;

public class Tournament {
private static final Logger LOGGER = LoggerFactory.getLogger(Tournament.class);

public void launch(String[] args) {
if (!init(args)) {
System.exit(-1);
}
final Settings settings = new Settings();
final ClockSettings clock = new PGNTimeControlTagParser().toClockSettings(args[0]);
settings.setClock(BasicClockSettings.fromClockSettings(clock));
settings.getPlayer1().setName(args[1]);
settings.getPlayer1().setEngine(new EngineSettings());
settings.getPlayer1().getEngine().setName(args[1]);
settings.getPlayer2().setName(args[2]);
settings.getPlayer2().setEngine(new EngineSettings());
settings.getPlayer2().getEngine().setName(args[2]);
final TournamentGameSession session = new TournamentGameSession(settings);
final Thread thread = new Thread(session);
thread.setName("Game session");
thread.start();
}

private boolean init(String[] args) {
if (args.length!=3) {
LOGGER.error("Expecting exactly 3 arguments but found {}", args.length);
return false;
} else {
try {
EngineLoader.init();
} catch (IOException e) {
LOGGER.error("An error occured while reading the external engine configuration file (data/engines.json)", e);
return false;
}
}
return Arrays.stream(args).skip(1).map(this::startEngine).allMatch(r -> r);
}

private boolean startEngine(String name) {
final List<EngineData> availableEngines = EngineLoader.getEngines();
final Optional<EngineData> engine = availableEngines.stream().filter(e -> name.equals(e.getName())).findFirst();
if (!engine.isPresent()) {
LOGGER.error("Can't find {} engine", name);
return false;
}
// Ensure engine is started
if (engine.get().getEngine()==null) {
try {
engine.get().start();
LOGGER.info("{} engine started", name);
} catch (IOException e) {
LOGGER.error("Can't start "+name+" engine", e);
return false;
}
}
return true;
}
}
Loading

0 comments on commit 1b2d50b

Please sign in to comment.