{"id":49052408,"url":"https://github.com/urmzd/gymnasia","last_synced_at":"2026-04-19T21:13:35.643Z","repository":{"id":340913088,"uuid":"1153112416","full_name":"urmzd/gymnasia","owner":"urmzd","description":"OpenAI Gymnasium environments in pure Rust.","archived":false,"fork":false,"pushed_at":"2026-04-16T02:46:53.000Z","size":3960,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-16T04:33:47.156Z","etag":null,"topics":["gymnasium","openai-gym","reinforcement-learning","rust","simulation"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"MathisWellmann/gym-rs","license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/urmzd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-08T22:55:51.000Z","updated_at":"2026-04-16T02:46:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/urmzd/gymnasia","commit_stats":null,"previous_names":["urmzd/gymnasia"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/urmzd/gymnasia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urmzd%2Fgymnasia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urmzd%2Fgymnasia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urmzd%2Fgymnasia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urmzd%2Fgymnasia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/urmzd","download_url":"https://codeload.github.com/urmzd/gymnasia/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urmzd%2Fgymnasia/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32022799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"online","status_checked_at":"2026-04-19T02:00:07.110Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gymnasium","openai-gym","reinforcement-learning","rust","simulation"],"created_at":"2026-04-19T21:13:33.182Z","updated_at":"2026-04-19T21:13:35.637Z","avatar_url":"https://github.com/urmzd.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ch1 align=\"center\"\u003egymnasia\u003c/h1\u003e\n  \u003cp align=\"center\"\u003e\n    OpenAI Gymnasium environments in pure Rust.\n    \u003cbr /\u003e\u003cbr /\u003e\n    \u003ca href=\"https://github.com/urmzd/gymnasia/releases\"\u003eInstall\u003c/a\u003e\n    \u0026middot;\n    \u003ca href=\"https://github.com/urmzd/gymnasia/issues\"\u003eReport Bug\u003c/a\u003e\n    \u0026middot;\n    \u003ca href=\"https://crates.io/crates/gymnasia\"\u003eCrates.io\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/urmzd/gymnasia/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/urmzd/gymnasia/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://crates.io/crates/gymnasia\"\u003e\u003cimg src=\"https://img.shields.io/crates/v/gymnasia\" alt=\"crates.io\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://docs.rs/gymnasia\"\u003e\u003cimg src=\"https://img.shields.io/docsrs/gymnasia\" alt=\"docs.rs\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/urmzd/gymnasia\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## Showcase\n\n\u003ctable align=\"center\"\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\n      \u003cimg src=\"showcase/cartpole.gif\" alt=\"CartPole\" width=\"300\" /\u003e\n      \u003cbr /\u003e\n      \u003csub\u003e\u003cb\u003eCartPole\u003c/b\u003e\u003c/sub\u003e\n    \u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\n      \u003cimg src=\"showcase/mountain-car.gif\" alt=\"MountainCar\" width=\"300\" /\u003e\n      \u003cbr /\u003e\n      \u003csub\u003e\u003cb\u003eMountainCar\u003c/b\u003e\u003c/sub\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\" colspan=\"2\"\u003e\n      \u003cimg src=\"showcase/cartpole-headless.gif\" alt=\"CartPole (headless)\" width=\"300\" /\u003e\n      \u003cbr /\u003e\n      \u003csub\u003e\u003cb\u003eCartPole (headless)\u003c/b\u003e\u003c/sub\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n## Contents\n\n- [Architecture](#architecture)\n- [Quick Start](#quick-start)\n- [Wrappers](#wrappers)\n- [Spaces](#spaces)\n- [Feature Flags](#feature-flags)\n- [Benchmarks](#benchmarks)\n- [Migrating from v2](#migrating-from-v2)\n- [History](#history)\n- [Contributing](#contributing)\n- [Agent Skill](#agent-skill)\n- [License](#license)\n\n## Architecture\n\nUnlike Python Gymnasium, gymnasia **separates simulation from rendering** and\nuses **pure-Rust dependencies only** — no C bindings, no SDL2, no system library\ninstallation.\n\n| Layer | What it does | Feature gate |\n|-------|-------------|--------------|\n| `Env` trait | Pure physics — `step()`, `reset()` | Always compiled |\n| `Renderable` trait | Produces a `DrawList` (backend-agnostic draw commands) | Always compiled |\n| `Wrapper` trait | Composable behavior wrappers (`TimeLimit`, `NormalizeObservation`, etc.) | Always compiled |\n| `Screen` | Translates `DrawList` into graphics calls (macroquad) | `render` feature |\n| `RenderEnv\u003cE\u003e` | Wraps `Env + Renderable` with a `Screen` — implements `Env` | `render` feature |\n\n### Design decisions\n\n- **`Env` has zero supertraits** — no `Clone + Debug + Serialize`. Wrappers with closures work.\n- **Wrappers own their data** — no info dict. `RecordEpisodeStatistics` exposes `episode_return()`, `TimeLimit` exposes `steps_remaining()`.\n- **`BoxSpace\u003cB: Bounded\u003e`** — generic over the bounds representation. Implement `Bounded` on your own types.\n- **`Flatten` is opt-in** — bidirectional `flatten()`/`unflatten()` for ML pipelines. Not required by `Env`.\n- **`StepResult` uses `f64` reward** — `O64` stays internal.\n\n## Quick Start\n\n```bash\ncargo add gymnasia\n```\n\nHeadless by default — no graphics dependencies. To enable rendering:\n\n```bash\ncargo add gymnasia --features render\n```\n\n### Development\n\n```bash\njust fetch     # fetch dependencies\njust build     # build the project\njust fmt       # format code\njust lint      # run clippy\njust check     # run all CI checks (fmt + lint + test)\n```\n\n### Headless\n\n```rust\nuse gymnasia::core::Env;\nuse gymnasia::envs::classical_control::cartpole::CartPoleEnv;\n\nlet mut env = CartPoleEnv::new();\nenv.reset(None, None);\nlet result = env.step(1); // 0 = left, 1 = right\nprintln!(\"reward: {}, terminated: {}\", result.reward, result.terminated);\n```\n\n```bash\ncargo run --example=cartpole_headless --no-default-features\n```\n\n### With wrappers\n\n```rust\nuse gymnasia::core::Env;\nuse gymnasia::envs::classical_control::cartpole::CartPoleEnv;\nuse gymnasia::wrappers::{TimeLimit, RecordEpisodeStatistics};\n\nlet env = CartPoleEnv::new();\nlet env = TimeLimit::new(env, 500);\nlet mut env = RecordEpisodeStatistics::new(env);\n// Type: RecordEpisodeStatistics\u003cTimeLimit\u003cCartPoleEnv\u003e\u003e\n\nlet obs = env.reset(None, None);\nlet result = env.step(1);\n\n// Typed access to wrapper data:\nprintln!(\"return: {}\", env.episode_return());\nprintln!(\"steps left: {}\", env.inner().steps_remaining());\n```\n\n### Flatten for ML\n\n```rust\nuse gymnasia::core::{Env, Flatten};\nuse gymnasia::envs::classical_control::cartpole::CartPoleEnv;\n\nlet mut env = CartPoleEnv::new();\nlet obs = env.reset(None, None);\nlet flat: Vec\u003cf64\u003e = obs.flatten();   // [x, x_dot, theta, theta_dot]\nassert_eq!(flat.len(), 4);\n```\n\n### With rendering\n\n```rust\nuse gymnasia::core::Env;\nuse gymnasia::render::{RenderEnv, renderer::RenderMode};\nuse gymnasia::envs::classical_control::cartpole::CartPoleEnv;\n\n#[macroquad::main(\"CartPole\")]\nasync fn main() {\n    let env = CartPoleEnv::new();\n    let mut renv = RenderEnv::new(env, RenderMode::Human);\n    renv.reset(None, None);\n    loop {\n        let result = renv.step(1);\n        renv.next_frame().await;\n        if result.terminated { break; }\n    }\n}\n```\n\n```bash\ncargo run --example=cartpole --features render\ncargo run --example=mountain_car --features render\n```\n\n### Custom environment\n\n```rust\nuse gymnasia::core::{Env, StepResult};\nuse gymnasia::spaces::{Bounded, BoxSpace, Discrete};\n\n// 1. Define your observation type\n#[derive(Clone, Debug)]\nstruct MyObs { x: f64, y: f64 }\n\n// 2. Implement Bounded so it works with BoxSpace\nimpl Bounded for MyObs {\n    fn in_bounds(v: \u0026Self, lo: \u0026Self, hi: \u0026Self) -\u003e bool {\n        v.x \u003e= lo.x \u0026\u0026 v.x \u003c= hi.x \u0026\u0026 v.y \u003e= lo.y \u0026\u0026 v.y \u003c= hi.y\n    }\n    fn sample_uniform\u003cR: rand::Rng\u003e(rng: \u0026mut R, lo: \u0026Self, hi: \u0026Self) -\u003e Self {\n        MyObs { x: rng.gen_range(lo.x..=hi.x), y: rng.gen_range(lo.y..=hi.y) }\n    }\n    fn clamp(v: Self, lo: \u0026Self, hi: \u0026Self) -\u003e Self {\n        MyObs { x: v.x.clamp(lo.x, hi.x), y: v.y.clamp(lo.y, hi.y) }\n    }\n}\n\n// 3. Implement Env\nstruct MyEnv { /* ... */ }\nimpl Env for MyEnv {\n    type Action = i64;\n    type Observation = MyObs;\n    type ActionSpace = Discrete;\n    type ObservationSpace = BoxSpace\u003cMyObs\u003e;\n    type ResetOptions = ();\n    // ...\n#   fn step(\u0026mut self, _: i64) -\u003e StepResult\u003cMyObs\u003e { todo!() }\n#   fn reset(\u0026mut self, _: Option\u003cu64\u003e, _: ()) -\u003e MyObs { todo!() }\n#   fn action_space(\u0026self) -\u003e \u0026Discrete { todo!() }\n#   fn observation_space(\u0026self) -\u003e \u0026BoxSpace\u003cMyObs\u003e { todo!() }\n}\n```\n\nFull API documentation is available on [docs.rs](https://docs.rs/gymnasia).\n\n## Wrappers\n\nWrappers are generic structs that implement `Env` by delegating to an inner environment. Stack them to compose behaviors:\n\n| Wrapper | Category | What it does |\n|---------|----------|-------------|\n| `TimeLimit` | Common | Truncates after N steps |\n| `OrderEnforcing` | Common | Panics if `step()` before `reset()` |\n| `Autoreset` | Common | Auto-resets on termination |\n| `RecordEpisodeStatistics` | Common | Tracks cumulative reward and episode length |\n| `ClipReward` | Reward | Clamps reward to `[min, max]` |\n| `NormalizeReward` | Reward | Running mean/variance normalization |\n| `TransformReward` | Reward | Apply custom `Fn(f64) -\u003e f64` |\n| `ClipAction` | Action | Clamps to action space bounds |\n| `RescaleAction` | Action | Affine rescaling of `f64` actions |\n| `TransformAction` | Action | Apply custom function, may change type |\n| `FlattenObservation` | Observation | Flattens via `Flatten` trait to `Vec\u003cf64\u003e` |\n| `NormalizeObservation` | Observation | Running mean/variance normalization |\n| `TransformObservation` | Observation | Apply custom function, may change type |\n\nWrappers that track metadata expose it via typed methods on the wrapper itself — there is no dynamic info dict.\n\n## Spaces\n\n| Space | Element | Description |\n|-------|---------|-------------|\n| `BoxSpace\u003cB: Bounded\u003e` | `B` | Continuous bounded space. Generic over bounds type. |\n| `Discrete` | `i64` | `{start, ..., start+n-1}`. Supports action masking. |\n| `MultiDiscrete` | `Vec\u003ci64\u003e` | Cartesian product of discrete spaces. |\n| `MultiBinary` | `Vec\u003cu8\u003e` | `{0, 1}^n`. |\n\nImplement `Bounded` on any type to use it with `BoxSpace`. We ship implementations for `f64`, `f32`, and `Tensor` (flat `Vec\u003cf64\u003e` with shape metadata for high-dimensional spaces like images).\n\n## Feature Flags\n\n| Feature | Default | Description |\n|---------|---------|-------------|\n| `render` | No | macroquad-based window rendering and pixel capture |\n\n## Benchmarks\n\n\u003c!-- fsrc src=\"benches/RESULTS.md\" --\u003e\n| Benchmark | Time (median) |\n|-----------|---------------|\n| `cartpole/step` | ~26 ns |\n| `cartpole/reset` | ~21 ns |\n| `cartpole/episode` | ~248 ns |\n| `mountain_car/step` | ~25 ns |\n| `mountain_car/reset` | ~15 ns |\n| `mountain_car/episode` | ~4.6 us |\n\n\u003e Apple M3 Pro — `cargo bench` via [Criterion](https://github.com/bheisler/criterion.rs). Run `cargo bench` to reproduce.\n\u003c!-- /fsrc --\u003e\n\n## Migrating from v2\n\nSee [CHANGELOG.md](./CHANGELOG.md) for the full list. Key changes:\n\n| v2 | v3 |\n|----|-----|\n| `ActionReward\u003cT, E\u003e` | `StepResult\u003cO\u003e` |\n| `reward: O64` | `reward: f64` |\n| `info: Option\u003cE\u003e` | removed — wrappers own their data |\n| `reset(seed, return_info, options)` | `reset(seed, options)` returns `Observation` |\n| `BoxR\u003cT\u003e` | `BoxSpace\u003cB: Bounded\u003e` |\n| `Observation: Into\u003cVec\u003cf64\u003e\u003e` required | `Flatten` trait (opt-in) |\n| `Clone + Debug + Serialize` on `Env` | no supertraits |\n| `Discrete(usize)` | `Discrete { n, start }` with `i64` element |\n| `DiscreteRange` | merged into `Discrete::with_start()` |\n| `RenderEnv` doesn't impl `Env` | `RenderEnv` implements `Env` |\n| No wrappers | 13 composable wrappers |\n| `use gymnasia::render::RenderEnv` | `use gymnasia::render::RenderEnv` |\n| `use gymnasia::utils::renderer::RenderMode` | `use gymnasia::render::renderer::RenderMode` |\n\n## History\n\nGymnasia is a fork of\n[MathisWellmann/gym-rs](https://github.com/MathisWellmann/gym-rs), which is no\nlonger actively maintained. OpenAI Gym itself has since evolved into\n[Gymnasium](https://github.com/Farama-Foundation/Gymnasium) — gymnasia tracks\nthat direction for Rust. See [docs/roadmap.md](./docs/roadmap.md) for feature parity status.\n\n## Contributing\n\nContributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.\n\n## Agent Skill\n\nThis repo's conventions are available as portable agent skills in [`skills/`](skills/).\n\n## License\n\nLicensed under [Apache 2.0](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Furmzd%2Fgymnasia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Furmzd%2Fgymnasia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Furmzd%2Fgymnasia/lists"}