diff --git a/client_web/src/trictrac/backend.rs b/client_web/src/trictrac/backend.rs
index 04b2a36..288f5e7 100644
--- a/client_web/src/trictrac/backend.rs
+++ b/client_web/src/trictrac/backend.rs
@@ -1,5 +1,5 @@
use backbone_lib::traits::{BackEndArchitecture, BackendCommand};
-use trictrac_store::{Dice, DiceRoller, GameEvent, GameState, TurnStage};
+use trictrac_store::{DiceRoller, GameEvent, GameState, TurnStage};
use crate::trictrac::types::{GameDelta, PlayerAction, PreGameRollState, SerStage, ViewState};
@@ -32,8 +32,12 @@ impl TrictracBackend {
guest_die: self.pre_game_dice[1],
tie_count: self.tie_count,
});
- // Both players roll independently; no single "active" player.
- vs.active_mp_player = None;
+ // The active mp player is whoever hasn't rolled yet (host rolls first).
+ vs.active_mp_player = match self.pre_game_dice {
+ [None, _] => Some(0),
+ [Some(_), None] => Some(1),
+ _ => None,
+ };
}
self.view_state = vs;
}
@@ -48,11 +52,16 @@ impl TrictracBackend {
/// Process one ceremony die-roll for `mp_player` (0 = host, 1 = guest).
fn handle_pre_game_roll(&mut self, mp_player: u16) {
- let idx = mp_player as usize;
- // Ignore if this player already rolled.
- if self.pre_game_dice[idx].is_some() {
+ // Enforce turn order: host rolls first, then guest.
+ let expected: u16 = match self.pre_game_dice {
+ [None, _] => 0,
+ [Some(_), None] => 1,
+ _ => return, // both already rolled (shouldn't happen)
+ };
+ if mp_player != expected {
return;
}
+ let idx = mp_player as usize;
let single = self.dice_roller.roll().values.0;
self.pre_game_dice[idx] = Some(single);
@@ -66,21 +75,9 @@ impl TrictracBackend {
self.broadcast_state();
} else {
// Highest die goes first.
- let goes_first = if h > g {
- HOST_PLAYER_ID
- } else {
- GUEST_PLAYER_ID
- };
+ let goes_first = if h > g { HOST_PLAYER_ID } else { GUEST_PLAYER_ID };
self.ceremony_started = false;
let _ = self.game.consume(&GameEvent::BeginGame { goes_first });
- // Use pre-game dice roll for the first move
- let _ = self.game.consume(&GameEvent::Roll {
- player_id: goes_first,
- });
- let _ = self.game.consume(&GameEvent::RollResult {
- player_id: goes_first,
- dice: Dice { values: (g, h) },
- });
self.broadcast_state();
}
} else {
@@ -135,8 +132,8 @@ impl TrictracBackend {
impl BackEndArchitecture
for TrictracBackend {
fn new(_rule_variation: u16) -> Self {
let mut game = GameState::new(false);
- game.init_player("Blancs");
- game.init_player("Noirs");
+ game.init_player("Host");
+ game.init_player("Guest");
let view_state = ViewState::from_game_state(&game, HOST_PLAYER_ID, GUEST_PLAYER_ID);
@@ -174,9 +171,7 @@ impl BackEndArchitecture for TrictracBackend
});
// Start the ceremony once both players have arrived.
- if self.arrived[0]
- && self.arrived[1]
- && self.game.stage == trictrac_store::Stage::PreGame
+ if self.arrived[0] && self.arrived[1] && self.game.stage == trictrac_store::Stage::PreGame
&& !self.ceremony_started
{
self.ceremony_started = true;
@@ -289,8 +284,8 @@ impl BackEndArchitecture for TrictracBackend
#[cfg(test)]
mod tests {
use super::*;
- use crate::trictrac::types::{SerStage, SerTurnStage};
use backbone_lib::traits::BackEndArchitecture;
+ use crate::trictrac::types::{SerStage, SerTurnStage};
fn make_backend() -> TrictracBackend {
TrictracBackend::new(0)
@@ -314,17 +309,10 @@ mod tests {
if b.get_view_state().stage != SerStage::PreGameRoll {
break;
}
- let pgr = b.get_view_state().pre_game_roll.clone().unwrap_or_default();
- let host_needs = pgr.host_die.is_none();
- let guest_needs = pgr.guest_die.is_none();
- if !host_needs && !guest_needs {
- break; // both rolled but stage not yet resolved — shouldn't happen
- }
- if host_needs {
- b.inform_rpc(0, PlayerAction::PreGameRoll);
- }
- if guest_needs {
- b.inform_rpc(1, PlayerAction::PreGameRoll);
+ match b.get_view_state().active_mp_player {
+ Some(0) => b.inform_rpc(0, PlayerAction::PreGameRoll),
+ Some(1) => b.inform_rpc(1, PlayerAction::PreGameRoll),
+ _ => break,
}
b.drain_commands();
}
@@ -342,10 +330,7 @@ mod tests {
let has_reset = cmds
.iter()
.any(|c| matches!(c, BackendCommand::ResetViewState));
- assert!(
- has_reset,
- "expected ResetViewState after both players arrive"
- );
+ assert!(has_reset, "expected ResetViewState after both players arrive");
// Stage should now be PreGameRoll, not InGame.
assert_eq!(b.get_view_state().stage, SerStage::PreGameRoll);
@@ -364,25 +349,19 @@ mod tests {
}
#[test]
- fn ceremony_any_order_allowed() {
+ fn ceremony_wrong_order_ignored() {
let mut b = make_backend();
b.player_arrival(0);
b.player_arrival(1);
b.drain_commands();
- // Guest may roll before host.
+ // Guest tries to roll before host (host goes first in ceremony).
b.inform_rpc(1, PlayerAction::PreGameRoll);
- let states = drain_deltas(&mut b);
+ let cmds = b.drain_commands();
assert!(
- !states.is_empty(),
- "guest PreGameRoll should broadcast a state"
+ cmds.is_empty(),
+ "guest PreGameRoll should be ignored when it is host's turn"
);
- let pgr = states.last().unwrap().pre_game_roll.as_ref().unwrap();
- assert!(
- pgr.guest_die.is_some(),
- "guest die should be set after guest rolls"
- );
- assert!(pgr.host_die.is_none(), "host die should still be blank");
}
#[test]
@@ -406,10 +385,7 @@ mod tests {
complete_ceremony(&mut b);
// Roll for whoever won the ceremony (either player could go first).
- let first_player = b
- .get_view_state()
- .active_mp_player
- .expect("someone should be active");
+ let first_player = b.get_view_state().active_mp_player.expect("someone should be active");
b.inform_rpc(first_player, PlayerAction::Roll);
let states = drain_deltas(&mut b);
assert!(!states.is_empty(), "expected a state broadcast after roll");
@@ -441,7 +417,10 @@ mod tests {
let wrong_player = if active == Some(0) { 1u16 } else { 0u16 };
b.inform_rpc(wrong_player, PlayerAction::Roll);
let cmds = b.drain_commands();
- assert!(cmds.is_empty(), "wrong player roll should be ignored");
+ assert!(
+ cmds.is_empty(),
+ "wrong player roll should be ignored"
+ );
}
#[test]
diff --git a/client_web/src/trictrac/bot_local.rs b/client_web/src/trictrac/bot_local.rs
index f94bfc9..73658ca 100644
--- a/client_web/src/trictrac/bot_local.rs
+++ b/client_web/src/trictrac/bot_local.rs
@@ -25,8 +25,8 @@ pub fn bot_decide(game: &GameState, pgr: Option<&PreGameRollState>) -> Option Some(PlayerAction::Roll),
- // TurnStage::HoldOrGoChoice => Some(PlayerAction::Go),
- TurnStage::Move | TurnStage::HoldOrGoChoice => {
+ TurnStage::HoldOrGoChoice => Some(PlayerAction::Go),
+ TurnStage::Move => {
let rules = MoveRules::new(&Color::Black, &game.board, game.dice);
let sequences = rules.get_possible_moves_sequences(true, vec![]);
let mut rng = rand::rng();
diff --git a/client_web/src/trictrac/types.rs b/client_web/src/trictrac/types.rs
index 3c0dfe2..b6f43da 100644
--- a/client_web/src/trictrac/types.rs
+++ b/client_web/src/trictrac/types.rs
@@ -31,7 +31,7 @@ pub struct GameDelta {
/// State of the pre-game ceremony where each player rolls one die to decide
/// who goes first. Present only when `stage == SerStage::PreGameRoll`.
-#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct PreGameRollState {
/// Die value (1–6) rolled by the host; `None` = not yet rolled this round.
pub host_die: Option,
diff --git a/doc/trictrac_rules.md b/doc/trictrac_rules.md
index 4aaa59e..9d16e5d 100644
--- a/doc/trictrac_rules.md
+++ b/doc/trictrac_rules.md
@@ -9,7 +9,7 @@ French terms follow the mapping in [vocabulary.md](refs/vocabulary.md).
## 1. Board and Starting Position
- 24 triangular fields (_flèches_ / _cases_), numbered 1–24 from each player's perspective.
-- 4 quarters of 6 fields: **small jan** (1–6), **big jan** (7–12), **opponent's big jan** (13–18), **return jan** (19–24, exit zone).
+- 4 quarters of 6 fields: **small jan** (1–6), **big jan** (7–12), **return jan** (13–18), **last jan** (19–24, exit zone).
- Field 12 (White) / 13 (Black) is the **rest corner** (_coin de repos_).
- Each player starts with all 15 checkers in a stack (_talon_) on field 1.
- Checkers always move in the same direction (White: 1→24; Black: mirror of that).
@@ -98,7 +98,7 @@ Ways to hit:
### 5d. Exit
-- When all 15 checkers are in the return jan (fields 19–24), the player may exit.
+- When all 15 checkers are in the last jan (fields 19–24), the player may exit.
- The exit rail counts as one additional field value.
- **Exact exit**: die value brings the checker directly to the exit rail — allowed.
- **Overflow** (_nombre excédant_): die value would carry the farthest checker past the rail — must exit.