Package com.fortemate.dicechess.runtime


package com.fortemate.dicechess.runtime
Transport runtime for DiceChess webhook bots.

A DiceChess bot is a small HTTP endpoint: the platform (play-api) delivers each turn or optional decision as a signed HTTP request, and the bot answers with a typed action. Everything in this package is the plumbing around that contract — HMAC-SHA256 signature verification (Signatures), the one-time ownership handshake and delivery orchestration (WebhookHandler), and (optionally) the HTTP server itself (CustomHandlerServer) — so a bot author supplies one BotStrategy.

The whole wiring for a bot's main method:

BotStrategy strategy = context -> new TurnAction(List.of("e2e4"));
String secret = System.getenv("DICECHESS_WEBHOOK_SECRET");
WebhookHandler handler = new WebhookHandler(secret, strategy);
CustomHandlerServer.startFromEnvironment(handler);

Only JDK types cross the public boundary — no Gson type, and nothing library-specific, appears in a public signature. BotStrategy is a functional interface: BotStrategy.onTurn(TurnContext) is its single abstract method, while BotStrategy.onDrawDecision(DrawDecisionContext) safely declines by default. Java, Kotlin, and Scala consumers can therefore continue to use a lambda when they need only normal turns.

Draw decisions

The exact lowercase draws webhook capability opts an endpoint into pre-roll drawDecision deliveries. Without that capability, play-api declines the offer on the bot's behalf, reveals the dice, and sends a normal yourTurn. With it, the strategy receives a dice-free DrawDecisionContext and returns a DrawAction. The default callback returns DrawAction.decline(), so enabling runtime v2 never silently accepts a draw.

BotStrategy strategy = new BotStrategy() {
    @Override
    public TurnAction onTurn(TurnContext context) {
        return new TurnAction(List.of("e2e4")); // offerDraw defaults to false
    }

    @Override
    public DrawAction onDrawDecision(DrawDecisionContext context) {
        return DrawAction.decline(); // accept only after an evaluated bot policy says so
    }
};

Stake doubling

The exact lowercase doubling webhook capability opts an endpoint into pre-roll stake doubling decisions in staked games. Staked games use closed-loop PLAY_CREDIT units and follow the accepted play-api contract (ADR-0019). The platform resolves any pending draw first. If no draw is pending and the turn owner is eligible to offer, play-api delivers a dice-free doubleOpportunity before rolling. If an offer is made, the responder receives a dice-free doubleDecision before the roll point.

A bot author implements an intentional policy by overriding BotStrategy.onDoubleOpportunity(DoubleOpportunityContext) and/or BotStrategy.onDoubleDecision(DoubleDecisionContext). Both methods have safe defaults:

BotStrategy strategy = new BotStrategy() {
    @Override
    public TurnAction onTurn(TurnContext context) {
        return new TurnAction(List.of("e2e4"));
    }

    @Override
    public DoubleOfferAction onDoubleOpportunity(DoubleOpportunityContext context) {
        // Evaluate engine policy using context.currentStake(), context.cubeValue(), context.dfen()
        return DoubleOfferAction.roll();
    }

    @Override
    public DoubleResponseAction onDoubleDecision(DoubleDecisionContext context) {
        // Evaluate engine take/drop threshold using context.proposedStake(), context.offeredBy()
        return DoubleResponseAction.decline();
    }
};

Bots with no engine of their own

TurnContext.legalMoves carries every complete legal turn, already walked from the server's prefix tree — a strategy can pick straight from that list and never parse a DFEN or generate a move itself. An empty list means the server is auto-passing, so no bot action is required; null means the tree is unknown. The rare turn where the tree is too large to inline falls back to GET /games/{id}/moves — a public, unauthenticated endpoint — if the WebhookHandler constructor that takes play-api's base URL was used; otherwise legalMoves is simply null on that turn, same as it always is when a strategy doesn't need it.

TurnContext.clock is null for an untimed game. Otherwise the GameClock values are milliseconds from the bot's point of view; only a Fischer control has a non-null increment. TurnContext.mayOfferDraw fails closed to false when the optional wire field is absent, null, or malformed.

Key rotation and verification v2 (ADR 004)

To support staged secret management, URL replacement, and zero-downtime same-URL secret rotation, WebhookKeys provides an immutable key-set configuration:

  • Active only: steady-state delivery verification;
  • Pending only: initial registration of an unverified endpoint before activation;
  • Active and pending: staged rotation during which activation challenges are verified using the pending key only, while ongoing gameplay deliveries are accepted under either key.

When both keys are configured, delivery signature verification evaluates both keys using constant-time comparisons without early return to eliminate timing oracles, and never exposes which key matched.

Legacy version-1 and version-absent verification challenges continue to echo the nonce without signature validation. Challenges declaring "version": 2 require signed headers over the exact raw request body with the pending key, and return an independent cryptographic HMAC response proof. Single-key endpoints cannot perform safe same-URL session rotation without downtime.

Concurrency

One strategy instance is shared by its handler. CustomHandlerServer uses virtual threads, so callbacks for different games may overlap. Contexts and actions are immutable snapshots; mutable engine, cache, or per-game state captured by a strategy must be synchronized or otherwise thread-safe.

Migration from v1

Version 2 intentionally removes the v1 Function<TurnContext, List<String>> callback and redesigns TurnContext. There is no compatibility constructor or callback adapter in the v2 artifact: return a TurnAction from onTurn, update context access, and override onDrawDecision only when adopting the draws capability. Previously published immutable v1 artifacts remain unchanged.

What is deliberately not here

DFEN parsing and independent move legality are still not this package's concern — it relays the server's own tree rather than recomputing one, so an engine-linked bot is free to ignore legalMoves entirely and keep deriving moves from dfen itself. It also does not read or write an opening book itself; JsonFiles is a generic string-map loader a strategy can use for that, or for any similarly simple lookup table.