kaggriculture-engine
A Rust port of the kaggle-environments Kaggriculture interpreter
(kaggriculture.py) that gives bit-identical results. It is about 10,000× faster
per episode than the Python original running under kaggle's Environment.
Given the same configuration and the same per-step agent actions, the port produces byte-identical state to the Python original. That includes float formatting, dict key order, RNG draws, rewards and statuses, and which Python exception (if any) a step raises.
Python (PyO3)
uv sync # builds the extension (maturin) into .venv
.venv/bin/maturin develop --release --uv # rebuild after changing Rust code
import kaggriculture_engine as ke
game = ke.Game({"seed": 42}) # == make("kaggriculture", configuration=...)
game.step([{"farmer": ["WATER"]}, {"market": [["BUY_SEED", "CARROT", 1]]}]) # == env.step
obs = game.observation(0, structified=True) # what kaggle hands agent 0 (obs.player, obs["farms"], ...)
game.statuses, game.rewards, game.current_step, game.render(), game.record()
# Whole games: named Rust agents run with the GIL released; any Python
# callable agent(obs[, config]) works too, e.g. the original kaggriculture agents.
finished = ke.run_episode(["starter", my_python_agent], {"seed": 42})
results = ke.run_batch(range(10_000), ("starter", "random:1")) # parallel, GIL released
step mirrors env.step exactly:
- An action can be any Python value, and each entry can instead be an exception
instance, which marks that agent ERROR (a
DeadlineExceededmarks it TIMEOUT). - When the original interpreter would raise,
stepraises the same exception class (TypeError,ValueError,OverflowErrororZeroDivisionError) and leaves the game unchanged. - Stepping a finished game raises
ke.FailedPrecondition. - Tuples, numpy scalars,
Decimals, sets, bytes and dicts with non-str keys behave as they do in kaggle.
The extension doesn't need kaggle-environments at runtime.
Build and run the CLI
cargo build --release
B=target/release/kaggriculture_engine
# Agent-driven episode (pass | starter | random[:SEED]); one JSON record per state
echo '{"seed": 42}' | $B play - starter random:7 [--render]
# Replay recorded inputs: {"configuration": {...}, "steps": [[in0, in1], ...]}
# where in = {"action": <any JSON>} | {"status": "ERROR"} | {"status": "TIMEOUT"}
$B replay scenario.json [--render]
# Throughput (single thread, then all cores via Rayon)
$B bench --episodes 8000 --agents starter,random:1 [--threads N]
Library use:
use kaggriculture_engine::{Action, AgentInput, Config, Game};
use std::sync::Arc;
let cfg = Arc::new(Config::from_json(&serde_json::json!({"seed": 42}))?);
let mut game = Game::new(cfg.clone());
while !game.done() {
let a = Action::from_json(&serde_json::json!({"farmer": ["WATER"]}), cfg.max_orders);
game.step(&[AgentInput::Act(a.clone()), AgentInput::Act(a)])?; // Err = Python raised; state untouched
}
batch::run_batch plays many seeds in parallel. To time a single game end to end
(cold and warm, with and without serialising every state), run
cargo run --release --example one_game.
Verifying bit-exactness
The Python side needs the project venv (uv sync).
.venv/bin/python tests/parity/run_parity.py # full sweep (~146 cases, ~30k records)
cargo test --release # unit tests + a 14-case parity slice
tests/python/test_bindings.py (.venv/bin/python -m unittest discover -s tests/python) checks the Python bindings against the same reference. It replays
fuzzed scenarios through Game.step, drives the original Python agents with the
bindings' observations, and checks that Python-only action values and agent exceptions
behave identically.
tests/parity/reference.py runs the original kaggriculture.py through the real
kaggle Environment (make → env.step). tests/parity/fuzz.py plays both seats
with a seeded, state-aware policy, so games reach crops, animals, hands, land, shops,
weeds and decay. It also injects malformed input: unhashable items, int()-hostile
counts ("abc", "1_0", "٣", 1e400), non-dict actions, agent ERROR/TIMEOUT
statuses, and hostile marketParams and configs. The Rust binary replays the exact
same JSON text, and every output line must be byte-identical. Mismatching inputs are
saved to tests/parity/failures/.
As a check on the harness itself, I planted small bugs (care-bonus arithmetic, round-half-up instead of Python's round-half-even) and the sweep flagged each of them.
Performance (Apple M1, 8 cores; 720-step episodes)
| agents | Python (kaggle env.run) |
Rust, 1 thread | Rust, 8 threads (Rayon) |
|---|---|---|---|
| starter / starter | ~1.2 s | 137 µs | 21 µs |
| random / random | ~1.0–1.3 s | 690 µs | 107 µs |
From Python, with starter vs starter:
ke.run_episodewith named agents takes 0.09 ms per game.- The original Python agents, called through
ke.run_episode, take 29 ms per game. That's still 29× faster than kaggle'senv.run, and building the observation dicts is now the bottleneck. - A plain Python loop costs 0.65 µs per
Game.step.
The Python figures include kaggle's framework overhead (it deep-copies the state every step), which is how the game is normally run.
Where the time went and what was done about it:
- Rayon runs episodes in parallel (
batch.rs). A single step (~150 ns) is far too small to split across threads. - SIMD (NEON): CPython's
random.Random(seed)(MT19937init_by_array) is a long serial multiply chain. The interpreter builds a new generator every in-game day, andrandom_agentbuilds one every turn. All of these seeds are known ahead of time, sopyrandom::LaneBatchseeds 32 of them at once across NEON lanes. That is 28× faster per generator than scalar. It also twists and tempers lazily: the first 227 outputs depend only on the untwisted state. The single-state twist and tempering also use NEON. Other architectures fall back to auto-vectorised lane arrays. - Price curves are memoised per inventory (sqrt/log calls were half of the step time), the decay scan is skipped until a plant can decay, the shed total is kept incrementally, and inventories are allocation-free ordered maps.
- Steps that cannot raise run in place. A step that might raise (malformed actions,
marketParamsoverrides, …) runs on a copy, matching kaggle's behaviour of discarding the state when the interpreter raises.
Layout
| file | role |
|---|---|
engine.rs |
interpreter() / _initialize and the env.step bookkeeping (step, status, reward, rollback) |
market.rs |
market_price / _shape with Python's exact int/float semantics |
action.rs |
typed actions; Python exceptions are kept as lazy errors that fire where Python would raise |
pyvalue.rs |
JSON as Python sees it: unbounded ints, int() coercion, json.dumps-exact output |
pyrandom.rs, simd.rs |
CPython-compatible MT19937, scalar and 32-lane SIMD |
bigint.rs |
minimal big integers (huge seeds, hire costs, exact int/int division) |
config.rs |
kaggle schema defaults and validation, and the interpreter's coercions |
agents.rs, render.rs, batch.rs |
bundled agents, text renderer, parallel runner |
python.rs, python/kaggriculture_engine/ |
PyO3 bindings (feature python), package and type stubs |
Known divergences (all outside realistic inputs)
marketParamsintegers beyond ±2⁶²,boardSize> 1024 andturnsPerDay> 2⁵⁸ are rejected at config time. Python would accept them.- Inputs must be standard JSON. Python's
jsonadditionally acceptsNaN/Infinityliterals and lone surrogates. - Exceptions are reported by class (
TypeError, …), not by message text. random_agenttakes an explicit seed; the Python original seeds from OS entropy on every call. The parity harness patches Python to use the same seeds. With no configured seed, both sides pick a random episode seed, so those runs aren't comparable.- Interpreter stdout (warnings) is returned in full; kaggle truncates logs at
maxLogLength(10,000 chars per step). - The renderer prints non-ASCII characters verbatim; CPython escapes the few
non-printable ones. This only matters for strings inside
marketParams. html_renderer(which only serves a bundled HTML file) is not ported.- Python bindings, callable agents: there is no act-timeout or overage-time
accounting, and observations omit kaggle's framework-only
remainingOverageTime. Observations use the package's ownStructrather than kaggle's class (same behaviour, differentisinstance). Nested dicts in the configuration passed to agents are left as plain dicts.
License
Licensed under the Apache License, Version 2.0.
This project is a port of kaggriculture.py from
kaggle-environments (Copyright 2020
Kaggle Inc, Apache-2.0). See NOTICE for attribution, including the CPython
algorithms reimplemented for bit-exact compatibility.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kaggriculture_engine-0.1.0.tar.gz.
File metadata
- Download URL: kaggriculture_engine-0.1.0.tar.gz
- Upload date:
- Size: 266.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63856ef07e80aaa132ed658b55671a695bf836af0dd4dcb6548d0e0801eef9d4
|
|
| MD5 |
9d9a18fbf04db9b70140cf8a417f5b5b
|
|
| BLAKE2b-256 |
5402906b2d2f5de1ecc9c1ae7f06bd2b50ca54e1a22823ee3c5598426cc4da6c
|
File details
Details for the file kaggriculture_engine-0.1.0-cp313-abi3-win_arm64.whl.
File metadata
- Download URL: kaggriculture_engine-0.1.0-cp313-abi3-win_arm64.whl
- Upload date:
- Size: 286.7 kB
- Tags: CPython 3.13+, Windows ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb416b500d9bf464d9871321fb9aa5793235eed234544a65bae0e23aed160a2e
|
|
| MD5 |
8df2f1d0c30707e79be457fcc8222392
|
|
| BLAKE2b-256 |
529f18b91e7d038a42c1ace5b1c956d41d05ba313d5194248c4a34e11265e12c
|
File details
Details for the file kaggriculture_engine-0.1.0-cp313-abi3-win_amd64.whl.
File metadata
- Download URL: kaggriculture_engine-0.1.0-cp313-abi3-win_amd64.whl
- Upload date:
- Size: 298.9 kB
- Tags: CPython 3.13+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5eb49401e8599c05c885d3276ae2c5c04d85b51d6eadbbebce21b732b27b656
|
|
| MD5 |
83157d2c4ad7bc0ed79dc09ddf903fbf
|
|
| BLAKE2b-256 |
2ea2da9e6629bc5aec615594923cc308617a0fa655d3734dc88c81af5c7ed2cc
|
File details
Details for the file kaggriculture_engine-0.1.0-cp313-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: kaggriculture_engine-0.1.0-cp313-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 422.4 kB
- Tags: CPython 3.13+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9cae78ae216c948c16608edfcdd6b9598c94bda01454c594fe29a8a292e79706
|
|
| MD5 |
005251a0783bd41c817de6903cac3def
|
|
| BLAKE2b-256 |
9bb62b54ba4e4efbb080073af67a38b152c2a280d53b0b2ccb4b2e84cecb3c6a
|
File details
Details for the file kaggriculture_engine-0.1.0-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: kaggriculture_engine-0.1.0-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 417.7 kB
- Tags: CPython 3.13+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b5439a1d1c4c67311121c8732bd343ffae63bafc87d12f89e068599668e87da
|
|
| MD5 |
792578cab91bc74efdf83207c4aa753a
|
|
| BLAKE2b-256 |
226a5de9618da05f288123b68f182e7ff8dfcfe062c961dcc4f51915004150e5
|
File details
Details for the file kaggriculture_engine-0.1.0-cp313-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: kaggriculture_engine-0.1.0-cp313-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 379.3 kB
- Tags: CPython 3.13+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c8c826bec1360375ce0c0553eb9dd81e15dc5cfd9cb164ae433a1dc657c8e98
|
|
| MD5 |
9e8fffd29edd9fcc00d5ddfd762afa09
|
|
| BLAKE2b-256 |
4432e5e6ef28696936b8043317cc71ac277a2aa0f95d2e9d8f4c1130e3fe5074
|