Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<dicechess.engine.version>1.10.4</dicechess.engine.version>
<dicechess.engine.version>1.12.0</dicechess.engine.version>
<dicechess.bot-runtime.version>1.0.0</dicechess.bot-runtime.version>
<onnxruntime.version>1.20.0</onnxruntime.version>
<junit.jupiter.version>6.1.2</junit.jupiter.version>
Expand Down
143 changes: 23 additions & 120 deletions src/main/java/dicechess/bot/OnnxStrategy.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,26 @@
package dicechess.bot;

import dicechess.engine.domain.FenParser;
import dicechess.engine.domain.GameState;
import dicechess.engine.search.TurnGenerator;
import dicechess.engine.jvmapi.JvmApi;

import lv.id.jc.dicechess.runtime.TurnContext;

import scala.collection.immutable.List;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Strategy implementation using an ONNX value model (or fallback engine evaluation)
* over all legal full-turn paths generated by TurnGenerator.
* Strategy implementation using an ONNX value model (or fallback engine evaluation) over every legal full turn
* available from the current position, via the engine's {@link JvmApi} facade.
*
* <p>TurnGenerator returns {@code List[List[Move]]} where {@code Move} is an opaque
* Int with layout: bits 0-5 = destination (to), bits 6-11 = source (from),
* bits 12-15 = flags.</p>
* <p>{@link TurnContext#legalMoves()} is not consulted here: scoring a candidate turn needs the resulting board
* position, not just its UCI tokens, so the engine's own enumeration ({@link JvmApi#legalTurns(GameState)}) has to
* run regardless of whether the platform's inline tree was present.</p>
*/
public class OnnxStrategy implements Strategy {

private static final Logger logger = System.getLogger(OnnxStrategy.class.getName());
private static final MethodHandle MAKE_MOVE_MH;

static {
try {
var clazz = Class.forName("dicechess.engine.domain.Position$package");
var methodType = MethodType.methodType(GameState.class, GameState.class, int.class);
MAKE_MOVE_MH = MethodHandles.lookup().findStatic(clazz, "makeMove_Move", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

private final OnnxEvaluator evaluator;

Expand All @@ -51,120 +34,40 @@ public OnnxStrategy(OnnxEvaluator evaluator) {
}

@Override
public java.util.List<String> chooseMoves(TurnContext context) {
public List<String> chooseMoves(TurnContext context) {
if (context == null || context.dfen() == null || context.dfen().isBlank()) {
logger.log(Level.WARNING, "Received empty or null DFEN context");
return Collections.emptyList();
}

var parseResult = FenParser.parse(context.dfen());
if (parseResult.isLeft()) {
var errorMsg = parseResult.left().toOption().isDefined()
? parseResult.left().toOption().get()
: "Unknown FEN error";
logger.log(Level.ERROR, "Failed to parse DFEN ''{0}'': {1}", context.dfen(), errorMsg);
GameState initialState;
try {
initialState = JvmApi.parseDfen(context.dfen());
} catch (IllegalArgumentException e) {
logger.log(Level.ERROR, "Failed to parse DFEN ''{0}'': {1}", context.dfen(), e.getMessage());
return Collections.emptyList();
}

var initialState = parseResult.toOption().get();
var activeColor = initialState.flags() & 1; // Bit 0 of flags is activeColor (0 = White, 1 = Black)

// TurnGenerator returns List[List[Move]]
@SuppressWarnings("unchecked")
var legalTurnPaths =
(List<List<Object>>) (Object) TurnGenerator.generateAllLegalTurnPaths(initialState);
var activeColor = JvmApi.activeColor(initialState);
var turns = JvmApi.legalTurns(initialState);

if (legalTurnPaths.isEmpty()) {
if (turns.isEmpty()) {
logger.log(Level.INFO, "No legal turn paths available for DFEN: {0}", context.dfen());
return Collections.emptyList();
}

java.util.List<Integer> bestPath = null;
JvmApi.Turn bestTurn = null;
float bestScore = -Float.MAX_VALUE;

for (var pathScala : scala.jdk.javaapi.CollectionConverters.asJava(legalTurnPaths)) {
var pathJava = toJavaIntegerList(pathScala);

// Apply sequence of Move values using makeMove_Move to get final turn state
var currentState = initialState;
for (var moveInt : pathJava) {
currentState = applyMove(currentState, moveInt);
}

// Score final state from initial mover's perspective
var score = evaluator.evaluate(currentState, activeColor);

if (score > bestScore || bestPath == null) {
for (var turn : turns) {
var score = evaluator.evaluate(turn.finalState(), activeColor);
if (bestTurn == null || score > bestScore) {
bestScore = score;
bestPath = pathJava;
bestTurn = turn;
}
}

if (bestPath == null || bestPath.isEmpty()) {
return Collections.emptyList();
}

var moveNotations = new ArrayList<String>();
for (var moveInt : bestPath) {
moveNotations.add(moveToNotation(moveInt));
}

logger.log(Level.DEBUG, "Chosen turn path: {0} with score {1}", moveNotations, bestScore);
return moveNotations;
}

private static GameState applyMove(GameState state, int moveInt) {
try {
return (GameState) MAKE_MOVE_MH.invokeExact(state, moveInt);
} catch (Throwable e) {
throw new IllegalStateException("Failed to apply move via engine: " + moveInt, e);
}
}

/**
* Converts a packed Move int to long algebraic notation (e.g. "e2e4", "e7e8q").
*
* <p>Move layout (from Move.scala):
* <ul>
* <li>Bits 0-5: destination square (to)</li>
* <li>Bits 6-11: source square (from)</li>
* <li>Bits 12-15: move flags</li>
* </ul>
*/
static String moveToNotation(int move) {
var to = move & 0x3f;
var from = (move >>> 6) & 0x3f;
var flags = (move >>> 12) & 0x0f;

var fromFile = (char) ('a' + (from % 8));
var fromRank = (from / 8) + 1;
var toFile = (char) ('a' + (to % 8));
var toRank = (to / 8) + 1;

// Promotion: flags bit 3 set (flags >= 8)
var promStr = "";
if ((flags & 8) != 0) {
promStr = switch (flags & 3) {
case 0 -> "n"; // KnightPromotion(8) or KnightPromoCapture(12)
case 1 -> "b"; // BishopPromotion(9) or BishopPromoCapture(13)
case 2 -> "r"; // RookPromotion(10) or RookPromoCapture(14)
case 3 -> "q"; // QueenPromotion(11) or QueenPromoCapture(15)
default -> "";
};
}

return "" + fromFile + fromRank + toFile + toRank + promStr;
}

private static java.util.List<Integer> toJavaIntegerList(List<Object> scalaList) {
var result = new ArrayList<Integer>();
for (var item : scala.jdk.javaapi.CollectionConverters.asJava(scalaList)) {
if (item instanceof Integer intValue) {
result.add(intValue);
} else if (item instanceof Number number) {
result.add(number.intValue());
}
}
return result;
logger.log(Level.DEBUG, "Chosen turn path: {0} with score {1}", bestTurn.uci(), bestScore);
return bestTurn.uci();
}
}
Loading