Skip to content

Commit

Permalink
Oups complete previous (early) commit.
Browse files Browse the repository at this point in the history
Bug fixes:
- AbstractEngine, after a go wtime xxx that ends with an exception, a go
command (without time options) continues to use the previous max time.
- AbstractEngine hanged with no transposition table
  • Loading branch information
fathzer committed Nov 25, 2024
1 parent 495e9ff commit 1c410e8
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 82 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ It does not directly support the following commands (but you can add them in an
- **Dont miss the ShredderChess Annual Barbeque**: This command was in the original specification ... But was a joke.
- **register**: As a promoter of open source free sofware, I will not encourage you to develop software that requires registration.
- **ponderhit** is not yet implemented.
- Only depth, score, hashfull and pv are implemented in info lines preceeding go reply.
- Only depth, score and pv are implemented in info lines preceding go reply.

It also does not recognize commands starting with unknown token (to be honest, it's not very hard to implement but seemed a very bad, error prone, idea to me).

Expand Down Expand Up @@ -92,4 +92,5 @@ If you do not use the *com.fathzer.jchess.uci.extended* and *com.fathzer.jchess.

## TODO
* Verify the engine is protected against strange client behavior (like changing the position during a go request).
* Implement support for multi-PV search.
* Implement support for pondering.
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<dependency>
<groupId>com.fathzer</groupId>
<artifactId>games-core</artifactId>
<version>0.0.11-SNAPSHOT</version>
<version>0.0.12-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -36,6 +36,11 @@
<version>4.2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.14.2</version>
</dependency>
</dependencies>

<build>
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/com/fathzer/jchess/uci/BackgroundTaskManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

import com.fathzer.games.util.exec.CustomThreadFactory;
import com.fathzer.jchess.uci.UCI.ThrowingRunnable;

class BackgroundTaskManager implements AutoCloseable {
static class Task {
private final Consumer<Exception> logger;
private final Runnable run;
private final ThrowingRunnable run;
private final Runnable stopTask;

Task(Runnable task, Runnable stopTask, Consumer<Exception> logger) {
Task(ThrowingRunnable task, Runnable stopTask, Consumer<Exception> logger) {
this.run = task;
this.stopTask = stopTask;
this.logger = logger;
}
}


private final ExecutorService exec = Executors.newFixedThreadPool(1);

private final ExecutorService exec = Executors.newFixedThreadPool(1, new CustomThreadFactory(()->"Stoppable Tasks", true));
private final AtomicReference<Task> current = new AtomicReference<>();

boolean doBackground(Task task) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/fathzer/jchess/uci/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ default List<Option<?>> getOptions() {
* <li>The supplier should be cooperative with the stopper; It should end as quickly as possible when stopper is invoked and <b>always</b> return a move.</li>
* </ul>
* @param params The go parameters.
* @return A long running task able to compute the engine's move.
* @return A task able to compute the engine's move.
*/
LongRunningTask<GoReply> go(GoParameters params);
StoppableTask<GoReply> go(GoParameters params);
}
27 changes: 9 additions & 18 deletions src/main/java/com/fathzer/jchess/uci/StoppableTask.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
package com.fathzer.jchess.uci;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.Callable;

/** A task that will be executed in the background of UCI interface.
/** A task that can be stopped.
* <br>Please note that stoppable is different from {@link java.util.concurrent.Cancellable}
* When a task is cancelled, it produces no result (for example, a {@link java.util.concurrent.CancellationException} if the task was cancelled).
* The typical use case of a StoppableTask is a best move search engine that performs iterative deepening. You may want to stop its deepening and get the current result.
* @param <T> The result of the task
*/
public abstract class LongRunningTask<T> {
private final AtomicBoolean stopped;

protected LongRunningTask() {
stopped = new AtomicBoolean();
}

public abstract T get();

public boolean isStopped() {
return stopped.get();
}

public void stop() {
stopped.set(true);
}
public interface StoppableTask<T> extends Callable<T> {
/** Stops the task.
*/
void stop();
}
15 changes: 11 additions & 4 deletions src/main/java/com/fathzer/jchess/uci/UCI.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@
*/
public class UCI implements Runnable, AutoCloseable {
public static final String INIT_COMMANDS_PROPERTY_FILE = "uciInitCommands";


@FunctionalInterface
/** A runnable that can throw an exception.
*/
public interface ThrowingRunnable {
void run() throws Exception;
}

private static final BufferedReader IN = new BufferedReader(new InputStreamReader(System.in));
private static final String MOVES = "moves";
private static final String ENGINE_CMD = "engine";
Expand Down Expand Up @@ -196,7 +203,7 @@ private String getFEN(Collection<String> tokens) {
* @param logger Where to send the exceptions
* @return true if the task is launched, false if another task is already running.
*/
protected boolean doBackground(Runnable task, Runnable stopper, Consumer<Exception> logger) {
protected boolean doBackground(ThrowingRunnable task, Runnable stopper, Consumer<Exception> logger) {
return backTasks.doBackground(new Task(task, stopper, logger));
}

Expand All @@ -206,9 +213,9 @@ protected void doGo(Deque<String> tokens) {
} else {
final Optional<GoParameters> goOptions = parse(GoParameters::new, GoParameters.PARSER, tokens);
if (goOptions.isPresent()) {
final LongRunningTask<GoReply> task = engine.go(goOptions.get());
final StoppableTask<GoReply> task = engine.go(goOptions.get());
final boolean started = doBackground(() -> {
final GoReply goReply = task.get();
final GoReply goReply = task.call();
final Optional<String> mainInfo = goReply.getMainInfoString();
if (mainInfo.isPresent()) {
this.out(mainInfo.get());
Expand Down
14 changes: 4 additions & 10 deletions src/main/java/com/fathzer/jchess/uci/extended/ExtendedUCI.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import com.fathzer.games.perft.PerfTTestData;
import com.fathzer.games.perft.TestableMoveGeneratorBuilder;
import com.fathzer.jchess.uci.Engine;
import com.fathzer.jchess.uci.LongRunningTask;
import com.fathzer.jchess.uci.StoppableTask;
import com.fathzer.jchess.uci.UCI;
import com.fathzer.jchess.uci.parameters.PerfStatsParameters;
import com.fathzer.jchess.uci.parameters.PerfTParameters;
Expand Down Expand Up @@ -67,22 +67,16 @@ protected <M> void doPerft(Deque<String> tokens) {
final Optional<PerfTParameters> params = parse(PerfTParameters::new, PerfTParameters.PARSER, tokens);
if (params.isPresent()) {
@SuppressWarnings("unchecked")
final LongRunningTask<PerfTResult<M>> task = new PerftTask<>((MoveGeneratorSupplier<M>)engine, params.get());
final StoppableTask<PerfTResult<M>> task = new PerftTask<>((MoveGeneratorSupplier<M>)engine, params.get());
if (!doBackground(() -> doPerft(task, params.get()), task::stop, e -> err(PERFT_COMMAND,e))) {
debug("Engine is already working");
}
}
}

// private void background(Runnable task, Runnable stopper) {
// if (!doBackground(task, stopper, e -> out(e,0))) {
// debug("Engine is already working");
// }
// }

private <M> void doPerft(LongRunningTask<PerfTResult<M>> task, PerfTParameters params) {
private <M> void doPerft(StoppableTask<PerfTResult<M>> task, PerfTParameters params) throws Exception {
final long start = System.currentTimeMillis();
final PerfTResult<M> result = task.get();
final PerfTResult<M> result = task.call();

final long duration = System.currentTimeMillis() - start;
if (result.isInterrupted()) {
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/com/fathzer/jchess/uci/extended/PerftTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import com.fathzer.games.perft.PerfT;
import com.fathzer.games.perft.PerfTResult;
import com.fathzer.games.util.exec.ContextualizedExecutor;
import com.fathzer.jchess.uci.LongRunningTask;
import com.fathzer.jchess.uci.StoppableTask;
import com.fathzer.jchess.uci.parameters.PerfTParameters;

class PerftTask<M> extends LongRunningTask<PerfTResult<M>> {
class PerftTask<M> implements StoppableTask<PerfTResult<M>> {
private PerfT<M> perft;
private final Supplier<MoveGenerator<M>> engine;
private final PerfTParameters params;
Expand All @@ -21,7 +21,7 @@ public PerftTask(Supplier<MoveGenerator<M>> engine, PerfTParameters params) {
}

@Override
public PerfTResult<M> get() {
public PerfTResult<M> call() {
try (ContextualizedExecutor<MoveGenerator<M>> exec = new ContextualizedExecutor<>(params.getParallelism())) {
this.perft = new PerfT<>(exec);
if (params.isLegal()) {
Expand All @@ -36,7 +36,6 @@ public PerfTResult<M> get() {

@Override
public void stop() {
super.stop();
perft.interrupt();
}

Expand Down
67 changes: 36 additions & 31 deletions src/main/java/com/fathzer/jchess/uci/helper/AbstractEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import com.fathzer.jchess.uci.GoReply.Info;
import com.fathzer.jchess.uci.GoReply.MateScore;
import com.fathzer.jchess.uci.GoReply.Score;
import com.fathzer.jchess.uci.StoppableTask;
import com.fathzer.jchess.uci.ClassicalOptions;
import com.fathzer.jchess.uci.Engine;
import com.fathzer.jchess.uci.LongRunningTask;
import com.fathzer.jchess.uci.UCIMove;
import com.fathzer.jchess.uci.extended.MoveGeneratorSupplier;
import com.fathzer.jchess.uci.extended.MoveToUCIConverter;
Expand Down Expand Up @@ -64,7 +64,8 @@ public abstract class AbstractEngine<M, B extends MoveGenerator<M>> implements E
*/
protected AbstractEngine(IterativeDeepeningEngine<M, B> engine, TimeManager<B> timeManager) {
this.engine = engine;
this.ttSizeInMB = engine.getTranspositionTable().getMemorySizeMB();
final TranspositionTable<M> transpositionTable = engine.getTranspositionTable();
this.ttSizeInMB = transpositionTable==null ? -1 : transpositionTable.getMemorySizeMB();
this.defaultThreads = engine.getParallelism();
this.defaultDepth = engine.getDeepeningPolicy().getDepth();
this.defaultMaxTime = engine.getDeepeningPolicy().getMaxTime();
Expand All @@ -85,8 +86,10 @@ public int getDefaultHashTableSize() {

@Override
public void setHashTableSize(int sizeInMB) {
if (engine.getTranspositionTable().getMemorySizeMB()!=sizeInMB) {
engine.setTranspositionTable(buildTranspositionTable(sizeInMB));
final TranspositionTable<M> transpositionTable = engine.getTranspositionTable();
final int currentSize = transpositionTable==null ? -1 : transpositionTable.getMemorySizeMB();
if (currentSize!=sizeInMB) {
engine.setTranspositionTable(sizeInMB<0 ? null : buildTranspositionTable(sizeInMB));
}
}

Expand Down Expand Up @@ -130,41 +133,43 @@ public void move(UCIMove move) {
}

@Override
public LongRunningTask<GoReply> go(GoParameters options) {
return new LongRunningTask<>() {
public StoppableTask<GoReply> go(GoParameters options) {
return new StoppableTask<>() {
@Override
public GoReply get() {
public GoReply call() {
final UCIEngineSearchConfiguration<M, B> c = new UCIEngineSearchConfiguration<>(timeManager);
final UCIEngineSearchConfiguration.EngineConfiguration previous = c.configure(engine, options, board);
final List<M> candidates = options.getMoveToSearch().stream().map(AbstractEngine.this::toMove).toList();
final SearchHistory<M> search = engine.getBestMoves(board, candidates.isEmpty() ? null : candidates);
c.set(engine, previous);
if (search.isEmpty()) {
return new GoReply(null);
try {
final List<M> candidates = options.getMoveToSearch().stream().map(AbstractEngine.this::toMove).toList();
final SearchHistory<M> search = engine.getBestMoves(board, candidates.isEmpty() ? null : candidates);
if (search.isEmpty()) {
return new GoReply(null);
}
final EvaluatedMove<M> move = getSelected(board, search);
final GoReply goReply = new GoReply(toUCI(move.getContent()));
final Info info = new Info(search.getDepth());
final TranspositionTable<M> tt = engine.getTranspositionTable();
final int entryCount = tt.getEntryCount();
if (entryCount>0) {
info.setHashFull((int)(1000L*entryCount/tt.getSize()));
}
final List<EvaluatedMove<M>> bestMoves = search.getBestMoves();
final Map<String, Optional<Score>> scores = bestMoves.stream().collect(Collectors.toMap(em -> toUCI(em.getContent()).toString(), em -> toScore(em.getEvaluation())));
info.setScoreBuilder(m -> scores.get(m.toString()));
info.setPvBuilder(m -> {
final List<UCIMove> list = tt.collectPV(board, toMove(m), info.getDepth()).stream().map(x -> toUCI(x)).toList();
return list.isEmpty() ? Optional.empty() : Optional.of(list);
});
info.setExtraMoves(bestMoves.stream().filter(em -> !move.getContent().equals(em.getContent())).limit(engine.getDeepeningPolicy().getSize()-1).map(em->toUCI(em.getContent())).toList());
goReply.setInfo(info);
return goReply;
} finally {
c.set(engine, previous);
}
final EvaluatedMove<M> move = getSelected(board, search);
final GoReply goReply = new GoReply(toUCI(move.getContent()));
final Info info = new Info(search.getDepth());
final TranspositionTable<M> tt = engine.getTranspositionTable();
final int entryCount = tt.getEntryCount();
if (entryCount>0) {
info.setHashFull((int)(1000L*entryCount/tt.getSize()));
}
final List<EvaluatedMove<M>> bestMoves = search.getBestMoves();
final Map<String, Optional<Score>> scores = bestMoves.stream().collect(Collectors.toMap(em -> toUCI(em.getContent()).toString(), em -> toScore(em.getEvaluation())));
info.setScoreBuilder(m -> scores.get(m.toString()));
info.setPvBuilder(m -> {
final List<UCIMove> list = tt.collectPV(board, toMove(m), info.getDepth()).stream().map(x -> toUCI(x)).toList();
return list.isEmpty() ? Optional.empty() : Optional.of(list);
});
info.setExtraMoves(bestMoves.stream().filter(em -> !move.getContent().equals(em.getContent())).limit(engine.getDeepeningPolicy().getSize()-1).map(em->toUCI(em.getContent())).toList());
goReply.setInfo(info);
return goReply;
}

@Override
public void stop() {
super.stop();
engine.interrupt();
}
};
Expand Down
8 changes: 6 additions & 2 deletions src/test/java/com/fathzer/jchess/uci/UCITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,15 @@ void bug20241123() {
engine.setPositionConsumer(s -> {});
assertTrue(uci.post("position fen toto", 10));

engine.setGoFunction(s -> new LongRunningTask<>() {
engine.setGoFunction(s -> new StoppableTask<>() {
@Override
public GoReply get() {
public GoReply call() {
throw new UnsupportedOperationException("I'm a buggy engine");
}

@Override
public void stop() {
}
});
uci.post("go", 10);
await().atMost(200, TimeUnit.MILLISECONDS).until(() -> uci.getExceptions().getOrDefault("go", new IllegalArgumentException()).getClass()==UnsupportedOperationException.class);
Expand Down
Loading

0 comments on commit 1c410e8

Please sign in to comment.