Skip to content

Maven Artifact & JVM Integration

The engine is the single source of truth for Dice Chess rules across the ecosystem. JVM backends — first of all dicechess-analytics (the Scala 3 analytics backend) — consume it as a regular Maven dependency instead of re-implementing game logic.

Non-Scala JVM callers (Java, Kotlin) bind to a dedicated facade rather than to the Scala API directly — see Consuming from Java or Kotlin below.

Every release publishes two JVM coordinates alongside the NPM packages, at one shared version:

dicechess-rules_3 (from 0.11.0)dicechess-engine_3
Group IDcom.fortematecom.fortemate
Contentsrules only: domain model, DFEN, move generation, legal turns, dice probabilitiesbots, evaluators, feature extractors, JvmApi; depends on dicechess-rules_3
Canonical registryMaven CentralMaven Central
Authenticated mirrorGitHub PackagesGitHub Packages

Which coordinate a project needs, and how a rules-only consumer switches, is covered in Published Artifacts & Rules-Only Migration. The rest of this page uses the engine coordinate in its examples; a rules-only project substitutes dicechess-rules / dicechess-rules_3.


Maven Central is the default resolver for all JVM build tools — no extra resolver or authentication is needed.

libraryDependencies += "com.fortemate" %% "dicechess-engine" % "<latest release>"
// rules only (0.11.0+): "com.fortemate" %% "dicechess-rules" % "<latest release>"

ONNX Consumers Declare onnxruntime Directly

Section titled “ONNX Consumers Declare onnxruntime Directly”

The published dicechess-engine_3 POM marks com.microsoft.onnxruntime:onnxruntime as an optional dependency (<optional>true</optional>). This prevents downstream rules-only consumers (such as dicechess-play-api or analytics services) from dragging in the ~54 MB ONNX native binaries when they do not evaluate ML models.

Consumers that use ONNX-backed search bots (OnnxEvalSearch, OnnxExpectimaxSearch) must declare onnxruntime directly in their own build configuration and match the version used by their engine release (the 0.13.0 release uses 1.30.0):

