+1010
-22
Diff
round #0
+83
CLAUDE.md
+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
+6
backend/archi.md
+162
backend/src/game/action.rs
+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
+8
backend/src/game/mod.rs
+162
backend/src/game/state.rs
+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
+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
+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
-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
+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
+
}