From 0024843a5c1bb4187906072b301fd8a1dea210af Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sat, 17 Jan 2026 20:42:59 +0100 Subject: [PATCH 1/4] chore:add beads --- .beads/.gitignore | 44 +++++++++++++++++++++ .beads/README.md | 81 +++++++++++++++++++++++++++++++++++++++ .beads/config.yaml | 62 ++++++++++++++++++++++++++++++ .beads/interactions.jsonl | 0 .beads/issues.jsonl | 0 .beads/metadata.json | 4 ++ .gitattributes | 3 ++ AGENTS.md | 40 +++++++++++++++++++ doc/ai/history/beads.md | 24 ++++++++++++ 9 files changed, 258 insertions(+) create mode 100644 .beads/.gitignore create mode 100644 .beads/README.md create mode 100644 .beads/config.yaml create mode 100644 .beads/interactions.jsonl create mode 100644 .beads/issues.jsonl create mode 100644 .beads/metadata.json create mode 100644 .gitattributes create mode 100644 AGENTS.md create mode 100644 doc/ai/history/beads.md diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000..d27a1db --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,44 @@ +# SQLite databases +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm + +# Daemon runtime files +daemon.lock +daemon.log +daemon.pid +bd.sock +sync-state.json +last-touched + +# Local version tracking (prevents upgrade notification spam after git ops) +.local_version + +# Legacy database files +db.sqlite +bd.db + +# Worktree redirect file (contains relative path to main repo's .beads/) +# Must not be committed as paths would be wrong in other clones +redirect + +# Merge artifacts (temporary files from 3-way merge) +beads.base.jsonl +beads.base.meta.json +beads.left.jsonl +beads.left.meta.json +beads.right.jsonl +beads.right.meta.json + +# Sync state (local-only, per-machine) +# These files are machine-specific and should not be shared across clones +.sync.lock +sync_base.jsonl + +# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here. +# They would override fork protection in .git/info/exclude, allowing +# contributors to accidentally commit upstream issue databases. +# The JSONL files (issues.jsonl, interactions.jsonl) and config files +# are tracked by git by default since no pattern above ignores them. diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 0000000..50f281f --- /dev/null +++ b/.beads/README.md @@ -0,0 +1,81 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --status in_progress +bd update --status done + +# Sync with git remote +bd sync +``` + +### Working with Issues + +Issues in Beads are: +- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Always in sync**: Auto-syncs with your commits + +## Why Beads? + +✨ **AI-Native Design** +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +🚀 **Developer Focused** +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +🔧 **Git Integration** +- Automatic sync with git commits +- Branch-aware issue tracking +- Intelligent JSONL merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +*Beads: Issue tracking that moves at the speed of thought* ⚡ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..1de3590 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,62 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: load from JSONL, no SQLite, write back after each command +# When true, bd will use .beads/issues.jsonl as the source of truth +# instead of SQLite database +# no-db: false + +# Disable daemon for RPC communication (forces direct database access) +# no-daemon: false + +# Disable auto-flush of database to JSONL after mutations +# no-auto-flush: false + +# Disable auto-import from JSONL when it's newer than database +# no-auto-import: false + +# Enable JSON output by default +# json: false + +# Default actor for audit trails (overridden by BD_ACTOR or --actor) +# actor: "" + +# Path to database (overridden by BEADS_DB or --db) +# db: "" + +# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON) +# auto-start-daemon: true + +# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE) +# flush-debounce: "5s" + +# Git branch for beads commits (bd sync will commit to this branch) +# IMPORTANT: Set this for team projects so all clones use the same sync branch. +# This setting persists across clones (unlike database config which is gitignored). +# Can also use BEADS_SYNC_BRANCH env var for local override. +# If not set, bd sync will require you to run 'bd config set sync.branch '. +sync-branch: "beads-sync" + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct JSONL +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo \ No newline at end of file diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..c787975 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..807d598 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +# Use bd merge for beads JSONL files +.beads/issues.jsonl merge=beads diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..df7a4af --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Agent Instructions + +This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. + +## Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --status in_progress # Claim work +bd close # Complete work +bd sync # Sync with git +``` + +## Landing the Plane (Session Completion) + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd sync + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + diff --git a/doc/ai/history/beads.md b/doc/ai/history/beads.md new file mode 100644 index 0000000..dad9923 --- /dev/null +++ b/doc/ai/history/beads.md @@ -0,0 +1,24 @@ +```sh +❯ bd init + Repository ID: d5459d4d + Clone ID: 73ab432945c43882 + ✓ Created AGENTS.md with landing-the-plane instructions + +✓ bd initialized successfully! + + Database: .beads/beads.db + Issue prefix: trictrac + Issues will be named: trictrac- (e.g., trictrac-a3f2dd) + +Run bd quickstart to get started. + +⚠ Setup incomplete. Some issues were detected: + • Git Hooks: Missing 1 recommended hook(s) + • Sync Divergence: 1 sync divergence issue(s) detected + • Claude Integration: Not configured + • Git Working Tree: Uncommitted changes present + • Version Tracking: Version tracking not initialized + • Sync Branch Config: sync-branch not configured + +Run bd doctor --fix to see details and fix these issues. +``` From 8be107129165793f3e92a6951a44c955c4ac661a Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sat, 10 Jan 2026 21:01:35 +0100 Subject: [PATCH 2/4] python bindings --- Cargo.lock | 79 ++++++++++ bot/pyproject.toml | 9 ++ bot/python/test.py | 4 + devenv.lock | 24 +-- devenv.nix | 25 ++++ doc/python.md | 31 ++++ justfile | 1 + store/Cargo.toml | 5 +- store/src/lib.rs | 3 + store/src/player.rs | 2 + store/src/pyengine.rs | 337 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 507 insertions(+), 13 deletions(-) create mode 100644 bot/pyproject.toml create mode 100644 bot/python/test.py create mode 100644 doc/python.md create mode 100644 store/src/pyengine.rs diff --git a/Cargo.lock b/Cargo.lock index a71f75a..de74a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3460,6 +3460,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "merge" version = "0.1.0" @@ -4210,6 +4219,69 @@ dependencies = [ "num-traits", ] +[[package]] +name = "pyo3" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.106", +] + [[package]] name = "qoi" version = "0.4.1" @@ -5154,6 +5226,7 @@ dependencies = [ "base64 0.21.7", "log", "merge", + "pyo3", "rand 0.8.5", "serde", "transpose", @@ -5892,6 +5965,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/bot/pyproject.toml b/bot/pyproject.toml new file mode 100644 index 0000000..8fe5762 --- /dev/null +++ b/bot/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) +features = ["pyo3/extension-module"] +# python-source = "python" +# module-name = "trictrac.game" diff --git a/bot/python/test.py b/bot/python/test.py new file mode 100644 index 0000000..7b13b7d --- /dev/null +++ b/bot/python/test.py @@ -0,0 +1,4 @@ +import store + +game = store.TricTrac() +print(game.get_state_dict()) diff --git a/devenv.lock b/devenv.lock index c3d5629..f30fbdc 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1753667201, + "lastModified": 1768056019, "owner": "cachix", "repo": "devenv", - "rev": "4d584d7686a50387f975879788043e55af9f0ad4", + "rev": "9bfc4a64c3a798ed8fa6cee3a519a9eac5e73cb5", "type": "github" }, "original": { @@ -19,14 +19,14 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1747046372, - "owner": "edolstra", + "lastModified": 1767039857, + "owner": "NixOS", "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -40,10 +40,10 @@ ] }, "locked": { - "lastModified": 1750779888, + "lastModified": 1767281941, "owner": "cachix", "repo": "git-hooks.nix", - "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", + "rev": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa", "type": "github" }, "original": { @@ -60,10 +60,10 @@ ] }, "locked": { - "lastModified": 1709087332, + "lastModified": 1762808025, "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", "type": "github" }, "original": { @@ -74,10 +74,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1753432016, + "lastModified": 1767995494, "owner": "NixOS", "repo": "nixpkgs", - "rev": "6027c30c8e9810896b92429f0092f624f7b1aace", + "rev": "45a1530683263666f42d1de4cdda328109d5a676", "type": "github" }, "original": { diff --git a/devenv.nix b/devenv.nix index 1b51c9d..af6f116 100644 --- a/devenv.nix +++ b/devenv.nix @@ -15,6 +15,12 @@ pkgs.samply # code profiler pkgs.feedgnuplot # to visualize bots training results + # --- AI training with python --- + # generate python classes from rust code + pkgs.maturin + # required by python numpy + pkgs.libz + # for bevy pkgs.alsa-lib pkgs.udev @@ -47,6 +53,25 @@ # https://devenv.sh/languages/ languages.rust.enable = true; + + # AI training with python + enterShell = '' + PYTHONPATH=$PYTHONPATH:$PWD/.devenv/state/venv/lib/python3/site-packages + ''; + + languages.python = { + enable = true; + uv.enable = true; + venv.enable = true; + venv.requirements = " + pip + gymnasium + numpy + stable-baselines3 + shimmy + "; + }; + # https://devenv.sh/scripts/ # scripts.hello.exec = "echo hello from $GREET"; diff --git a/doc/python.md b/doc/python.md new file mode 100644 index 0000000..65b0239 --- /dev/null +++ b/doc/python.md @@ -0,0 +1,31 @@ +# Python bindings + +## Génération bindings + +```sh +# Generate trictrac python lib as a wheel +maturin build -m store/Cargo.toml --release +# Install wheel in local python env +pip install --no-deps --force-reinstall --prefix .devenv/state/venv target/wheels/*.whl +``` + +## Usage + +Pour vérifier l'accès à la lib : lancer le shell interactif `python` + +```python +Python 3.13.11 (main, Dec 5 2025, 16:06:33) [GCC 15.2.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import store +>>> game = store.TricTrac() +>>> game.get_active_player_id() +1 +``` + +### Appels depuis python + +`python bot/python/test.py` + +## Interfaces + +## Entraînement diff --git a/justfile b/justfile index 9c8bf58..33c0654 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,7 @@ profile: cargo build --profile profiling samply record ./target/profiling/client_cli --bot dummy,dummy pythonlib: + rm -rf target/wheels maturin build -m store/Cargo.toml --release pip install --no-deps --force-reinstall --prefix .devenv/state/venv target/wheels/*.whl trainbot algo: diff --git a/store/Cargo.toml b/store/Cargo.toml index a071dd1..0517553 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -7,14 +7,17 @@ edition = "2021" [lib] name = "store" +# "cdylib" is necessary to produce a shared library for Python to import from. # Only "rlib" is needed for other Rust crates to use this library -crate-type = ["rlib"] +crate-type = ["cdylib", "rlib"] [dependencies] base64 = "0.21.7" # provides macros for creating log messages to be used by a logger (for example env_logger) log = "0.4.20" merge = "0.1.0" +# generate python lib (with maturin) to be used in AI training +pyo3 = { version = "0.23", features = ["extension-module", "abi3-py38"] } rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } transpose = "0.2.2" diff --git a/store/src/lib.rs b/store/src/lib.rs index 58a5727..60639e5 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -16,3 +16,6 @@ pub use board::CheckerMove; mod dice; pub use dice::{Dice, DiceRoller}; + +// python interface "trictrac_engine" (for AI training..) +mod pyengine; diff --git a/store/src/player.rs b/store/src/player.rs index d990a1f..eeb5829 100644 --- a/store/src/player.rs +++ b/store/src/player.rs @@ -1,9 +1,11 @@ +use pyo3::prelude::*; use serde::{Deserialize, Serialize}; use std::fmt; // This just makes it easier to dissern between a player id and any ol' u64 pub type PlayerId = u64; +#[pyclass(eq, eq_int)] #[derive(Copy, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Color { White, diff --git a/store/src/pyengine.rs b/store/src/pyengine.rs new file mode 100644 index 0000000..af2b650 --- /dev/null +++ b/store/src/pyengine.rs @@ -0,0 +1,337 @@ +//! # Expose trictrac game state and rules in a python module +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use crate::board::CheckerMove; +use crate::dice::Dice; +use crate::game::{GameEvent, GameState, Stage, TurnStage}; +use crate::game_rules_moves::MoveRules; +use crate::game_rules_points::PointsRules; +use crate::player::{Color, PlayerId}; + +#[pyclass] +struct TricTrac { + game_state: GameState, + dice_roll_sequence: Vec<(u8, u8)>, + current_dice_index: usize, +} + +#[pymethods] +impl TricTrac { + #[new] + fn new() -> Self { + let mut game_state = GameState::new(false); // schools_enabled = false + + // Initialiser 2 joueurs + game_state.init_player("player1"); + game_state.init_player("bot"); + + // Commencer la partie avec le joueur 1 + game_state.consume(&GameEvent::BeginGame { goes_first: 1 }); + + TricTrac { + game_state, + dice_roll_sequence: Vec::new(), + current_dice_index: 0, + } + } + + /// Obtenir l'état du jeu sous forme de chaîne de caractères compacte + fn get_state_id(&self) -> String { + self.game_state.to_string_id() + } + + /// Obtenir l'état du jeu sous forme de dictionnaire pour faciliter l'entrainement + fn get_state_dict(&self) -> PyResult> { + Python::with_gil(|py| { + let state_dict = PyDict::new(py); + + // Informations essentielles sur l'état du jeu + state_dict.set_item("active_player", self.game_state.active_player_id)?; + state_dict.set_item("stage", format!("{:?}", self.game_state.stage))?; + state_dict.set_item("turn_stage", format!("{:?}", self.game_state.turn_stage))?; + + // Dés + let (dice1, dice2) = self.game_state.dice.values; + state_dict.set_item("dice", (dice1, dice2))?; + + // Points des joueurs + if let Some(white_player) = self.game_state.get_white_player() { + state_dict.set_item("white_points", white_player.points)?; + state_dict.set_item("white_holes", white_player.holes)?; + } + + if let Some(black_player) = self.game_state.get_black_player() { + state_dict.set_item("black_points", black_player.points)?; + state_dict.set_item("black_holes", black_player.holes)?; + } + + // Positions des pièces + let white_positions = self.get_checker_positions(Color::White); + let black_positions = self.get_checker_positions(Color::Black); + + state_dict.set_item("white_positions", white_positions)?; + state_dict.set_item("black_positions", black_positions)?; + + // État compact pour la comparaison d'états + state_dict.set_item("state_id", self.game_state.to_string_id())?; + + Ok(state_dict.into()) + }) + } + + /// Renvoie les positions des pièces pour un joueur spécifique + fn get_checker_positions(&self, color: Color) -> Vec<(usize, i8)> { + self.game_state.board.get_color_fields(color) + } + + /// Obtenir la liste des mouvements légaux sous forme de paires (from, to) + fn get_available_moves(&self) -> Vec<((usize, usize), (usize, usize))> { + // L'agent joue toujours le joueur actif + let color = self + .game_state + .player_color_by_id(&self.game_state.active_player_id) + .unwrap_or(Color::White); + + // Si ce n'est pas le moment de déplacer les pièces, retourner une liste vide + if self.game_state.turn_stage != TurnStage::Move + && self.game_state.turn_stage != TurnStage::HoldOrGoChoice + { + return vec![]; + } + + let rules = MoveRules::new(&color, &self.game_state.board, self.game_state.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Convertir les mouvements CheckerMove en tuples (from, to) pour Python + possible_moves + .into_iter() + .map(|(move1, move2)| { + ( + (move1.get_from(), move1.get_to()), + (move2.get_from(), move2.get_to()), + ) + }) + .collect() + } + + /// Jouer un coup ((from1, to1), (from2, to2)) + fn play_move(&mut self, moves: ((usize, usize), (usize, usize))) -> bool { + let ((from1, to1), (from2, to2)) = moves; + + // Vérifier que c'est au tour du joueur de jouer + if self.game_state.turn_stage != TurnStage::Move + && self.game_state.turn_stage != TurnStage::HoldOrGoChoice + { + return false; + } + + let move1 = CheckerMove::new(from1, to1).unwrap_or_default(); + let move2 = CheckerMove::new(from2, to2).unwrap_or_default(); + + let event = GameEvent::Move { + player_id: self.game_state.active_player_id, + moves: (move1, move2), + }; + + // Vérifier si le mouvement est valide + if !self.game_state.validate(&event) { + return false; + } + + // Exécuter le mouvement + self.game_state.consume(&event); + + // Si l'autre joueur doit lancer les dés maintenant, simuler ce lancement + if self.game_state.turn_stage == TurnStage::RollDice { + self.roll_dice(); + } + + true + } + + /// Lancer les dés (soit aléatoirement, soit en utilisant une séquence prédéfinie) + fn roll_dice(&mut self) -> (u8, u8) { + // Vérifier que c'est au bon moment pour lancer les dés + if self.game_state.turn_stage != TurnStage::RollDice + && self.game_state.turn_stage != TurnStage::RollWaiting + { + return self.game_state.dice.values; + } + + // Simuler un lancer de dés + let dice_values = if !self.dice_roll_sequence.is_empty() + && self.current_dice_index < self.dice_roll_sequence.len() + { + // Utiliser la séquence prédéfinie + let dice = self.dice_roll_sequence[self.current_dice_index]; + self.current_dice_index += 1; + dice + } else { + // Générer aléatoirement + ( + (1 + (rand::random::() % 6)), + (1 + (rand::random::() % 6)), + ) + }; + + // Envoyer les événements appropriés + let roll_event = GameEvent::Roll { + player_id: self.game_state.active_player_id, + }; + + if self.game_state.validate(&roll_event) { + self.game_state.consume(&roll_event); + } + + let roll_result_event = GameEvent::RollResult { + player_id: self.game_state.active_player_id, + dice: Dice { + values: dice_values, + }, + }; + + if self.game_state.validate(&roll_result_event) { + self.game_state.consume(&roll_result_event); + } + + dice_values + } + + /// Marquer des points + fn mark_points(&mut self, points: u8) -> bool { + // Vérifier que c'est au bon moment pour marquer des points + if self.game_state.turn_stage != TurnStage::MarkPoints + && self.game_state.turn_stage != TurnStage::MarkAdvPoints + { + return false; + } + + let event = GameEvent::Mark { + player_id: self.game_state.active_player_id, + points, + }; + + // Vérifier si l'événement est valide + if !self.game_state.validate(&event) { + return false; + } + + // Exécuter l'événement + self.game_state.consume(&event); + + // Si l'autre joueur doit lancer les dés maintenant, simuler ce lancement + if self.game_state.turn_stage == TurnStage::RollDice { + self.roll_dice(); + } + + true + } + + /// Choisir de "continuer" (Go) après avoir gagné un trou + fn choose_go(&mut self) -> bool { + // Vérifier que c'est au bon moment pour choisir de continuer + if self.game_state.turn_stage != TurnStage::HoldOrGoChoice { + return false; + } + + let event = GameEvent::Go { + player_id: self.game_state.active_player_id, + }; + + // Vérifier si l'événement est valide + if !self.game_state.validate(&event) { + return false; + } + + // Exécuter l'événement + self.game_state.consume(&event); + + // Simuler le lancer de dés pour le prochain tour + self.roll_dice(); + + true + } + + /// Calcule les points maximaux que le joueur actif peut obtenir avec les dés actuels + fn calculate_points(&self) -> u8 { + let active_player = self + .game_state + .players + .get(&self.game_state.active_player_id); + + if let Some(player) = active_player { + let dice_roll_count = player.dice_roll_count; + let color = player.color; + + let points_rules = + PointsRules::new(&color, &self.game_state.board, self.game_state.dice); + let (points, _) = points_rules.get_points(dice_roll_count); + + points + } else { + 0 + } + } + + /// Réinitialise la partie + fn reset(&mut self) { + self.game_state = GameState::new(false); + + // Initialiser 2 joueurs + self.game_state.init_player("player1"); + self.game_state.init_player("bot"); + + // Commencer la partie avec le joueur 1 + self.game_state + .consume(&GameEvent::BeginGame { goes_first: 1 }); + + // Réinitialiser l'index de la séquence de dés + self.current_dice_index = 0; + } + + /// Vérifie si la partie est terminée + fn is_done(&self) -> bool { + self.game_state.stage == Stage::Ended || self.game_state.determine_winner().is_some() + } + + /// Obtenir le gagnant de la partie + fn get_winner(&self) -> Option { + self.game_state.determine_winner() + } + + /// Obtenir le score du joueur actif (nombre de trous) + fn get_score(&self, player_id: PlayerId) -> i32 { + if let Some(player) = self.game_state.players.get(&player_id) { + player.holes as i32 + } else { + -1 + } + } + + /// Obtenir l'ID du joueur actif + fn get_active_player_id(&self) -> PlayerId { + self.game_state.active_player_id + } + + /// Définir une séquence de dés à utiliser (pour la reproductibilité) + fn set_dice_sequence(&mut self, sequence: Vec<(u8, u8)>) { + self.dice_roll_sequence = sequence; + self.current_dice_index = 0; + } + + /// Afficher l'état du jeu (pour le débogage) + fn __str__(&self) -> String { + format!("{}", self.game_state) + } +} + +/// A Python module implemented in Rust. The name of this function must match +/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to +/// import the module. +#[pymodule] +fn store(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + + Ok(()) +} From 13ec2009a55f71b4a30c432f518cfd247ce8a63b Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sun, 18 Jan 2026 18:41:08 +0100 Subject: [PATCH 3/4] fix: comment --- bot/src/training_common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/src/training_common.rs b/bot/src/training_common.rs index 8c85021..3754086 100644 --- a/bot/src/training_common.rs +++ b/bot/src/training_common.rs @@ -15,7 +15,8 @@ pub const ACTION_SPACE_SIZE: usize = 514; pub enum TrictracAction { /// Lancer les dés Roll, - /// Continuer après avoir gagné un trou + /// Faire un nouveau 'relevé' (repositionnement des dames à l'état de départ) après avoir gagné un trou, + /// au lieu de continuer dans la position courante Go, /// Effectuer un mouvement de pions Move { From 39fd807339cc255f9ae5408ab83fcf880e050738 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sun, 18 Jan 2026 20:19:10 +0100 Subject: [PATCH 4/4] feat: python bindings (wip) --- AGENTS.md | 36 ++--- {bot => store}/pyproject.toml | 0 store/src/pyengine.rs | 253 ++++++++++------------------------ 3 files changed, 83 insertions(+), 206 deletions(-) rename {bot => store}/pyproject.toml (100%) diff --git a/AGENTS.md b/AGENTS.md index df7a4af..296820f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,40 +1,24 @@ # Agent Instructions -This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. +This project uses **bd** (beads) for issue tracking. + +Run `bd prime` for workflow context, or install hooks (`bd hooks install`) for auto-injection. ## Quick Reference -```bash -bd ready # Find available work -bd show # View issue details -bd update --status in_progress # Claim work -bd close # Complete work -bd sync # Sync with git -``` +- `bd ready` - Find unblocked work +- `bd create "Title" --type task --priority 2` - Create issue +- `bd update --status in_progress` # Claim work +- `bd close ` - Complete work +- `bd sync` - Sync with git (run at session end) ## Landing the Plane (Session Completion) -**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. +**When ending a work session**, you MUST complete ALL steps below. **MANDATORY WORKFLOW:** 1. **File issues for remaining work** - Create issues for anything that needs follow-up 2. **Run quality gates** (if code changed) - Tests, linters, builds 3. **Update issue status** - Close finished work, update in-progress items -4. **PUSH TO REMOTE** - This is MANDATORY: - ```bash - git pull --rebase - bd sync - git push - git status # MUST show "up to date with origin" - ``` -5. **Clean up** - Clear stashes, prune remote branches -6. **Verify** - All changes committed AND pushed -7. **Hand off** - Provide context for next session - -**CRITICAL RULES:** -- Work is NOT complete until `git push` succeeds -- NEVER stop before pushing - that leaves work stranded locally -- NEVER say "ready to push when you are" - YOU must push -- If push fails, resolve and retry until it succeeds - +4. **Hand off** - Provide context for next session diff --git a/bot/pyproject.toml b/store/pyproject.toml similarity index 100% rename from bot/pyproject.toml rename to store/pyproject.toml diff --git a/store/src/pyengine.rs b/store/src/pyengine.rs index af2b650..b436baa 100644 --- a/store/src/pyengine.rs +++ b/store/src/pyengine.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; use crate::board::CheckerMove; -use crate::dice::Dice; +use crate::dice::{Dice, DiceRoller}; use crate::game::{GameEvent, GameState, Stage, TurnStage}; use crate::game_rules_moves::MoveRules; use crate::game_rules_points::PointsRules; @@ -24,7 +24,7 @@ impl TricTrac { // Initialiser 2 joueurs game_state.init_player("player1"); - game_state.init_player("bot"); + game_state.init_player("player2"); // Commencer la partie avec le joueur 1 game_state.consume(&GameEvent::BeginGame { goes_first: 1 }); @@ -36,50 +36,81 @@ impl TricTrac { } } + /// Obtenir l'état du jeu sous forme de dictionnaire + fn get_state_dict<'py>(&self, py: Python<'py>) -> PyResult> { + let dict = PyDict::new(py); + dict.set_item("stage", format!("{:?}", self.game_state.stage))?; + dict.set_item("turn_stage", format!("{:?}", self.game_state.turn_stage))?; + dict.set_item("active_player_id", self.game_state.active_player_id)?; + + // Board + let board_list = self.game_state.board.to_vec(); // returns Vec + dict.set_item("board", board_list)?; + + // Dice + dict.set_item("dice", (self.game_state.dice.values.0, self.game_state.dice.values.1))?; + + // Players + let players_dict = PyDict::new(py); + for (id, player) in &self.game_state.players { + let p_dict = PyDict::new(py); + p_dict.set_item("color", format!("{:?}", player.color))?; + p_dict.set_item("holes", player.holes)?; + p_dict.set_item("points", player.points)?; + p_dict.set_item("can_bredouille", player.can_bredouille)?; + p_dict.set_item("dice_roll_count", player.dice_roll_count)?; + players_dict.set_item(id, p_dict)?; + } + dict.set_item("players", players_dict)?; + + Ok(dict) + } + + /// Lance les dés ou utilise la séquence prédéfinie + fn roll_dice(&mut self) -> PyResult<(u8, u8)> { + let player_id = self.game_state.active_player_id; + + if self.game_state.turn_stage != TurnStage::RollDice { + return Err(pyo3::exceptions::PyRuntimeError::new_err("Not in RollDice stage")); + } + + self.game_state.consume(&GameEvent::Roll { player_id }); + + let dice = if self.current_dice_index < self.dice_roll_sequence.len() { + let vals = self.dice_roll_sequence[self.current_dice_index]; + self.current_dice_index += 1; + Dice { values: vals } + } else { + DiceRoller::default().roll() + }; + + self.game_state.consume(&GameEvent::RollResult { player_id, dice }); + + Ok(dice.values) + } + + /// Applique un mouvement (deux déplacements de dames) + fn apply_move(&mut self, from1: usize, to1: usize, from2: usize, to2: usize) -> PyResult<()> { + let player_id = self.game_state.active_player_id; + + let m1 = CheckerMove::new(from1, to1).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + let m2 = CheckerMove::new(from2, to2).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + + let moves = (m1, m2); + + if !self.game_state.validate(&GameEvent::Move { player_id, moves }) { + return Err(pyo3::exceptions::PyValueError::new_err("Invalid move")); + } + + self.game_state.consume(&GameEvent::Move { player_id, moves }); + Ok(()) + } + /// Obtenir l'état du jeu sous forme de chaîne de caractères compacte fn get_state_id(&self) -> String { self.game_state.to_string_id() } - /// Obtenir l'état du jeu sous forme de dictionnaire pour faciliter l'entrainement - fn get_state_dict(&self) -> PyResult> { - Python::with_gil(|py| { - let state_dict = PyDict::new(py); - - // Informations essentielles sur l'état du jeu - state_dict.set_item("active_player", self.game_state.active_player_id)?; - state_dict.set_item("stage", format!("{:?}", self.game_state.stage))?; - state_dict.set_item("turn_stage", format!("{:?}", self.game_state.turn_stage))?; - - // Dés - let (dice1, dice2) = self.game_state.dice.values; - state_dict.set_item("dice", (dice1, dice2))?; - - // Points des joueurs - if let Some(white_player) = self.game_state.get_white_player() { - state_dict.set_item("white_points", white_player.points)?; - state_dict.set_item("white_holes", white_player.holes)?; - } - - if let Some(black_player) = self.game_state.get_black_player() { - state_dict.set_item("black_points", black_player.points)?; - state_dict.set_item("black_holes", black_player.holes)?; - } - - // Positions des pièces - let white_positions = self.get_checker_positions(Color::White); - let black_positions = self.get_checker_positions(Color::Black); - - state_dict.set_item("white_positions", white_positions)?; - state_dict.set_item("black_positions", black_positions)?; - - // État compact pour la comparaison d'états - state_dict.set_item("state_id", self.game_state.to_string_id())?; - - Ok(state_dict.into()) - }) - } - /// Renvoie les positions des pièces pour un joueur spécifique fn get_checker_positions(&self, color: Color) -> Vec<(usize, i8)> { self.game_state.board.get_color_fields(color) @@ -115,144 +146,6 @@ impl TricTrac { .collect() } - /// Jouer un coup ((from1, to1), (from2, to2)) - fn play_move(&mut self, moves: ((usize, usize), (usize, usize))) -> bool { - let ((from1, to1), (from2, to2)) = moves; - - // Vérifier que c'est au tour du joueur de jouer - if self.game_state.turn_stage != TurnStage::Move - && self.game_state.turn_stage != TurnStage::HoldOrGoChoice - { - return false; - } - - let move1 = CheckerMove::new(from1, to1).unwrap_or_default(); - let move2 = CheckerMove::new(from2, to2).unwrap_or_default(); - - let event = GameEvent::Move { - player_id: self.game_state.active_player_id, - moves: (move1, move2), - }; - - // Vérifier si le mouvement est valide - if !self.game_state.validate(&event) { - return false; - } - - // Exécuter le mouvement - self.game_state.consume(&event); - - // Si l'autre joueur doit lancer les dés maintenant, simuler ce lancement - if self.game_state.turn_stage == TurnStage::RollDice { - self.roll_dice(); - } - - true - } - - /// Lancer les dés (soit aléatoirement, soit en utilisant une séquence prédéfinie) - fn roll_dice(&mut self) -> (u8, u8) { - // Vérifier que c'est au bon moment pour lancer les dés - if self.game_state.turn_stage != TurnStage::RollDice - && self.game_state.turn_stage != TurnStage::RollWaiting - { - return self.game_state.dice.values; - } - - // Simuler un lancer de dés - let dice_values = if !self.dice_roll_sequence.is_empty() - && self.current_dice_index < self.dice_roll_sequence.len() - { - // Utiliser la séquence prédéfinie - let dice = self.dice_roll_sequence[self.current_dice_index]; - self.current_dice_index += 1; - dice - } else { - // Générer aléatoirement - ( - (1 + (rand::random::() % 6)), - (1 + (rand::random::() % 6)), - ) - }; - - // Envoyer les événements appropriés - let roll_event = GameEvent::Roll { - player_id: self.game_state.active_player_id, - }; - - if self.game_state.validate(&roll_event) { - self.game_state.consume(&roll_event); - } - - let roll_result_event = GameEvent::RollResult { - player_id: self.game_state.active_player_id, - dice: Dice { - values: dice_values, - }, - }; - - if self.game_state.validate(&roll_result_event) { - self.game_state.consume(&roll_result_event); - } - - dice_values - } - - /// Marquer des points - fn mark_points(&mut self, points: u8) -> bool { - // Vérifier que c'est au bon moment pour marquer des points - if self.game_state.turn_stage != TurnStage::MarkPoints - && self.game_state.turn_stage != TurnStage::MarkAdvPoints - { - return false; - } - - let event = GameEvent::Mark { - player_id: self.game_state.active_player_id, - points, - }; - - // Vérifier si l'événement est valide - if !self.game_state.validate(&event) { - return false; - } - - // Exécuter l'événement - self.game_state.consume(&event); - - // Si l'autre joueur doit lancer les dés maintenant, simuler ce lancement - if self.game_state.turn_stage == TurnStage::RollDice { - self.roll_dice(); - } - - true - } - - /// Choisir de "continuer" (Go) après avoir gagné un trou - fn choose_go(&mut self) -> bool { - // Vérifier que c'est au bon moment pour choisir de continuer - if self.game_state.turn_stage != TurnStage::HoldOrGoChoice { - return false; - } - - let event = GameEvent::Go { - player_id: self.game_state.active_player_id, - }; - - // Vérifier si l'événement est valide - if !self.game_state.validate(&event) { - return false; - } - - // Exécuter l'événement - self.game_state.consume(&event); - - // Simuler le lancer de dés pour le prochain tour - self.roll_dice(); - - true - } - /// Calcule les points maximaux que le joueur actif peut obtenir avec les dés actuels fn calculate_points(&self) -> u8 { let active_player = self @@ -280,7 +173,7 @@ impl TricTrac { // Initialiser 2 joueurs self.game_state.init_player("player1"); - self.game_state.init_player("bot"); + self.game_state.init_player("player2"); // Commencer la partie avec le joueur 1 self.game_state