diff --git a/client_web/src/trictrac/backend.rs b/client_web/src/trictrac/backend.rs
index 288f5e7..04b2a36 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::{DiceRoller, GameEvent, GameState, TurnStage};
+use trictrac_store::{Dice, DiceRoller, GameEvent, GameState, TurnStage};
use crate::trictrac::types::{GameDelta, PlayerAction, PreGameRollState, SerStage, ViewState};
@@ -32,12 +32,8 @@ impl TrictracBackend {
guest_die: self.pre_game_dice[1],
tie_count: self.tie_count,
});
- // 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,
- };
+ // Both players roll independently; no single "active" player.
+ vs.active_mp_player = None;
}
self.view_state = vs;
}
@@ -52,16 +48,11 @@ impl TrictracBackend {
/// Process one ceremony die-roll for `mp_player` (0 = host, 1 = guest).
fn handle_pre_game_roll(&mut self, mp_player: u16) {
- // 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 {
+ let idx = mp_player as usize;
+ // Ignore if this player already rolled.
+ if self.pre_game_dice[idx].is_some() {
return;
}
- let idx = mp_player as usize;
let single = self.dice_roller.roll().values.0;
self.pre_game_dice[idx] = Some(single);
@@ -75,9 +66,21 @@ 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 {
@@ -132,8 +135,8 @@ impl TrictracBackend {
impl BackEndArchitecture
for TrictracBackend {
fn new(_rule_variation: u16) -> Self {
let mut game = GameState::new(false);
- game.init_player("Host");
- game.init_player("Guest");
+ game.init_player("Blancs");
+ game.init_player("Noirs");
let view_state = ViewState::from_game_state(&game, HOST_PLAYER_ID, GUEST_PLAYER_ID);
@@ -171,7 +174,9 @@ 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;
@@ -284,8 +289,8 @@ impl BackEndArchitecture for TrictracBackend
#[cfg(test)]
mod tests {
use super::*;
- use backbone_lib::traits::BackEndArchitecture;
use crate::trictrac::types::{SerStage, SerTurnStage};
+ use backbone_lib::traits::BackEndArchitecture;
fn make_backend() -> TrictracBackend {
TrictracBackend::new(0)
@@ -309,10 +314,17 @@ mod tests {
if b.get_view_state().stage != SerStage::PreGameRoll {
break;
}
- 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,
+ 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);
}
b.drain_commands();
}
@@ -330,7 +342,10 @@ 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);
@@ -349,19 +364,25 @@ mod tests {
}
#[test]
- fn ceremony_wrong_order_ignored() {
+ fn ceremony_any_order_allowed() {
let mut b = make_backend();
b.player_arrival(0);
b.player_arrival(1);
b.drain_commands();
- // Guest tries to roll before host (host goes first in ceremony).
+ // Guest may roll before host.
b.inform_rpc(1, PlayerAction::PreGameRoll);
- let cmds = b.drain_commands();
+ let states = drain_deltas(&mut b);
assert!(
- cmds.is_empty(),
- "guest PreGameRoll should be ignored when it is host's turn"
+ !states.is_empty(),
+ "guest PreGameRoll should broadcast a state"
);
+ 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]
@@ -385,7 +406,10 @@ 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");
@@ -417,10 +441,7 @@ 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 73658ca..f94bfc9 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 => Some(PlayerAction::Go),
+ TurnStage::Move | TurnStage::HoldOrGoChoice => {
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 b6f43da..3c0dfe2 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, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Default, 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 9d16e5d..4aaa59e 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), **return jan** (13–18), **last jan** (19–24, exit zone).
+- 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).
- 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 last jan (fields 19–24), the player may exit.
+- When all 15 checkers are in the return 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.