From 39fd807339cc255f9ae5408ab83fcf880e050738 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sun, 18 Jan 2026 20:19:10 +0100 Subject: [PATCH] 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