Developers & Community
HedgeHog ships its in-browser engine and a set of open-weight neural networks for anyone who wants to build bots, analysis tools, training drills, or research projects on top of HedgeHog. No account, no API key. Grab the files, read the docs, ship something.
View source & docs on GitLab → Running a platform? Analysis handoff ↓ Analysing for your users? Partner API ↓WebAssembly Engine
The OGXEngine WASM module exposes position evaluation, move generation, and N-ply search via a single class. It is compiled with Emscripten and Highway SIMD, and runs in every modern browser (Chrome 91+, Firefox 90+, Safari 16.4+, Edge 91+).
ES module loader. Exports a default async factory that returns the module instance.
WebAssembly binary (SIMD build). Loaded by ogx.js.
Web Worker wrapper. Run the engine off the main thread for a responsive UI.
All three files must be served from the same origin. ogx.js fetches ogx.wasm relative to its own URL.
Open-Weight Networks
OGXF is HedgeHog's extensible chunk-based format for neural networks. These models are released for community use: redistribute them, fine-tune from them, or embed them in your own projects.
Premium Standard and Pro tier models (deeper ensembles, specialist experts) remain server-side only and are accessed through the analysis API.
Quick Start
Inline (main thread)
<script type="module">
import createOGXModule from '/static/wasm/ogx.js';
const ogx = await createOGXModule();
const engine = new ogx.OGXEngine();
// Load an open-weight network
const buf = await fetch('/model.bin').then(r => r.arrayBuffer());
const ptr = ogx._malloc(buf.byteLength);
ogx.HEAPU8.set(new Uint8Array(buf), ptr);
engine.loadModelData(ptr, buf.byteLength);
ogx._free(ptr);
// Evaluate the starting position
const ogid = engine.getStartingPosition();
const evalResult = engine.evaluate(ogid);
console.log('equity =', evalResult.equity);
// Best move for dice 3-1 at 2-ply
const best = engine.findBestMoveNPly(ogid, 3, 1, 2);
console.log(best.move, best.equity);
</script>Web Worker (recommended)
const worker = new Worker('/static/wasm/ogx_worker.js', { type: 'module' });
worker.postMessage({ id: 1, action: 'init' });
worker.postMessage({ id: 2, action: 'loadModel', params: { url: '/model.bin' } });
worker.postMessage({
id: 3,
action: 'findBestMove',
params: { ogid: '4HPwATDgc/ABMA', d1: 3, d2: 1, ply: 2 },
});
worker.onmessage = (e) => {
if (e.data.type === 'success') console.log('result', e.data.result);
};OGXEngine API
Model
| Method | Description |
|---|---|
| loadModelData(ptr, size) | Load an OGXF model from a HEAPU8 pointer. Returns true on success. |
| isModelLoaded() | Boolean: whether a model is currently loaded. |
| isEnsemble() | Boolean: true for ensemble (multi-network) models. |
| getNetworkConfig() | JSON string describing architecture: input / hidden / output sizes, expert & MoE config. |
| getLastLoadError() | Human-readable reason for the last failed load. |
Position & evaluation
| Method | Description |
|---|---|
| isValidOgid(ogid) | Validate an OGID string. |
| getStartingPosition() | Canonical opening OGID. |
| getOpeningRoll() | Random opening roll: { startingColor, dice }. |
| evaluate(ogid) | Direct NN eval. Returns { win, gammon_win, bg_win, gammon_loss, bg_loss, equity }. |
| getActivations(ogid) | Intermediate layer activations for visualization / interpretation work. |
| getExpertFeatures(ogid) | Per-expert feature values (models with expert features only). |
Moves & search
| Method | Description |
|---|---|
| countMoves(ogid, d1, d2) | Number of legal move combinations for the given roll. |
| getMoves(ogid, d1, d2) | All legal moves with direct NN evals (fast, no lookahead). |
| getMovesNPly(ogid, d1, d2, ply) | All legal moves, scored by N-ply search. ply ∈ {1, 2}. |
| findBestMove(ogid, d1, d2) | Best move at 1-ply (pick the successor with the highest NN eval). |
| findBestMove2Ply(ogid, d1, d2) | Best move at 2-ply. |
| findBestMoveNPly(ogid, d1, d2, ply) | Best move at N-ply. Community build accepts ply ∈ {1, 2}; values > 2 are clamped. |
Cube decisions
| Method | Description |
|---|---|
| getCubeDecision(ogid, cubeValue, cubeOwner, matchLength, myScore, oppScore, depth, isCrawford) | Full Janowski cube analysis: no-double / double-take / double-pass equities, take & double points, winning probability, recommended action. isCrawford cannot be derived from the score, so pass it during the Crawford game. |
| shouldDouble(ogid, cubeValue, cubeOwner, matchLength, myScore, oppScore, isCrawford) | Simplified wrapper: { shouldDouble, action }. |
| shouldTake(ogid, cubeValue, cubeOwner, matchLength, myScore, oppScore, isCrawford) | Simplified take/pass decision from the doubled player's perspective (that player on roll in the ogid). cubeValue and cubeOwner describe the cube BEFORE the double. |
Perspective. All outputs are on-roll: equity is positive when the side to move is winning.
Search. The community WASM ships a stripped-down expectiminimax capped at 2-ply. Star pruning, move filtering, late-move reductions, transposition tables, and deeper searches (3-ply, 4-ply, rollouts) are excluded from this build and are only available through server-side Standard and Pro analysis.
Cube. Cube decisions use Janowski's cubeful equity formulas on top of a 2-ply search, the same math as the engine's full build, just with a shallower search horizon.
Data Formats
OGID
Spec Canonical position string: white:black:cube:dice:color:state:ws:bs:ml:mid. Round-trips with GNUbg and XG position IDs.
OGXF
Spec →Chunk-based neural network container (NETW, FEAT, LAYR, WGHT, EXPR, MXOE, ENSR, SUBN, META, CSUM). Versioned and checksummed.
OGXM
Details ↓Compact match format storing actions (not positions), with typed event, platform and player context. Optional analysis sections are ancillary and strippable. Streams hold a whole database.
OGXM Match Format
OGXM is HedgeHog's compact binary format for a whole match plus its analysis. It stores each turn as a small ply record (dice, checker steps, and cube actions) and reconstructs every board position by replaying them, so no positions are stored on disk. Analysis (per-move evaluations and alternatives) rides along in separate, ancillary sections that can be stripped without touching the moves. It reads from C++, from the browser via WASM, and from Python via a JSON projection. The current format is version 2.1; version 1 files still load, and are converted on the way in.
A file says where its match came from the way a chess PGN header does. Version 2.1 types the event (name, year, stage, round, table, city, country), the platform (its host name and its own match id, together a stable citation) and a record per player: the platform's user id, the rating before the match with the name of its rating system, country, and whether the player is a human, a bot or an engine. Every field is a fact fixed by the end of the match, so a signed analysis never goes stale when a rating moves afterwards.
The moves
File header, match metadata, and one GAME section per game, about 3 bytes per ply (5 for doubles). Clocks, video marks, annotations and a match signature are optional sections beside them.
The evaluations
One DECS record per decision, holding every legal alternative move at 19 bytes each with its probabilities. The analysis is 90-99% of the file.
How big is a file?
Mean file size of finished matches analysed on HedgeHog, at each length, split into the match part (moves) and the analysis part. The moves are the thin band at the base of each bar.
| Match | N | Games | Plies | Decisions | Match part | Analysis | Total | Range |
|---|---|---|---|---|---|---|---|---|
| 1 pt | 439 | 1.0 | 53 | 50 | 301 B | 14.2 KB | 14.5 KB | 0.6-35.2 |
| 3 pt | 84 | 2.2 | 106 | 103 | 432 B | 29.5 KB | 29.9 KB | 2.6-62.2 |
| 5 pt | 294 | 3.3 | 156 | 149 | 1,026 B | 42.8 KB | 43.8 KB | 9.6-128.4 |
| 7 pt | 235 | 4.7 | 213 | 202 | 2,266 B | 59 KB | 61.2 KB | 2.1-150.9 |
| 9 pt | 15 | 5.9 | 246 | 242 | 7,877 B | 68.3 KB | 76 KB | 14.7-196.7 |
N = finished matches at each length with a 2-ply analysis, as stored on hedgehog-bg.com in OGXM v2 (1,067 in all). Match part in bytes: the plies alone stay near 3 bytes each, and the longer matches carry more clock and annotation data. Analysis and total in KB. Range = smallest and largest total observed.
The lever for size is the alternatives. Default analysis keeps every legal move for each decision, not just the top few. Capping alternatives or leaving out their per-move probabilities (9 bytes each instead of 19) cuts size sharply, and a match-only OGXM is 10-70× smaller than one with full analysis. Every file here is far inside the format's 64 MiB limit.
The frozen version 1 format, which the library still reads: binary and JSON. The specifications are licensed CC BY 4.0: anyone may implement them, and credit is owed only by a copy or adaptation of the documents, never by a reader or writer.
Match databases: OGXM streams
An OGXM stream (.ogxms) is complete OGXM files back to back, with no header or index of its own, so a month of a platform's matches is one file and cat a.ogxm b.ogxm > s.ogxms makes one. Every member carries its own checksum, so a reader tells a corrupt match from a good one and can resume after it. A truncated download still reads: every complete match before the cut comes out. Streams compress with zstd as .ogxms.zst and are read straight from the decompressor.
zstdcat matches.ogxms.zst | ogxm_stream count
zstdcat matches.ogxms.zst | ogxm_stream verify --resync
zstdcat matches.ogxms.zst | ogxm_stream split out/ # one .ogxm per match
zstdcat matches.ogxms.zst | ogxm_stream to-jsonl > matches.jsonl A match split out of a stream opens here like any other: drop the .ogxm on the analyse page. The JSON lines are for pandas or jq; the binary members are what to store and exchange.
Reading OGXM in your own code
libogxm is the reference codec, the same one HedgeHog runs: every rule of the format lives in it, so a file it accepts is a file every OGXM reader accepts. Releases are tagged (build against a tag, never main) and each one ships, for x86_64 and aarch64 Linux, the ogxm Python wheel with the library inside it (also on PyPI), and the library with the ogxm_stream and ogxm_convert tools.
# pip install ogxm
import ogxm
for member in ogxm.iter_stream(open("matches.ogxms", "rb")):
doc = ogxm.binary_to_json(member)
print(doc.get("site"), doc.get("match_ref"), doc["player_white"], doc["player_black"])Analysis Handoff
If you run a backgammon platform, you can send a finished match to HedgeHog for analysis with a single link - no server-to-server call, no API key, no account. The match travels in the URL fragment, which browsers never transmit. It goes from your page straight into the engine running in the player's own browser. Your server never posts it to us, and it never lands in our access logs.
The link
https://hedgehog-bg.com/analyze#h1.mat.gz.H4sIAAAAAAAA_+1Y...| h1 | Envelope version. Bumped only if this grammar changes. |
| mat | Payload format: mat for JellyFish .mat text, or ogxm for the bytes of an OGXM match file, which is smaller and keeps the match context (event, site, player ratings). |
| gz | gzip, or raw for no compression. Use whichever comes out shorter. |
| <payload> | base64url of the compressed bytes, padding stripped. |
HedgeHog opens its import dialog with the match preloaded. The player confirms the names and which side they played, then analysis runs on device. Anything HedgeHog cannot read is reported as a plain message rather than a silently empty analysis.
Building a link
// Build a HedgeHog analysis link for a match: JellyFish .mat text, or the
// bytes of an OGXM file (a Uint8Array) with format 'ogxm'.
// The payload rides in the URL fragment, which browsers never transmit -
// the match goes from this page straight into the user's engine.
async function hedgehogLink(match, format = 'mat') {
const packed = new Uint8Array(await new Response(
new Blob([match]).stream().pipeThrough(new CompressionStream('gzip'))
).arrayBuffer());
let bin = '';
for (const b of packed) bin += String.fromCharCode(b);
const payload = btoa(bin)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return `https://hedgehog-bg.com/analyze#h1.${format}.gz.${payload}`;
}
// Point a link at it, or window.open() it from a click handler.
document.querySelector('#analyse').href = await hedgehogLink(matText);CompressionStream needs Chrome 80+, Firefox 113+, or Safari 16.4+. Generating the link on your backend works too, since it is stock gzip and base64url in any language. If you skip compression, send h1.mat.raw.<payload> (or h1.ogxm.raw.<payload>) instead.
Try it
Paste a .mat match (or load a file) to build a real handoff link against this origin. Everything happens in this tab.
Will a match fit?
Comfortably. .mat is repetitive ASCII and compresses about 3.3×. Measured over 1,500 real files:
| Match | Raw .mat | Link characters |
|---|---|---|
| 3-point match | 5.2 KB | 1,800 |
| 7-point match | 9.0 KB | 2,800 |
| 25-point match | 27 KB | 7,000 |
| Median of 1,500 .mat files | 13 KB | 4,000 |
| 99th percentile of those files | 18 KB | 5,400 |
Firefox has the tightest URL ceiling of the modern browsers at ~65,536 characters; Safari allows ~80,000 and Chrome ~2,000,000. Even a 50-game monster lands near 11,200. The 8-16 KB limit servers place on a request line does not apply here: that is exactly the part of the URL that is never sent. HedgeHog refuses to build a link past 24,000 characters; above that, offer a file download.
An ogxm link for the same match is about 45% of the mat one (the median over 273 matches), so the same ceiling holds a match twice as long.
Two things to know
- It must be the fragment. Put the payload after
#, never in a query string. A query string is sent to the server, blows past request-line limits, and lands in logs, losing every property that makes this worth doing. - .mat is lossy. It carries moves, not positions, so a malformed or ambiguous export can fail to replay. HedgeHog validates the whole match by replay and rejects it with a reason rather than showing a half-empty analysis. If you can emit OGXM, it is about half the size and has none of the ambiguity. Tell us and we will add the format tag.
Partner API
A handoff link opens HedgeHog for the player. The partner API is for an application that keeps the player in its own window: a desktop study tool, a browser app, a club or play site sends a match or a position to HedgeHog's server on behalf of one of its users and reads the analysis back. The user connects their HedgeHog account once, over standard OAuth 2.0, and your application never sees their password.
How it works
- Publish a metadata document at an HTTPS URL of your own. That URL is your client_id: there is nothing to register and no secret.
- Send the user to hedgehog-bg.com/oauth/authorize with a PKCE challenge. They sign in on HedgeHog and approve what your app may do.
- Exchange the code for an access and a refresh token, then call the API as that user: store a match or a .mat file, analyse it, read the result.
- Every analysis is charged to the user's own plan, and every refusal carries a message written for your user to read as it is.
The contract: connecting a user, every route, what it costs the user, every error code and webhooks.
A complete client in standard-library Python: connect, send a .mat, analyse, poll, refresh.
A metadata document to publish at your client_id URL. Change every example.com.
Run the example
python3 hedgehog_partner_client.py --client-id https://example.com/oauth/client.json match.matThe first run opens the browser to connect; later runs reuse the saved tokens. Python 3.10 or later, nothing to install. The same file is run against the API by HedgeHog's own tests, so it stays in step with the documentation.
Getting approved
For now only approved applications can connect. Send your application's name, its client_id URL and what it will do with HedgeHog to hello@hedgehog-bg.com. The developer terms come with the approval. Until then you can build against the documentation and the example.
Translations
Each catalogue is every interface message for one language, as JSON keyed by where the message appears. A language's glossary is the backgammon vocabulary its catalogue follows, with the terms no language translates.
To review a language or correct a message, edit its catalogue against its glossary and send it to hello@hedgehog-bg.com. Keep every placeholder in braces ({n}, {name}) as it is, and keep the | between a message's plural forms.
License & Terms
The WebAssembly engine and the open-weight networks on this page are free to use in non-commercial work: personal projects, research, teaching, and open-source software built on HedgeHog. You do not need an account or a key. All we ask is that you credit HedgeHog and keep this license alongside any copies you pass on.
What you can do
- Embed the engine and the networks in your own non-commercial applications.
- Redistribute the engine files unmodified, with this license and the HedgeHog credit kept intact.
- Fine-tune the open-weight networks, train new ones from them, and share what you build.
What needs a word with us first
- Commercial use of the engine or the networks. Get in touch and we will sort out terms.
- Reverse-engineering or decompiling the engine binary to reconstruct its source. The networks are open by design; the engine is not.
The match format is not under these terms. The OGXM codec (libogxm, its standalone WebAssembly build, the ogxm Python package and the stream tools) is MIT-licensed, free for any use, commercial included. The OGID and OGXM specifications are CC BY 4.0.
Building something? Tell us at hello@hedgehog-bg.com and we will link the good projects from this page.