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
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This repository serves two primary roles:

- **Java 25 & JDK HttpServer**: Built on modern Java 25 (LTS) with minimal dependencies and zero heavy frameworks (~64 MB RAM footprint).
- **ONNX Model Evaluation**: Evaluates candidate full-turn move paths using ONNX value models (`models/baseline.onnx`) with JvmApi engine heuristic fallback.
- **Bot Runtime Integration**: Uses `lv.id.jc:dicechess-bot-runtime` for HMAC-SHA256 signature verification, webhook handshakes, and `TurnContext` processing.
- **Bot Runtime Integration**: Uses `com.fortemate:dicechess-bot-runtime` (v2) for HMAC-SHA256 signature verification, zero-downtime dual-key rotation (`WebhookKeys`), webhook handshakes, and decision-oriented `BotStrategy` processing.
- **Engine Rules Integration**: Uses the `com.fortemate:dicechess-engine_3:0.3.0` JvmApi facade from Maven Central for strict DFEN parsing, legal turn path generation, and game state evaluation.

## Architecture
Expand All @@ -41,12 +41,13 @@ graph TD

## Environment Variables

| Variable | Default | Description |
|----------------------------|------------------------|------------------------------------------------------------|
| `DICECHESS_WEBHOOK_SECRET` | `""` | Per-bot secret token for HMAC-SHA256 webhook verification |
| `PORT` | `8080` | HTTP server listening port (Koyeb / Cloud Run / VPS) |
| `MODEL_PATH` | `models/baseline.onnx` | Path to the ONNX value model file |
| `JAVA_OPTS` | `-Xmx256m --enable-native-access=ALL-UNNAMED` | JVM memory, GC, and native access settings |
| Variable | Default | Description |
|---------------------------------|------------------------|------------------------------------------------------------|
| `DICECHESS_WEBHOOK_SECRET` | `""` | Active secret token for HMAC-SHA256 webhook verification |
| `DICECHESS_WEBHOOK_NEXT_SECRET` | `""` | Optional pending secret token for zero-downtime rotation |
| `PORT` | `8080` | HTTP server listening port (Koyeb / Cloud Run / VPS) |
| `MODEL_PATH` | `models/baseline.onnx` | Path to the ONNX value model file |
| `JAVA_OPTS` | `-Xmx256m --enable-native-access=ALL-UNNAMED` | JVM memory, GC, and native access settings |

## Quick Start