libraryDependencies ++= Seq(
"com.fortemate" %% "dicechess-engine" % "<latest release>",
"com.microsoft.onnxruntime" % "onnxruntime" % "1.30.0"
)
<dependencies>
<dependency>
<groupId>com.fortemate</groupId>
<artifactId>dicechess-engine_3</artifactId>
<version>${dicechess.engine.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.onnxruntime</groupId>
<artifactId>onnxruntime</artifactId>
<version>1.30.0</version>
</dependency>
</dependencies>

Java and Kotlin callers depend on the same artifact, but bind to dicechess.engine.jvmapi.JvmApi rather than to the Scala API. The Scala surface leans on constructs that do not survive the language boundary intact — Either returns, extension methods (which compile onto synthetic $package classes with no ordinary entry point), and opaque types like Move that erase to int, turning a List[List[Move]] into an unchecked List[List[Object]] of boxed integers. JvmApi keeps all of that away from the caller: its signatures use java.util types, primitives, opaque handles passed straight back to the engine, and one facade-owned result type, JvmApi.Turn.

Java callability is pinned by a Java-source test that CI compiles and runs on every build; Kotlin consumes the same static methods and java.util types but is not exercised here — see What “Java and Kotlin” is based on.

Note the _3 suffix in the artifact ID: Maven has no equivalent of sbt’s %% operator, so the Scala binary-version suffix has to be spelled out.

<properties>
<dicechess.engine.version><!-- latest release --></dicechess.engine.version>
</properties>
<dependencies>
<dependency>
<groupId>com.fortemate</groupId>
<artifactId>dicechess-engine_3</artifactId>
<version>${dicechess.engine.version}</version>
</dependency>
</dependencies>

No <repositories> block is needed — Maven Central is configured by default in all standard Maven installations.

dicechess-bot-java is the reference consumer.

The Scala runtime is two artifacts, not one

Section titled “The Scala runtime is two artifacts, not one”

The engine’s POM declares org.scala-lang:scala3-library_3, but since the Scala 3.8 library unification that artifact is an empty shim — its jar contains a manifest and nothing else. The actual runtime classes live in org.scala-lang:scala-library of the same version, which the shim pulls in as a transitive dependency.

Maven, Gradle, and sbt resolve that transitively, so a normal dependency declaration needs no extra work. It breaks when something keeps scala-library off the runtime classpath: a hand-assembled classpath (a hand-listed -cp, a shaded or “fat” jar built from a hand-picked file list, a container image that copies selected jars), or dependency metadata that excludes it, marks it optional, or narrows it to provided scope. Such a classpath links and starts fine and then fails on the first real call into the engine:

java.lang.NoClassDefFoundError: scala/util/boundary$Break

The fix is to put scala-library on the classpath as well (or let the build tool compute the classpath). Verifying is one command — the shim is the jar with a single entry:

Terminal window
unzip -l scala3-library_3-*.jar

Maven Central is the recommended resolver because it is public and requires no configuration. Every new release is also mirrored to GitHub Packages under the same coordinates for authenticated GitHub consumers. The mirror requires a GitHub token with read:packages:

resolvers += "GitHub Packages (dicechess-engine)" at
"https://maven.pkg.github.com/fortemate/dicechess-engine"
credentials += Credentials(
"GitHub Package Registry",
"maven.pkg.github.com",
sys.env("GITHUB_ACTOR"),
sys.env("GITHUB_TOKEN")
)
libraryDependencies += "com.fortemate" %% "dicechess-engine" % "<latest release>"

Use the mirror only when GitHub-authenticated dependency resolution is intentional. Do not commit a token to the build; inject GITHUB_ACTOR and GITHUB_TOKEN from the consumer’s secret store.


Local Development Against Unreleased Changes

Section titled “Local Development Against Unreleased Changes”

When a downstream project needs engine changes that are not released yet, publish the JVM artifact to the local Ivy repository:

Terminal window
mise run publish:local

This publishes the current -SNAPSHOT version to ~/.ivy2/local, where sbt resolves it before any remote registry.

Maven does not read ~/.ivy2/local, so a Maven-built consumer needs the artifact in the local Maven repository instead:

Terminal window
sbt rootJVM/publishM2

That writes to ~/.m2/repository, where Maven picks it up. Point the consumer’s dicechess.engine.version at the -SNAPSHOT value while iterating, and remember to move it back to a real release before opening a PR — CI has no access to your local repository, so a -SNAPSHOT dependency that builds locally fails there.


  • build.sbt sets sonatypeCredentialHost := "central.sonatype.com", selecting the new Sonatype Central Portal. sbt-ci-release (via project/plugins.sbt) manages the canonical publishTo automatically: a release version goes to Sonatype staging, a -SNAPSHOT goes to the snapshot repository.
  • The Maven Central copy is GPG-signed by sbt-pgp using the org key stored in the PGP_SECRET org secret; the passphrase is in PGP_PASSPHRASE. Maven Central verifies the signature against the public key published at keyserver.ubuntu.com.
  • Both CD workflows (release.yaml and publish.yaml) first run .mise/lib/maven-registry-state.sh, which checks the POM plus main, sources, and javadoc jars of each coordinate (dicechess-rules_3, dicechess-engine_3) on the authenticated GitHub Packages registry. A coordinate is published only when its complete version is absent; a partial immutable version fails closed. The same per-coordinate check runs against Maven Central; the absent coordinates are uploaded with rulesJVM/publishSigned and/or rootJVM/publishSigned and promoted together with one sonaRelease (one Central Portal deployment named after both coordinates).
  • Every publish is preceded by the guards of both rows: no coverage instrumentation, no bench classes, META-INF/LICENSE in every jar, a rules POM without third-party compile dependencies, and onnxruntime marked optional in the engine POM.
  • Publish commands run with sbt --server, which executes a foreground process instead of the persistent native thin client. This prevents credentials or coverage settings from a previous session leaking into publication and avoids thin-client startup deadlocks.
  • The benchmark, arena, and cli modules are excluded via publish / skip := true.
  • The steps are intentionally duplicated in both workflows: tags pushed by release.yaml via GITHUB_TOKEN do not trigger publish.yaml (GitHub’s recursion guard).

See CI/CD & Automated Releases for the full pipeline. full pipeline.