this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(backend): game engine #1

open opened by renaud.tngl.sh targeting main from feat/backend-game-engine
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xjsjnwe6s76eea6wugk3rdxs/sh.tangled.repo.pull/3mjvvl6kbh222
+1010 -22
Diff #0
+83
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Project 6 + 7 + A turn-based management game with a card-based UI, playable in a web browser. The player runs a citizen-managed football (soccer) club rebuilt from bankruptcy. 8 + 9 + ### Lore 10 + 11 + The club was driven into bankruptcy by its previous owner, a predatory financier obsessed with his own legacy. A citizen collective bought the name and crest from the commercial court. The player is the **general secretary**, elected by the collective. The club has been demoted to the lower divisions and must rebuild โ€” but **the goal is to endure**, not to climb back to the top flight. The score is the number of turns survived. 12 + 13 + ### Current scope (engine) 14 + 15 + - Three gauges: `money`, `internal_support`, `mental_load`. 16 + - Game over: `money <= 0`, `internal_support <= 0`, or `mental_load >= 100`. 17 + - Turn-based, solo. One action played per turn. 18 + - Card-based actions. Hand of 3, draws 1 at the start of each turn โ†’ player picks 1 of 4. 19 + - Action effects are deterministic for now (no randomness on outcomes). 20 + 21 + ### Planned (not implemented yet) 22 + 23 + - Per-turn events with 3 player choices. 24 + - Match results affecting gauges. 25 + - Multiple teams (pro, youth, women's, solidarity squads). 26 + - More divisions, deeper club structure (academies, partnerships). 27 + 28 + This is a learning project โ€” the user is discovering game development, Rust, and Svelte simultaneously. Prioritize explanations and trade-offs over raw code output. 29 + 30 + ## Learning mode (important) 31 + 32 + This is a learning project. Do NOT write implementation code on the user's behalf when they could learn by writing it themselves. Instead, leverage the Learning output style to orient them: explain concepts, compare trade-offs, point at the relevant patterns, and use the "Learn by Doing" request format to hand off meaningful code decisions. Scaffolding, configuration, and boilerplate are fair game to write directly โ€” but core logic (game engine, state transitions, business rules, component architecture) should come from the user, with Claude guiding. 33 + 34 + ## Architecture 35 + 36 + - **`backend/`** โ€” Rust (Axum + SQLx + Tokio). Serves the game API and owns the game state machine. Game state is stored as JSONB in PostgreSQL. 37 + - **`frontend/`** โ€” Svelte 5 + TypeScript SPA (Vite, no SvelteKit). Communicates with the backend via REST. 38 + - **`infra/`** โ€” Kubernetes manifests (deployment target). Not used for local dev. 39 + - **`docker-compose.yml`** โ€” Local dev: PostgreSQL 17 only. 40 + 41 + ## Development 42 + 43 + ### Prerequisites 44 + - Rust toolchain, Node.js, Docker 45 + - `cargo install sqlx-cli --no-default-features --features postgres` 46 + 47 + ### Start local environment 48 + ```bash 49 + docker compose up -d # PostgreSQL 50 + cd backend && cargo run # API on :3000 (runs migrations automatically) 51 + cd frontend && npm run dev # Svelte dev server 52 + ``` 53 + 54 + ### Backend commands (from `backend/`) 55 + ```bash 56 + cargo run # build + run (migrations auto-applied) 57 + cargo build # compile only 58 + cargo sqlx prepare # generate offline query metadata for CI builds 59 + RUST_LOG=tower_http=debug cargo run # verbose HTTP request logging 60 + ``` 61 + 62 + ### Frontend commands (from `frontend/`) 63 + ```bash 64 + npm run dev # vite dev server with HMR 65 + npm run build # production build 66 + npm run check # svelte-check + tsc type checking 67 + ``` 68 + 69 + ### Database 70 + ```bash 71 + sqlx database create # create the dyfc database 72 + sqlx migrate run # apply migrations (also done on cargo run) 73 + docker compose down -v # full DB reset (destroys volume) 74 + ``` 75 + 76 + ### SQLx compile-time checking 77 + SQLx macros (`query!`, `query_scalar!`) verify SQL against the live database at compile time. PostgreSQL must be running and migrations applied before `cargo build` will succeed. Set `SQLX_OFFLINE=true` to build against cached metadata instead (run `cargo sqlx prepare` first). 78 + 79 + ## Key conventions 80 + 81 + - Backend environment variables live in `backend/.env` (loaded by dotenvy). Required: `DATABASE_URL`, `RUST_LOG`. 82 + - SQL migrations go in `backend/migrations/` with numeric prefix ordering. 83 + - Game state is serialized as a single JSONB column via serde. The game engine logic (state machine) should remain pure โ€” no HTTP or DB concerns โ€” for testability.
+6
backend/archi.md
··· 1 + src/ 2 + main.rs # HTTP + DB glue 3 + game/ 4 + mod.rs # re-exports 5 + state.rs # GameState, Gauges, GameStatus 6 + action.rs # Action, GaugeDelta, apply_action
+162
backend/src/game/action.rs
··· 1 + use std::collections::HashMap; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + use crate::game::{GameState, GameStatus, Gauges}; 6 + 7 + #[derive(Default, PartialEq, Serialize, Deserialize, Debug, Clone)] 8 + #[serde(deny_unknown_fields)] 9 + pub struct GaugeDelta { 10 + pub money: i32, 11 + pub internal_support: i32, 12 + pub mental_load: i32, 13 + } 14 + 15 + #[derive(Debug, Serialize, Deserialize, Clone)] 16 + #[serde(deny_unknown_fields)] 17 + pub struct Action { 18 + pub id: String, 19 + pub label: String, 20 + pub effect: GaugeDelta, 21 + } 22 + 23 + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 24 + pub enum GameError { 25 + GameOver, 26 + CardNotInHand, 27 + CardNotInGame, 28 + } 29 + 30 + pub fn apply_action( 31 + game_state: &GameState, 32 + action_id: &str, 33 + catalog: &HashMap<String, Action>, 34 + ) -> Result<GameState, GameError> { 35 + if game_state.status != GameStatus::Running { 36 + return Err(GameError::GameOver); 37 + } 38 + 39 + if game_state.hand.iter().any(|id| id == action_id) { 40 + let action = catalog.get(action_id).ok_or(GameError::CardNotInGame)?; 41 + let next_gauges = Gauges { 42 + money: game_state.gauges.money + action.effect.money, 43 + mental_load: game_state.gauges.mental_load + action.effect.mental_load, 44 + internal_support: game_state.gauges.internal_support + action.effect.internal_support, 45 + }; 46 + 47 + let next_game_status = GameStatus::from_gauges(next_gauges); 48 + 49 + let mut next_deck = game_state.deck.clone(); 50 + 51 + let mut next_hand = game_state 52 + .hand 53 + .iter() 54 + .filter(|id| id != &action_id) 55 + .cloned() 56 + .collect::<Vec<_>>(); 57 + 58 + match next_deck.pop() { 59 + Some(id) => { 60 + next_hand.push(id); 61 + } 62 + None => { 63 + // When the deck is empty refills it with the catalog minus the current hand 64 + let available: Vec<String> = catalog 65 + .keys() 66 + .filter(|id| !next_hand.contains(id)) 67 + .cloned() 68 + .collect(); 69 + next_deck = GameState::fresh_deck(&available); 70 + // fresh_deck panics if ids is empty so it garantise next_deck.pop() has a Some 71 + next_hand.push(next_deck.pop().unwrap()); 72 + } 73 + } 74 + 75 + Ok(GameState { 76 + turn: game_state.turn + 1, 77 + gauges: next_gauges, 78 + status: next_game_status, 79 + hand: next_hand, 80 + deck: next_deck, 81 + }) 82 + } else { 83 + Err(GameError::CardNotInHand) 84 + } 85 + } 86 + 87 + #[cfg(test)] 88 + mod tests { 89 + use crate::game::{GameOverReason, default_catalog, state::HAND_SIZE}; 90 + 91 + use super::*; 92 + 93 + #[test] 94 + fn test_apply_action_new_game() { 95 + let catalog = default_catalog(); 96 + let ids = catalog.keys().cloned().collect::<Vec<_>>(); 97 + let new_game = GameState::new(&ids); 98 + let next_action_id = new_game.hand[0].clone(); 99 + let next_game_state = apply_action(&new_game, &next_action_id, &catalog).unwrap(); 100 + assert_eq!(next_game_state.turn, 2); 101 + } 102 + 103 + #[test] 104 + fn test_apply_action_new_turn() { 105 + let catalog = default_catalog(); 106 + let ids = catalog.keys().cloned().collect::<Vec<_>>(); 107 + let new_game = GameState::new(&ids); 108 + let next_action_id = new_game.hand[0].clone(); 109 + let next_game_state = apply_action(&new_game, &next_action_id, &catalog).unwrap(); 110 + assert_eq!(next_game_state.turn, 2); 111 + assert_eq!(next_game_state.hand.len(), HAND_SIZE); 112 + assert_eq!(new_game.deck.len(), ids.len() - HAND_SIZE); 113 + assert!(next_game_state.hand.iter().all(|id| !id.is_empty())); 114 + } 115 + 116 + #[test] 117 + fn test_apply_action_reset_deck_when_empty() { 118 + let catalog = default_catalog(); 119 + let ids = catalog.keys().cloned().take(1).collect::<Vec<_>>(); 120 + let new_game = GameState::new(&ids); 121 + let next_action_id = new_game.hand[0].clone(); 122 + let next_game_state = apply_action(&new_game, &next_action_id, &catalog).unwrap(); 123 + assert_eq!(next_game_state.turn, 2); 124 + assert_eq!(next_game_state.hand.len(), 1); 125 + // All the catalog minus the action just played. 126 + assert_eq!(next_game_state.deck.len(), catalog.len() - 1); 127 + } 128 + 129 + #[test] 130 + fn test_apply_action_game_over() { 131 + let action_id = String::from("test_action"); 132 + let action = Action { 133 + id: action_id.clone(), 134 + label: String::from("Test action"), 135 + effect: GaugeDelta { 136 + money: 0, 137 + internal_support: -1, 138 + mental_load: 0, 139 + }, 140 + }; 141 + let mut catalog = default_catalog(); 142 + catalog.insert(action_id.clone(), action); 143 + let mut new_game = GameState { 144 + turn: 1, 145 + status: GameStatus::Running, 146 + gauges: Gauges { 147 + mental_load: 1, 148 + money: 50, 149 + internal_support: 1, 150 + }, 151 + hand: Vec::new(), 152 + deck: Vec::new(), 153 + }; 154 + // Manually add the cart into the hand 155 + new_game.hand.push(action_id.clone()); 156 + let next_game_state = apply_action(&new_game, &action_id, &catalog).unwrap(); 157 + assert_eq!( 158 + next_game_state.status, 159 + GameStatus::Lost(GameOverReason::LostSupport) 160 + ); 161 + } 162 + }
+8
backend/src/game/mod.rs
··· 1 + pub mod state; 2 + pub use state::{GameOverReason, GameState, GameStatus, Gauges}; 3 + 4 + pub mod action; 5 + pub use action::{Action, GameError, GaugeDelta, apply_action}; 6 + 7 + pub mod catalog; 8 + pub use catalog::default_catalog;
+162
backend/src/game/state.rs
··· 1 + use rand::rng; 2 + use rand::seq::SliceRandom; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + pub const HAND_SIZE: usize = 3; 6 + 7 + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 8 + pub struct Gauges { 9 + pub money: i32, 10 + pub internal_support: i32, 11 + pub mental_load: i32, 12 + } 13 + 14 + impl Gauges { 15 + pub fn starting() -> Self { 16 + Self { 17 + money: 1000, 18 + internal_support: 50, 19 + mental_load: 20, 20 + } 21 + } 22 + } 23 + 24 + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 25 + #[serde(tag = "kind", content = "reason")] 26 + pub enum GameStatus { 27 + Running, 28 + Lost(GameOverReason), 29 + } 30 + 31 + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 32 + pub enum GameOverReason { 33 + Bankruptcy, 34 + LostSupport, 35 + Burnout, 36 + } 37 + 38 + impl GameStatus { 39 + pub fn from_gauges(gauges: Gauges) -> Self { 40 + // Prioritรฉ en cas de cumul: support > burnout > bankruptcy 41 + if gauges.internal_support <= 0 { 42 + return GameStatus::Lost(GameOverReason::LostSupport); 43 + } 44 + if gauges.mental_load >= 100 { 45 + return GameStatus::Lost(GameOverReason::Burnout); 46 + } 47 + if gauges.money <= 0 { 48 + return GameStatus::Lost(GameOverReason::Bankruptcy); 49 + } 50 + GameStatus::Running 51 + } 52 + } 53 + 54 + #[derive(Debug, Clone, Serialize, Deserialize)] 55 + pub struct GameState { 56 + pub turn: u32, 57 + pub gauges: Gauges, 58 + pub status: GameStatus, 59 + pub hand: Vec<String>, 60 + pub deck: Vec<String>, 61 + } 62 + 63 + impl GameState { 64 + pub fn new(catalog_ids: &[String]) -> Self { 65 + let starting_gauges = Gauges::starting(); 66 + 67 + let mut deck = GameState::fresh_deck(&catalog_ids); 68 + 69 + let hand: Vec<String> = deck.drain(0..HAND_SIZE.min(deck.len())).collect(); 70 + 71 + Self { 72 + turn: 1, 73 + gauges: starting_gauges, 74 + status: GameStatus::from_gauges(starting_gauges), 75 + deck, 76 + hand, 77 + } 78 + } 79 + 80 + pub fn fresh_deck(catalog_ids: &[String]) -> Vec<String> { 81 + if catalog_ids.is_empty() { 82 + panic!("Catalog must not be empty."); 83 + } 84 + let mut deck = catalog_ids.to_vec(); 85 + deck.shuffle(&mut rng()); 86 + deck 87 + } 88 + } 89 + 90 + #[cfg(test)] 91 + mod tests { 92 + use super::*; 93 + 94 + #[test] 95 + fn game_status_from_gauges() { 96 + let game_status = GameStatus::from_gauges(Gauges::starting()); 97 + assert_eq!(game_status, GameStatus::Running); 98 + 99 + assert_eq!( 100 + GameStatus::from_gauges(Gauges { 101 + money: 1, 102 + internal_support: 1, 103 + mental_load: 99 104 + }), 105 + GameStatus::Running 106 + ); 107 + 108 + assert_eq!( 109 + GameStatus::from_gauges(Gauges { 110 + money: 0, 111 + internal_support: 1, 112 + mental_load: 99 113 + }), 114 + GameStatus::Lost(GameOverReason::Bankruptcy) 115 + ); 116 + 117 + assert_eq!( 118 + GameStatus::from_gauges(Gauges { 119 + money: 1, 120 + internal_support: 0, 121 + mental_load: 99 122 + }), 123 + GameStatus::Lost(GameOverReason::LostSupport) 124 + ); 125 + 126 + assert_eq!( 127 + GameStatus::from_gauges(Gauges { 128 + money: 1, 129 + internal_support: 1, 130 + mental_load: 100 131 + }), 132 + GameStatus::Lost(GameOverReason::Burnout) 133 + ); 134 + 135 + assert_eq!( 136 + GameStatus::from_gauges(Gauges { 137 + money: 1, 138 + internal_support: 0, 139 + mental_load: 100 140 + }), 141 + GameStatus::Lost(GameOverReason::LostSupport) 142 + ); 143 + 144 + assert_eq!( 145 + GameStatus::from_gauges(Gauges { 146 + money: 0, 147 + internal_support: 1, 148 + mental_load: 100 149 + }), 150 + GameStatus::Lost(GameOverReason::Burnout) 151 + ); 152 + 153 + assert_eq!( 154 + GameStatus::from_gauges(Gauges { 155 + money: 0, 156 + internal_support: 0, 157 + mental_load: 100 158 + }), 159 + GameStatus::Lost(GameOverReason::LostSupport) 160 + ); 161 + } 162 + }
+214
todo.md
··· 1 + # TODO โ€” Moteur de jeu (premiรจre itรฉration) 2 + 3 + Suivi de ce qu'il reste ร  implรฉmenter pour avoir un moteur de jeu minimal: 4 + jauges + action simple, vรฉrifiable par `cargo check` puis testable via l'API. 5 + 6 + --- 7 + 8 + ## 1. Crรฉer la structure du module `game` 9 + 10 + - [x] Crรฉer le dossier `backend/src/game/`. 11 + - [x] Crรฉer `backend/src/game/mod.rs` โ€” dรฉclare les sous-modules (`pub mod state;`, 12 + `pub mod action;`) et re-exporte les types publics (`pub use state::...;`). 13 + - [x] Ajouter `mod game;` au dรฉbut de `backend/src/main.rs` pour que le compilateur 14 + dรฉcouvre le module. 15 + 16 + ## 2. Dรฉfinir les types d'รฉtat (`backend/src/game/state.rs`) 17 + 18 + - [x] `Gauges` โ€” struct avec `money: i32`, `internal_support: i32`, `mental_load: i32`. 19 + Dรฉrive `Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq`. 20 + - [x] `Gauges::starting()` โ€” valeurs de dรฉpart (`money: 1000`, `internal_support: 50`, 21 + `mental_load: 20`). 22 + - [x] `GameOverReason` โ€” enum unitaire: `Bankruptcy`, `LostSupport`, `Burnout`. 23 + - [ ] `GameStatus` โ€” enum: `Running` | `Lost(GameOverReason)`. Utiliser 24 + `#[serde(tag = "kind", content = "reason")]` pour un JSON propre. 25 + - [x] `GameState` โ€” struct: `turn: u32`, `gauges: Gauges`, `status: GameStatus`. 26 + - [x] `GameState::new()` โ€” construit l'รฉtat initial en utilisant `Gauges::starting()` 27 + et `GameStatus::from_gauges(...)`. 28 + 29 + ## 3. Rรจgle de fin de partie (`backend/src/game/state.rs`) 30 + 31 + - [x] `GameStatus::from_gauges(Gauges) -> GameStatus`. 32 + - `money <= 0` โ†’ `Lost(Bankruptcy)` 33 + - `internal_support <= 0` โ†’ `Lost(LostSupport)` 34 + - `mental_load >= 100` โ†’ `Lost(Burnout)` 35 + - sinon โ†’ `Running` 36 + - Attention: inรฉgalitรฉs strictes cรดtรฉ rรจgles (`0` et `100` sont dรฉjร  perdants). 37 + - Dรฉcision ร  faire: ordre de prioritรฉ si plusieurs conditions sont vraies en mรชme temps. 38 + 39 + ## 4. Actions et transition (`backend/src/game/action.rs`) 40 + 41 + - [x] `GaugeDelta` โ€” mรชme forme que `Gauges`. Dรฉrive `Default` pour construire avec 42 + `..Default::default()`. 43 + - [x] `Action` โ€” struct: `id: String`, `label: String`, `effect: GaugeDelta`. 44 + - [x] `GameError` โ€” enum avec au moins `GameOver` pour l'instant. 45 + - [x] `apply_action(&GameState, &Action) -> Result<GameState, GameError>`. 46 + - Si `state.status` n'est pas `Running` โ†’ `Err(GameOver)`. 47 + - Sinon: additionner `effect` ร  `gauges`, `turn + 1`, recalculer `status`. 48 + - Cล“ur de la boucle de jeu โ€” รฉtat immuable en entrรฉe, nouvel รฉtat en sortie. 49 + 50 + ## 5. Cรขblage dans `main.rs` 51 + 52 + - [x] Supprimer les anciens `struct GameState` et `struct Resources` (placeholders 53 + `gold/food/population`) devenus obsolรจtes. 54 + - [x] Adapter `create_game` pour utiliser le nouveau `game::GameState::new()`. 55 + - [ ] Vรฉrifier que `cargo build` passe (SQLx a besoin de Postgres lancรฉ). 56 + 57 + ## 6. Tests unitaires (optionnel mais recommandรฉ) 58 + 59 + - [x] Dans `state.rs`, module `#[cfg(test)]` qui vรฉrifie `from_gauges` sur chaque 60 + condition de fin + cas de coexistence (money ET support ร  0 en mรชme temps). 61 + - [x] Dans `action.rs`, test qui applique une action et vรฉrifie le nouvel รฉtat 62 + (gauges, turn, status). 63 + 64 + --- 65 + 66 + # Phase 2 โ€” Boucle d'actions HTTP (cartes en main) 67 + 68 + Objectif: jouable de bout en bout depuis un client HTTP. Le joueur dรฉmarre avec 69 + 3 cartes en main, pioche 1 au dรฉbut de chaque tour, joue 1 parmi 4. 70 + 71 + ## 7. Catalogue d'actions 72 + 73 + - [x] `src/game/catalog.rs` crรฉรฉ par Claude (10 actions thรฉmatiques). 74 + - [x] Dรฉclarer `pub mod catalog;` dans `mod.rs` et re-exporter `default_catalog`. 75 + - [x] Vรฉrifier que รงa compile (`cargo check`) โ€” rรฉvรจlera probablement les 76 + รฉlรฉments ร  corriger ci-dessous. 77 + 78 + ## 8. Finir la "publication" d'`Action` 79 + 80 + `catalog.rs` construit des `Action { id, label, effect }` directement โ†’ les 81 + champs doivent รชtre `pub` (idem pour `GaugeDelta`). Sinon erreur "field is 82 + private". 83 + 84 + - [x] `pub` sur les champs d'`Action`. 85 + - [x] Vรฉrifier que `GaugeDelta` a aussi tous ses champs en `pub`. 86 + 87 + ## 9. Sรฉrialisation d'`Action` et `GaugeDelta` 88 + 89 + - [x] Dรฉriver `Serialize, Deserialize, Clone, Debug, PartialEq` sur les deux. 90 + - [x] Ajouter `#[serde(deny_unknown_fields)]` sur les deux. (Ces types 91 + ne sont jamais persistรฉs en JSONB โ€” uniquement reรงus en requรชte, 92 + donc on peut รชtre strict.) 93 + 94 + ## 10. Mรฉcanique de main et pioche dans `GameState` 95 + 96 + Le cล“ur de la phase 2. Dรฉcision d'architecture ร  prendre: 97 + 98 + **Question A โ€” quoi stocker dans la main?** 99 + - (a) `hand: Vec<String>` (ids d'actions, on relie au catalogue ร  la lecture). 100 + - (b) `hand: Vec<Action>` (actions complรจtes dupliquรฉes dans le state). 101 + 102 + โ†’ Reco: **(a)**. Plus lรฉger, plus cohรฉrent (le catalogue reste source unique 103 + de vรฉritรฉ). Coรปt: chaque lecture du JSONB doit "hydrater" les ids depuis le 104 + catalogue avant de rรฉpondre โ€” pas grave. 105 + 106 + **Question B โ€” gรฉrer un discard pile ou pas?** 107 + - (a) `deck: Vec<String>` + `discard: Vec<String>`, on reshuffle quand le 108 + deck est vide. 109 + - (b) Juste `deck: Vec<String>`, les cartes jouรฉes sont consommรฉes; partie 110 + perdue par รฉpuisement du deck (ou rรจgle alternative). 111 + 112 + โ†’ Reco: **(b)** pour dรฉmarrer. Tu pourras ajouter un discard plus tard quand 113 + รงa aura un sens gameplay (ex: certaines cartes "se recyclent", d'autres pas). 114 + 115 + ร€ implรฉmenter: 116 + - [x] ร‰tendre `GameState` avec `hand: Vec<String>` et `deck: Vec<String>`. 117 + - [x] Constante (ou fonction) `HAND_SIZE: usize = 3`. 118 + - [x] `GameState::new(catalog_ids: &[String])` โ€” prend la liste des ids 119 + disponibles, les mรฉlange dans le deck, pioche `HAND_SIZE` dans la main. 120 + Note: `GameState::new` doit rester un constructeur **pur** (pas de 121 + dรฉpendance HTTP/DB). Lui passer la liste d'ids est plus propre que 122 + lui passer la `HashMap` du catalogue. 123 + - [x] Ajouter la dรฉpendance `rand` ร  `Cargo.toml` (`rand = "0.8"` ou rรฉcent) 124 + pour `SliceRandom::shuffle`. 125 + 126 + ## 11. ร‰tendre `apply_action` 127 + 128 + L'API change. Nouvelle signature ร  dรฉbattre: 129 + 130 + ```rust 131 + pub fn apply_action( 132 + state: &GameState, 133 + action_id: &str, 134 + catalog: &HashMap<String, Action>, 135 + ) -> Result<GameState, GameError> 136 + ``` 137 + 138 + Logique: 139 + 1. Vรฉrifier `status == Running` (existant). 140 + 2. Vรฉrifier que `action_id` est dans `state.hand` โ†’ sinon 141 + `Err(GameError::CardNotInHand)`. 142 + 3. Rรฉcupรฉrer l'`Action` depuis le catalogue โ†’ sinon `UnknownAction`. 143 + 4. Retirer la carte jouรฉe de la main. 144 + 5. Piocher la prochaine carte du deck (si vide โ†’ dรฉcision: continuer sans 145 + pioche? `Err(GameError::DeckEmpty)`? ร€ toi de trancher pour la sensation 146 + de jeu). 147 + 6. Appliquer l'effet, incrรฉmenter le tour, recalculer le status. 148 + 149 + - [x] ร‰tendre `GameError` avec les nouvelles variantes (`CardNotInHand`, 150 + `UnknownAction`, et รฉventuellement `DeckEmpty`). 151 + - [x] Mettre ร  jour les tests unitaires existants (signature changรฉe). 152 + 153 + ## 12. `AppState` partage le catalogue 154 + 155 + Actuellement `AppState` n'a que `db`. Le handler HTTP aura besoin du 156 + catalogue. Comme `AppState` est clonรฉ ร  chaque requรชte, il faut รฉviter de 157 + cloner la `HashMap` ร  chaque fois. 158 + 159 + - [x] Ajouter `catalog: Arc<HashMap<String, Action>>` ร  `AppState`. 160 + - [x] Dans `main`, construire le catalogue une fois et l'envelopper dans 161 + `Arc::new(...)` avant de le passer ร  `AppState`. 162 + 163 + ## 13. ร‰tendre `AppError` 164 + 165 + Pour mapper `GameError` vers du HTTP: 166 + 167 + - [x] Ajouter au moins `BadRequest(String)` (โ†’ 400) et `Conflict(String)` 168 + (โ†’ 409, pour les erreurs de rรจgle comme `GameOver`/`CardNotInHand`). 169 + - [x] Implรฉmenter `From<GameError> for AppError` โ€” choix: lesquelles 170 + mappent vers 400 vs 409 vs 422? (`GameOver` = 409 conflict, le client 171 + ne peut plus jouer; `UnknownAction` = 400 bad input; etc.) 172 + 173 + ## 14. Handler `POST /games/{id}/actions` 174 + 175 + - [x] Dรฉfinir un type `PlayActionRequest { action_id: String }` avec 176 + `Deserialize` et `deny_unknown_fields`. 177 + - [x] Handler async: 178 + 1. Charger la ligne `games` par id (`fetch_optional` โ†’ 404 si absent). 179 + 2. Dรฉsรฉrialiser le `state` JSONB en `GameState`. 180 + 3. Appeler `apply_action(&game_state, &body.action_id, &state.catalog)`. 181 + 4. Sรฉrialiser le nouvel รฉtat. 182 + 5. `UPDATE games SET state = $1, updated_at = NOW() WHERE id = $2`. 183 + 6. Retourner le nouvel รฉtat en JSON. 184 + - [x] Enregistrer la route: `.route("/games/{id}/actions", post(...))`. 185 + 186 + Pas de transaction pour l'instant (jeu solo, on accepte le risque de race). 187 + ร€ durcir plus tard avec `pool.begin()` + `SELECT ... FOR UPDATE`. 188 + 189 + ## 15. Mettre ร  jour `create_game` 190 + 191 + - [x] `GameState::new(...)` prend maintenant la liste d'ids du catalogue. 192 + Rรฉcupรฉrer `state.catalog.keys()` et les passer. 193 + - [x] La rรฉponse renvoie le nouvel รฉtat (avec `hand` peuplรฉe) โ€” le frontend 194 + verra ses 3 cartes initiales. 195 + 196 + ## 16. Test bout en bout (manuel) 197 + 198 + - [ ] `curl -X POST localhost:3000/games` โ†’ noter l'`id` et regarder la `hand`. 199 + - [ ] `curl -X POST localhost:3000/games/{id}/actions -d '{"action_id":"..."}'` 200 + avec un id de la main โ†’ vรฉrifier que le nouvel รฉtat revient avec une 201 + main rafraรฎchie et un tour incrรฉmentรฉ. 202 + - [ ] Tenter une action_id qui n'est pas dans la main โ†’ 409. 203 + - [ ] Tenter une action_id inexistante โ†’ 400. 204 + 205 + --- 206 + 207 + ## Plus tard (toujours hors scope) 208 + 209 + - Systรจme d'รฉvรฉnements (3 choix par tour, tirรฉs d'un pool d'รฉvรฉnements). 210 + - Rรฉsultats de matchs (รฉquipes pro/jeunes/fรฉminines/solidaires) et leur 211 + influence sur les jauges. 212 + - Persistence du nombre de tours survรฉcus comme score. 213 + - Concurrence: `SELECT ... FOR UPDATE` dans une transaction. 214 + - Systรจme de discard / recyclage de cartes.
+272 -20
backend/Cargo.lock
··· 48 48 49 49 50 50 51 + checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" 52 + dependencies = [ 53 + "axum-core", 54 + "axum-macros", 55 + "bytes", 56 + "form_urlencoded", 57 + "futures-util", 51 58 52 59 53 60 ··· 87 94 88 95 89 96 97 + "tracing", 98 + ] 90 99 100 + [[package]] 101 + name = "axum-macros" 102 + version = "0.5.1" 103 + source = "registry+https://github.com/rust-lang/crates.io-index" 104 + checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" 105 + dependencies = [ 106 + "proc-macro2", 107 + "quote", 108 + "syn", 109 + ] 91 110 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 - 100 - 101 - 111 + [[package]] 112 + name = "backend" 113 + version = "0.1.0" 102 114 103 115 "anyhow", 104 116 "axum", 105 117 "dotenvy", 118 + "rand 0.10.1", 106 119 "serde", 107 120 "serde_json", 108 121 "sqlx", ··· 161 174 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 162 175 163 176 [[package]] 177 + name = "chacha20" 178 + version = "0.10.0" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" 181 + dependencies = [ 182 + "cfg-if", 183 + "cpufeatures 0.3.0", 184 + "rand_core 0.10.1", 185 + ] 186 + 187 + [[package]] 164 188 name = "concurrent-queue" 165 189 version = "2.5.0" 166 190 ··· 185 209 ] 186 210 187 211 [[package]] 212 + name = "cpufeatures" 213 + version = "0.3.0" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" 216 + dependencies = [ 217 + "libc", 218 + ] 219 + 220 + [[package]] 188 221 name = "crc" 189 222 version = "3.4.0" 190 223 ··· 430 463 ] 431 464 432 465 [[package]] 466 + name = "getrandom" 467 + version = "0.4.2" 468 + source = "registry+https://github.com/rust-lang/crates.io-index" 469 + checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" 470 + dependencies = [ 471 + "cfg-if", 472 + "libc", 473 + "r-efi", 474 + "rand_core 0.10.1", 475 + "wasip2", 476 + "wasip3", 477 + ] 478 + 479 + [[package]] 433 480 name = "hashbrown" 434 481 version = "0.15.5" 435 482 ··· 657 704 ] 658 705 659 706 [[package]] 707 + name = "id-arena" 708 + version = "2.3.0" 709 + source = "registry+https://github.com/rust-lang/crates.io-index" 710 + checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 711 + 712 + [[package]] 660 713 name = "idna" 661 714 version = "1.1.0" 662 715 ··· 685 738 dependencies = [ 686 739 "equivalent", 687 740 "hashbrown 0.17.0", 741 + "serde", 742 + "serde_core", 688 743 ] 689 744 690 745 [[package]] ··· 703 758 ] 704 759 705 760 [[package]] 761 + name = "leb128fmt" 762 + version = "0.1.0" 763 + source = "registry+https://github.com/rust-lang/crates.io-index" 764 + checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 765 + 766 + [[package]] 706 767 name = "libc" 707 768 version = "0.2.185" 708 769 ··· 825 886 "num-integer", 826 887 "num-iter", 827 888 "num-traits", 828 - "rand", 889 + "rand 0.8.6", 829 890 "smallvec", 830 891 "zeroize", 831 892 ] ··· 968 1029 ] 969 1030 970 1031 [[package]] 1032 + name = "prettyplease" 1033 + version = "0.2.37" 1034 + source = "registry+https://github.com/rust-lang/crates.io-index" 1035 + checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 1036 + dependencies = [ 1037 + "proc-macro2", 1038 + "syn", 1039 + ] 1040 + 1041 + [[package]] 971 1042 name = "proc-macro2" 972 1043 version = "1.0.106" 973 1044 ··· 986 1057 ] 987 1058 988 1059 [[package]] 1060 + name = "r-efi" 1061 + version = "6.0.0" 1062 + source = "registry+https://github.com/rust-lang/crates.io-index" 1063 + checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 1064 + 1065 + [[package]] 989 1066 name = "rand" 990 1067 version = "0.8.6" 991 1068 ··· 993 1070 dependencies = [ 994 1071 "libc", 995 1072 "rand_chacha", 996 - "rand_core", 1073 + "rand_core 0.6.4", 1074 + ] 1075 + 1076 + [[package]] 1077 + name = "rand" 1078 + version = "0.10.1" 1079 + source = "registry+https://github.com/rust-lang/crates.io-index" 1080 + checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" 1081 + dependencies = [ 1082 + "chacha20", 1083 + "getrandom 0.4.2", 1084 + "rand_core 0.10.1", 997 1085 ] 998 1086 999 1087 [[package]] ··· 1003 1091 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1004 1092 dependencies = [ 1005 1093 "ppv-lite86", 1006 - "rand_core", 1094 + "rand_core 0.6.4", 1007 1095 ] 1008 1096 1009 1097 [[package]] ··· 1012 1100 source = "registry+https://github.com/rust-lang/crates.io-index" 1013 1101 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1014 1102 dependencies = [ 1015 - "getrandom", 1103 + "getrandom 0.2.17", 1016 1104 ] 1017 1105 1018 1106 [[package]] 1107 + name = "rand_core" 1108 + version = "0.10.1" 1109 + source = "registry+https://github.com/rust-lang/crates.io-index" 1110 + checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" 1111 + 1112 + [[package]] 1019 1113 name = "redox_syscall" 1020 1114 version = "0.5.18" 1021 1115 ··· 1063 1157 "num-traits", 1064 1158 "pkcs1", 1065 1159 "pkcs8", 1066 - "rand_core", 1160 + "rand_core 0.6.4", 1067 1161 "signature", 1068 1162 "spki", 1069 1163 "subtle", ··· 1083 1177 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1084 1178 1085 1179 [[package]] 1180 + name = "semver" 1181 + version = "1.0.28" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" 1184 + 1185 + [[package]] 1086 1186 name = "serde" 1087 1187 version = "1.0.228" 1088 1188 ··· 1155 1255 checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1156 1256 dependencies = [ 1157 1257 "cfg-if", 1158 - "cpufeatures", 1258 + "cpufeatures 0.2.17", 1159 1259 "digest", 1160 1260 ] 1161 1261 ··· 1166 1266 checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1167 1267 dependencies = [ 1168 1268 "cfg-if", 1169 - "cpufeatures", 1269 + "cpufeatures 0.2.17", 1170 1270 "digest", 1171 1271 ] 1172 1272 ··· 1196 1296 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1197 1297 dependencies = [ 1198 1298 "digest", 1199 - "rand_core", 1299 + "rand_core 0.6.4", 1200 1300 ] 1201 1301 1202 1302 [[package]] ··· 1357 1457 "memchr", 1358 1458 "once_cell", 1359 1459 "percent-encoding", 1360 - "rand", 1460 + "rand 0.8.6", 1361 1461 "rsa", 1362 1462 "serde", 1363 1463 "sha1", ··· 1395 1495 "md-5", 1396 1496 "memchr", 1397 1497 "once_cell", 1398 - "rand", 1498 + "rand 0.8.6", 1399 1499 "serde", 1400 1500 "serde_json", 1401 1501 "sha2", ··· 1715 1815 checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 1716 1816 1717 1817 [[package]] 1818 + name = "unicode-xid" 1819 + version = "0.2.6" 1820 + source = "registry+https://github.com/rust-lang/crates.io-index" 1821 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1822 + 1823 + [[package]] 1718 1824 name = "url" 1719 1825 version = "2.5.8" 1720 1826 ··· 1757 1863 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1758 1864 1759 1865 [[package]] 1866 + name = "wasip2" 1867 + version = "1.0.3+wasi-0.2.9" 1868 + source = "registry+https://github.com/rust-lang/crates.io-index" 1869 + checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" 1870 + dependencies = [ 1871 + "wit-bindgen 0.57.1", 1872 + ] 1873 + 1874 + [[package]] 1875 + name = "wasip3" 1876 + version = "0.4.0+wasi-0.3.0-rc-2026-01-06" 1877 + source = "registry+https://github.com/rust-lang/crates.io-index" 1878 + checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" 1879 + dependencies = [ 1880 + "wit-bindgen 0.51.0", 1881 + ] 1882 + 1883 + [[package]] 1760 1884 name = "wasite" 1761 1885 version = "0.1.0" 1762 1886 source = "registry+https://github.com/rust-lang/crates.io-index" 1763 1887 checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 1764 1888 1765 1889 [[package]] 1890 + name = "wasm-encoder" 1891 + version = "0.244.0" 1892 + source = "registry+https://github.com/rust-lang/crates.io-index" 1893 + checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" 1894 + dependencies = [ 1895 + "leb128fmt", 1896 + "wasmparser", 1897 + ] 1898 + 1899 + [[package]] 1900 + name = "wasm-metadata" 1901 + version = "0.244.0" 1902 + source = "registry+https://github.com/rust-lang/crates.io-index" 1903 + checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 1904 + dependencies = [ 1905 + "anyhow", 1906 + "indexmap", 1907 + "wasm-encoder", 1908 + "wasmparser", 1909 + ] 1910 + 1911 + [[package]] 1912 + name = "wasmparser" 1913 + version = "0.244.0" 1914 + source = "registry+https://github.com/rust-lang/crates.io-index" 1915 + checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 1916 + dependencies = [ 1917 + "bitflags", 1918 + "hashbrown 0.15.5", 1919 + "indexmap", 1920 + "semver", 1921 + ] 1922 + 1923 + [[package]] 1766 1924 name = "whoami" 1767 1925 version = "1.6.1" 1768 1926 ··· 1854 2012 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1855 2013 1856 2014 [[package]] 2015 + name = "wit-bindgen" 2016 + version = "0.51.0" 2017 + source = "registry+https://github.com/rust-lang/crates.io-index" 2018 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 2019 + dependencies = [ 2020 + "wit-bindgen-rust-macro", 2021 + ] 2022 + 2023 + [[package]] 2024 + name = "wit-bindgen" 2025 + version = "0.57.1" 2026 + source = "registry+https://github.com/rust-lang/crates.io-index" 2027 + checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" 2028 + 2029 + [[package]] 2030 + name = "wit-bindgen-core" 2031 + version = "0.51.0" 2032 + source = "registry+https://github.com/rust-lang/crates.io-index" 2033 + checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" 2034 + dependencies = [ 2035 + "anyhow", 2036 + "heck", 2037 + "wit-parser", 2038 + ] 2039 + 2040 + [[package]] 2041 + name = "wit-bindgen-rust" 2042 + version = "0.51.0" 2043 + source = "registry+https://github.com/rust-lang/crates.io-index" 2044 + checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" 2045 + dependencies = [ 2046 + "anyhow", 2047 + "heck", 2048 + "indexmap", 2049 + "prettyplease", 2050 + "syn", 2051 + "wasm-metadata", 2052 + "wit-bindgen-core", 2053 + "wit-component", 2054 + ] 2055 + 2056 + [[package]] 2057 + name = "wit-bindgen-rust-macro" 2058 + version = "0.51.0" 2059 + source = "registry+https://github.com/rust-lang/crates.io-index" 2060 + checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" 2061 + dependencies = [ 2062 + "anyhow", 2063 + "prettyplease", 2064 + "proc-macro2", 2065 + "quote", 2066 + "syn", 2067 + "wit-bindgen-core", 2068 + "wit-bindgen-rust", 2069 + ] 2070 + 2071 + [[package]] 2072 + name = "wit-component" 2073 + version = "0.244.0" 2074 + source = "registry+https://github.com/rust-lang/crates.io-index" 2075 + checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 2076 + dependencies = [ 2077 + "anyhow", 2078 + "bitflags", 2079 + "indexmap", 2080 + "log", 2081 + "serde", 2082 + "serde_derive", 2083 + "serde_json", 2084 + "wasm-encoder", 2085 + "wasm-metadata", 2086 + "wasmparser", 2087 + "wit-parser", 2088 + ] 2089 + 2090 + [[package]] 2091 + name = "wit-parser" 2092 + version = "0.244.0" 2093 + source = "registry+https://github.com/rust-lang/crates.io-index" 2094 + checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" 2095 + dependencies = [ 2096 + "anyhow", 2097 + "id-arena", 2098 + "indexmap", 2099 + "log", 2100 + "semver", 2101 + "serde", 2102 + "serde_derive", 2103 + "serde_json", 2104 + "unicode-xid", 2105 + "wasmparser", 2106 + ] 2107 + 2108 + [[package]] 1857 2109 name = "writeable" 1858 2110 version = "0.6.3"
+3 -2
backend/Cargo.toml
··· 3 3 4 4 5 5 6 - 6 + [dependencies] 7 7 anyhow = "1.0.102" 8 - axum = "0.8.9" 8 + axum = { version = "0.8.9", features = ["macros"] } 9 9 dotenvy = "0.15.7" 10 + rand = "0.10.1" 10 11 serde = { version = "1.0.228", features = ["derive"] } 11 12 serde_json = "1.0.149" 12 13 sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "json"] }
+100
backend/src/game/catalog.rs
··· 1 + use std::collections::HashMap; 2 + 3 + use super::action::{Action, GaugeDelta}; 4 + 5 + pub fn default_catalog() -> HashMap<String, Action> { 6 + let actions = vec![ 7 + Action { 8 + id: "tombola_associative".to_string(), 9 + label: "Organiser une tombola associative".to_string(), 10 + effect: GaugeDelta { 11 + money: 150, 12 + internal_support: 5, 13 + mental_load: 5, 14 + }, 15 + }, 16 + Action { 17 + id: "vente_jeune_espoir".to_string(), 18 + label: "Vendre un jeune joueur formรฉ au club".to_string(), 19 + effect: GaugeDelta { 20 + money: 400, 21 + internal_support: -20, 22 + mental_load: 10, 23 + }, 24 + }, 25 + Action { 26 + id: "partenariat_boulangerie".to_string(), 27 + label: "Sceller un partenariat avec la boulangerie du quartier".to_string(), 28 + effect: GaugeDelta { 29 + money: 80, 30 + internal_support: 5, 31 + mental_load: 0, 32 + }, 33 + }, 34 + Action { 35 + id: "journee_chantier_benevole".to_string(), 36 + label: "Organiser une journรฉe de chantier au stade".to_string(), 37 + effect: GaugeDelta { 38 + money: 50, 39 + internal_support: 10, 40 + mental_load: -5, 41 + }, 42 + }, 43 + Action { 44 + id: "assemblee_generale".to_string(), 45 + label: "Convoquer une assemblรฉe gรฉnรฉrale du collectif".to_string(), 46 + effect: GaugeDelta { 47 + money: -20, 48 + internal_support: 15, 49 + mental_load: 10, 50 + }, 51 + }, 52 + Action { 53 + id: "reporter_salaires".to_string(), 54 + label: "Reporter les salaires de l'รฉquipe pro d'un mois".to_string(), 55 + effect: GaugeDelta { 56 + money: 300, 57 + internal_support: -25, 58 + mental_load: 15, 59 + }, 60 + }, 61 + Action { 62 + id: "dossier_subvention_municipale".to_string(), 63 + label: "Dรฉposer un dossier de subvention municipale".to_string(), 64 + effect: GaugeDelta { 65 + money: 200, 66 + internal_support: 0, 67 + mental_load: 15, 68 + }, 69 + }, 70 + Action { 71 + id: "refus_sponsor_petrolier".to_string(), 72 + label: "Refuser un sponsor pรฉtrolier".to_string(), 73 + effect: GaugeDelta { 74 + money: -100, 75 + internal_support: 20, 76 + mental_load: 0, 77 + }, 78 + }, 79 + Action { 80 + id: "weekend_repos".to_string(), 81 + label: "S'accorder un week-end de repos".to_string(), 82 + effect: GaugeDelta { 83 + money: -30, 84 + internal_support: -3, 85 + mental_load: -20, 86 + }, 87 + }, 88 + Action { 89 + id: "entrainement_ouvert_quartier".to_string(), 90 + label: "Ouvrir les entraรฎnements aux รฉcoles du quartier".to_string(), 91 + effect: GaugeDelta { 92 + money: -40, 93 + internal_support: 10, 94 + mental_load: 5, 95 + }, 96 + }, 97 + ]; 98 + 99 + actions.into_iter().map(|a| (a.id.clone(), a)).collect() 100 + }

History

1 round 0 comments
sign up or login to add to the discussion
renaud.tngl.sh submitted #0
3 commits
expand
wip
feat(server): implemented deck rotations
feat(server): implemented action post
merge conflicts detected
expand
  • backend/src/main.rs:11
expand 0 comments