Expand Down Expand Up @@ -140,7 +141,7 @@ To create a custom bot strategy:
}
}
```
2. Pass your strategy to `WebhookHandler` in `Main.java`.
2. Pass your strategy to `WebhookHandler` in `Main.java` (since `Strategy` extends `BotStrategy`, it integrates directly with the runtime's turn and optional decision cycle).

## Contributing & Security

Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<dicechess.engine.version>0.12.0</dicechess.engine.version>
<dicechess.bot-runtime.version>1.0.1</dicechess.bot-runtime.version>
<dicechess.bot-runtime.version>2.0.0</dicechess.bot-runtime.version>
<onnxruntime.version>1.29.0</onnxruntime.version>
<junit.jupiter.version>6.1.3</junit.jupiter.version>
<logback.version>1.6.3</logback.version>
Expand All @@ -52,7 +52,7 @@
<dependencies>
<!-- Dice Chess Bot Runtime (Java artifact: HMAC, Handshake, TurnContext, JDK HttpServer) -->
<dependency>
<groupId>lv.id.jc</groupId>
<groupId>com.fortemate</groupId>
<artifactId>dicechess-bot-runtime</artifactId>
<version>${dicechess.bot-runtime.version}</version>
</dependency>
Expand Down
140 changes: 106 additions & 34 deletions src/main/java/com/fortemate/dicechess/bot/Main.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.fortemate.dicechess.bot;

import com.fortemate.dicechess.runtime.CustomHandlerServer;
import com.fortemate.dicechess.runtime.WebhookHandler;
import com.fortemate.dicechess.runtime.WebhookKeys;
import com.sun.net.httpserver.HttpServer;
import lv.id.jc.dicechess.runtime.CustomHandlerServer;
import lv.id.jc.dicechess.runtime.WebhookHandler;

import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;

/**
* Entry point for the Dice Chess Java bot starter template.
Expand All @@ -30,41 +33,42 @@ private Main() {
*/
@SuppressWarnings("java:S1172")
public static void main(String[] args) {
var secret = System.getenv().getOrDefault("DICECHESS_WEBHOOK_SECRET", "");
if (secret.isEmpty()) {
logger.log(Level.WARNING, "DICECHESS_WEBHOOK_SECRET is not set — webhook verification handshake may fail");
var server = startApplication(System.getenv());
if (server != null) {
try {
Thread.currentThread().join();
} catch (InterruptedException _) {
Thread.currentThread().interrupt();
}
}
}

var modelPath = System.getenv().getOrDefault("MODEL_PATH", "models/baseline.onnx");
var port = resolvePort();
/**
* Starts the application server using configuration from the provided environment map.
* Aborts and returns null if webhook keys are not configured or binding fails.
*
* @param env the environment variables map
* @return the running HttpServer, or null if initialization aborted
*/
static HttpServer startApplication(Map<String, String> env) {
var keysOpt = resolveWebhookKeys(env);
if (keysOpt.isEmpty()) {
return null;
}

var modelPath = env.getOrDefault("MODEL_PATH", "models/baseline.onnx");
var port = resolvePort(env.get("PORT"));

var evaluator = new OnnxEvaluator(modelPath);
var strategy = new OnnxStrategy(evaluator);

var handler = new WebhookHandler(secret, strategy);

HttpServer server;
try {
server = CustomHandlerServer.start(port, DEFAULT_WEBHOOK_PATH, handler);
// Register health check endpoints for Koyeb / Cloud Run / Kubernetes
server.createContext("/", exchange -> {
var response = "OK".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, response.length);
try (var os = exchange.getResponseBody()) {
os.write(response);
}
});
server.createContext("/health", exchange -> {
var response = "OK".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, response.length);
try (var os = exchange.getResponseBody()) {
os.write(response);
}
});
} catch (IOException e) {
server = start(port, keysOpt.get(), strategy);
} catch (IOException | IllegalArgumentException e) {
logger.log(Level.ERROR, "Failed to start HTTP server on port {0}: {1}", port, e.getMessage());
evaluator.close();
return;
return null;
}

logger.log(Level.INFO, "Dice Chess Java Bot initialized and listening on port {0} at path {1}", port, DEFAULT_WEBHOOK_PATH);
Expand All @@ -75,21 +79,89 @@ public static void main(String[] args) {
evaluator.close();
}));

return server;
}

/**
* Resolves the webhook keys from system environment variables.
*
* @return the resolved webhook keys, or empty if neither active nor pending secret is configured
*/
static Optional<WebhookKeys> resolveWebhookKeys() {
return resolveWebhookKeys(System.getenv());
}

/**
* Resolves the webhook keys from the provided environment map.
* Fails closed by logging an error and returning empty if keys are missing or invalid.
*
* @param env the environment mapping
* @return the resolved webhook keys, or empty if not configured
*/
static Optional<WebhookKeys> resolveWebhookKeys(Map<String, String> env) {
try {
Thread.currentThread().join();
} catch (InterruptedException _) {
Thread.currentThread().interrupt();
return Optional.of(WebhookKeys.fromEnvironment(env));
} catch (IllegalArgumentException e) {
logger.log(Level.ERROR, "Missing or invalid webhook signing keys: {0}", e.getMessage());
return Optional.empty();
}
}

/**
* Resolves the server port from the PORT environment variable.
* Falls back to 8080 if PORT is not set or is invalid.
* Starts the webhook server on an explicit port with configured keys and strategy,
* registering default health endpoints.
*
* @param port the listening port (0 for ephemeral)
* @param keys the webhook key configuration
* @param strategy the bot strategy
* @return the running HTTP server
* @throws IOException if the server fails to bind
*/
public static HttpServer start(int port, WebhookKeys keys, Strategy strategy) throws IOException {
var handler = new WebhookHandler(keys, strategy);
var server = CustomHandlerServer.start(port, DEFAULT_WEBHOOK_PATH, handler);
registerHealthEndpoints(server);
return server;
}

/**
* Starts the webhook server with a single active secret.
*
* @param port the listening port (0 for ephemeral)
* @param secret the active secret
* @param strategy the bot strategy
* @return the running HTTP server
* @throws IOException if the server fails to bind
*/
public static HttpServer start(int port, String secret, Strategy strategy) throws IOException {
return start(port, WebhookKeys.activeOnly(secret), strategy);
}

static void registerHealthEndpoints(HttpServer server) {
server.createContext("/", exchange -> {
var response = "OK".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, response.length);
try (var os = exchange.getResponseBody()) {
os.write(response);
}
});
server.createContext("/health", exchange -> {
var response = "OK".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, response.length);
try (var os = exchange.getResponseBody()) {
os.write(response);
}
});
}

/**
* Resolves the server port from the given port string.
* Falls back to 8080 if string is null, blank, or invalid.
*
* @param portStr the port string from environment
* @return the resolved port number
*/
private static int resolvePort() {
var portStr = System.getenv("PORT");
static int resolvePort(String portStr) {
if (portStr != null && !portStr.isBlank()) {
try {
return Integer.parseInt(portStr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import dicechess.engine.domain.GameState;
import dicechess.engine.jvmapi.JvmApi;

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

import java.lang.System.Logger;
import java.lang.System.Logger.Level;
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/com/fortemate/dicechess/bot/Strategy.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.fortemate.dicechess.bot;

import lv.id.jc.dicechess.runtime.TurnContext;
import com.fortemate.dicechess.runtime.BotStrategy;
import com.fortemate.dicechess.runtime.TurnAction;
import com.fortemate.dicechess.runtime.TurnContext;

import java.util.List;
import java.util.function.Function;

/**
* Common functional interface for Java bot strategies mapping a TurnContext to move notations.
* Implementations must return a list of UCI move notations representing a complete turn.
* Extends {@link BotStrategy} to provide decision-oriented runtime integration while allowing
* simple functional implementations of {@link #chooseMoves(TurnContext)}.
*/
@FunctionalInterface
public interface Strategy extends Function<TurnContext, List<String>> {
public interface Strategy extends BotStrategy, Function<TurnContext, List<String>> {

/**
* Choose the best list of move notations (micro-moves forming a turn) for the given TurnContext.
Expand All @@ -21,6 +24,11 @@ public interface Strategy extends Function<TurnContext, List<String>> {
*/
List<String> chooseMoves(TurnContext context);

@Override
default TurnAction onTurn(TurnContext context) {
return new TurnAction(chooseMoves(context));
}

@Override
default List<String> apply(TurnContext context) {
return chooseMoves(context);
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/com/fortemate/dicechess/bot/package-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* <li>{@link com.fortemate.dicechess.bot.Main}: Application entry point. Configures port, loads secrets from env,
* instantiates evaluator and strategy, and launches the HTTP webhook server.</li>
* <li>{@link com.fortemate.dicechess.bot.Strategy}: Core functional interface for decision-making logic.
* Maps a {@link lv.id.jc.dicechess.runtime.TurnContext} to a list of long algebraic move notations.</li>
* Maps a {@link com.fortemate.dicechess.runtime.TurnContext} to a list of long algebraic move notations.</li>
* <li>{@link com.fortemate.dicechess.bot.OnnxStrategy}: Primary strategy implementation. Parses DFEN via
* {@link dicechess.engine.jvmapi.JvmApi}, expands full multi-move turns via {@link dicechess.engine.jvmapi.JvmApi#legalTurns},
* and scores candidate positions using {@link com.fortemate.dicechess.bot.OnnxEvaluator}.</li>
Expand All @@ -25,11 +25,12 @@
* <caption>Environment Configuration</caption>
* <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
* <tr><td>{@code DICECHESS_WEBHOOK_SECRET}</td><td><em>Empty</em></td><td>HMAC secret key for verifying incoming webhook requests.</td></tr>
* <tr><td>{@code DICECHESS_WEBHOOK_NEXT_SECRET}</td><td><em>Empty</em></td><td>Pending HMAC secret key for dual-key rotation or verification.</td></tr>
* <tr><td>{@code MODEL_PATH}</td><td>{@code models/baseline.onnx}</td><td>Path to the ONNX model file on disk.</td></tr>
* <tr><td>{@code PORT}</td><td>{@code 8080}</td><td>HTTP server binding port for incoming webhook deliveries.</td></tr>
* </table>
*
* @see lv.id.jc.dicechess.runtime.CustomHandlerServer
* @see lv.id.jc.dicechess.runtime.WebhookHandler
* @see com.fortemate.dicechess.runtime.CustomHandlerServer
* @see com.fortemate.dicechess.runtime.WebhookHandler
*/
package com.fortemate.dicechess.bot;
Loading