Compare commits
6 commits
24f5dba065
...
2838d59f30
| Author | SHA1 | Date | |
|---|---|---|---|
| 2838d59f30 | |||
| 00326cd645 | |||
| 1562ed1e40 | |||
| 89916c63ca | |||
| 87677a09b0 | |||
| 6995f9c888 |
8 changed files with 92 additions and 53 deletions
|
|
@ -520,6 +520,9 @@ body {
|
||||||
.die-face.die-used rect { fill: #d4d0c4; stroke: #9a8a70; }
|
.die-face.die-used rect { fill: #d4d0c4; stroke: #9a8a70; }
|
||||||
.die-face.die-used circle { fill: #9a8a70; }
|
.die-face.die-used circle { fill: #9a8a70; }
|
||||||
|
|
||||||
|
.die-face .die-question { fill: #1a0a00; font-family: sans-serif; }
|
||||||
|
.die-face.die-used .die-question { fill: #9a8a70; }
|
||||||
|
|
||||||
/* ── Jan panel ──────────────────────────────────────────────────────── */
|
/* ── Jan panel ──────────────────────────────────────────────────────── */
|
||||||
.jan-panel {
|
.jan-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ pub fn App() -> impl IntoView {
|
||||||
let is_host = session.is_host;
|
let is_host = session.is_host;
|
||||||
let player_id = session.player_id;
|
let player_id = session.player_id;
|
||||||
let reconnect_token = session.reconnect_token;
|
let reconnect_token = session.reconnect_token;
|
||||||
let mut vs = ViewState::default_with_names("Host", "Guest");
|
let mut vs = ViewState::default_with_names("Blancs", "Noirs");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
futures::select! {
|
futures::select! {
|
||||||
|
|
@ -270,6 +270,7 @@ pub fn App() -> impl IntoView {
|
||||||
view_state: Some(vs.clone()),
|
view_state: Some(vs.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let is_own_move = prev_vs.active_mp_player == Some(player_id);
|
||||||
push_or_show(
|
push_or_show(
|
||||||
&prev_vs,
|
&prev_vs,
|
||||||
GameUiState {
|
GameUiState {
|
||||||
|
|
@ -281,7 +282,7 @@ pub fn App() -> impl IntoView {
|
||||||
pause_reason: None,
|
pause_reason: None,
|
||||||
my_scored_event: None,
|
my_scored_event: None,
|
||||||
opp_scored_event: None,
|
opp_scored_event: None,
|
||||||
last_moves: compute_last_moves(&prev_vs, &vs),
|
last_moves: compute_last_moves(&prev_vs, &vs, is_own_move),
|
||||||
},
|
},
|
||||||
pending,
|
pending,
|
||||||
screen,
|
screen,
|
||||||
|
|
@ -376,7 +377,7 @@ async fn run_local_bot_game(
|
||||||
pause_reason: None,
|
pause_reason: None,
|
||||||
my_scored_event: scored,
|
my_scored_event: scored,
|
||||||
opp_scored_event: opp_scored,
|
opp_scored_event: opp_scored,
|
||||||
last_moves: compute_last_moves(&prev_vs, &vs),
|
last_moves: compute_last_moves(&prev_vs, &vs, true),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Some(NetCommand::PlayVsBot) => return true,
|
Some(NetCommand::PlayVsBot) => return true,
|
||||||
|
|
@ -406,7 +407,7 @@ async fn run_local_bot_game(
|
||||||
pause_reason: None,
|
pause_reason: None,
|
||||||
my_scored_event: None,
|
my_scored_event: None,
|
||||||
opp_scored_event: None,
|
opp_scored_event: None,
|
||||||
last_moves: compute_last_moves(&delta_prev_vs, &vs),
|
last_moves: compute_last_moves(&delta_prev_vs, &vs, false),
|
||||||
},
|
},
|
||||||
pending,
|
pending,
|
||||||
screen,
|
screen,
|
||||||
|
|
@ -421,7 +422,8 @@ async fn run_local_bot_game(
|
||||||
|
|
||||||
/// Returns the checker moves to animate when the board changed between two ViewStates.
|
/// Returns the checker moves to animate when the board changed between two ViewStates.
|
||||||
/// Returns `None` when the board is unchanged or no real moves were recorded.
|
/// Returns `None` when the board is unchanged or no real moves were recorded.
|
||||||
fn compute_last_moves(prev: &ViewState, next: &ViewState) -> Option<(CheckerMove, CheckerMove)> {
|
/// `own_move`: when true, m1 was already shown via staged-moves UI, so only animate m2.
|
||||||
|
fn compute_last_moves(prev: &ViewState, next: &ViewState, own_move: bool) -> Option<(CheckerMove, CheckerMove)> {
|
||||||
if prev.board == next.board {
|
if prev.board == next.board {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -432,6 +434,11 @@ fn compute_last_moves(prev: &ViewState, next: &ViewState) -> Option<(CheckerMove
|
||||||
// without setting dice_moves would bypass this guard and replay stale animation.
|
// without setting dice_moves would bypass this guard and replay stale animation.
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if own_move {
|
||||||
|
// m1 was already shown via the staged-moves overlay; only animate m2.
|
||||||
|
if m2 == CheckerMove::default() { return None; }
|
||||||
|
return Some((m2, CheckerMove::default()));
|
||||||
|
}
|
||||||
Some((m1, m2))
|
Some((m1, m2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub fn Die(
|
||||||
value: u8,
|
value: u8,
|
||||||
used: bool,
|
used: bool,
|
||||||
#[prop(default = false)] is_double: bool,
|
#[prop(default = false)] is_double: bool,
|
||||||
) -> impl IntoView {
|
) -> AnyView {
|
||||||
let mut cls = if used {
|
let mut cls = if used {
|
||||||
"die-face die-used".to_string()
|
"die-face die-used".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -31,6 +31,15 @@ pub fn Die(
|
||||||
if is_double && !used {
|
if is_double && !used {
|
||||||
cls.push_str(" die-double");
|
cls.push_str(" die-double");
|
||||||
}
|
}
|
||||||
|
if value == 0 {
|
||||||
|
return view! {
|
||||||
|
<svg class=cls width="48" height="48" viewBox="0 0 48 48">
|
||||||
|
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7" />
|
||||||
|
<text x="24" y="32" text-anchor="middle" font-size="24" font-weight="bold"
|
||||||
|
class="die-question">{"?"}</text>
|
||||||
|
</svg>
|
||||||
|
}.into_any();
|
||||||
|
}
|
||||||
let dots: Vec<AnyView> = dot_positions(value)
|
let dots: Vec<AnyView> = dot_positions(value)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(cx, cy)| view! { <circle cx=cx cy=cy r="4.5" /> }.into_any())
|
.map(|&(cx, cy)| view! { <circle cx=cx cy=cy r="4.5" /> }.into_any())
|
||||||
|
|
@ -40,5 +49,5 @@ pub fn Die(
|
||||||
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7" />
|
<rect x="1.5" y="1.5" width="45" height="45" rx="7" ry="7" />
|
||||||
{dots}
|
{dots}
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ use futures::channel::mpsc::UnboundedSender;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveRules};
|
use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveRules};
|
||||||
|
|
||||||
|
use super::die::Die;
|
||||||
use crate::app::{GameUiState, NetCommand, PauseReason};
|
use crate::app::{GameUiState, NetCommand, PauseReason};
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
use crate::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage};
|
use crate::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage};
|
||||||
use super::die::Die;
|
|
||||||
|
|
||||||
use super::board::Board;
|
use super::board::Board;
|
||||||
use super::score_panel::PlayerScorePanel;
|
use super::score_panel::PlayerScorePanel;
|
||||||
|
|
@ -81,9 +81,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
// wait until the buffer is drained and the live screen state is shown.
|
// wait until the buffer is drained and the live screen state is shown.
|
||||||
// Guard: never auto-roll during the pre-game ceremony (the ceremony overlay
|
// Guard: never auto-roll during the pre-game ceremony (the ceremony overlay
|
||||||
// has its own Roll button for PlayerAction::PreGameRoll).
|
// has its own Roll button for PlayerAction::PreGameRoll).
|
||||||
let show_roll = is_my_turn
|
let show_roll =
|
||||||
&& vs.turn_stage == SerTurnStage::RollDice
|
is_my_turn && vs.turn_stage == SerTurnStage::RollDice && vs.stage != SerStage::PreGameRoll;
|
||||||
&& vs.stage != SerStage::PreGameRoll;
|
|
||||||
if show_roll && !waiting_for_confirm {
|
if show_roll && !waiting_for_confirm {
|
||||||
let cmd_tx_auto = cmd_tx.clone();
|
let cmd_tx_auto = cmd_tx.clone();
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
|
|
@ -374,7 +373,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
});
|
});
|
||||||
let my_die = if player_id == 0 { pgr.host_die } else { pgr.guest_die };
|
let my_die = if player_id == 0 { pgr.host_die } else { pgr.guest_die };
|
||||||
let opp_die = if player_id == 0 { pgr.guest_die } else { pgr.host_die };
|
let opp_die = if player_id == 0 { pgr.guest_die } else { pgr.host_die };
|
||||||
let can_roll = is_my_turn && !waiting_for_confirm;
|
let can_roll = my_die.is_none() && !waiting_for_confirm;
|
||||||
let show_tie = pgr.tie_count > 0;
|
let show_tie = pgr.tie_count > 0;
|
||||||
view! {
|
view! {
|
||||||
<div class="ceremony-overlay">
|
<div class="ceremony-overlay">
|
||||||
|
|
@ -385,7 +384,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
})}
|
})}
|
||||||
<div class="ceremony-dice">
|
<div class="ceremony-dice">
|
||||||
<div class="ceremony-die-slot">
|
<div class="ceremony-die-slot">
|
||||||
<span class="ceremony-die-label">{my_name_ceremony}</span>
|
<span class="ceremony-die-label">{my_name_ceremony}{t!(i18n, you_suffix)}</span>
|
||||||
<Die value=my_die.unwrap_or(0) used=false />
|
<Die value=my_die.unwrap_or(0) used=false />
|
||||||
</div>
|
</div>
|
||||||
<div class="ceremony-die-slot">
|
<div class="ceremony-die-slot">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use backbone_lib::traits::{BackEndArchitecture, BackendCommand};
|
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};
|
use crate::trictrac::types::{GameDelta, PlayerAction, PreGameRollState, SerStage, ViewState};
|
||||||
|
|
||||||
|
|
@ -32,12 +32,8 @@ impl TrictracBackend {
|
||||||
guest_die: self.pre_game_dice[1],
|
guest_die: self.pre_game_dice[1],
|
||||||
tie_count: self.tie_count,
|
tie_count: self.tie_count,
|
||||||
});
|
});
|
||||||
// The active mp player is whoever hasn't rolled yet (host rolls first).
|
// Both players roll independently; no single "active" player.
|
||||||
vs.active_mp_player = match self.pre_game_dice {
|
vs.active_mp_player = None;
|
||||||
[None, _] => Some(0),
|
|
||||||
[Some(_), None] => Some(1),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
self.view_state = vs;
|
self.view_state = vs;
|
||||||
}
|
}
|
||||||
|
|
@ -52,16 +48,11 @@ impl TrictracBackend {
|
||||||
|
|
||||||
/// Process one ceremony die-roll for `mp_player` (0 = host, 1 = guest).
|
/// Process one ceremony die-roll for `mp_player` (0 = host, 1 = guest).
|
||||||
fn handle_pre_game_roll(&mut self, mp_player: u16) {
|
fn handle_pre_game_roll(&mut self, mp_player: u16) {
|
||||||
// Enforce turn order: host rolls first, then guest.
|
let idx = mp_player as usize;
|
||||||
let expected: u16 = match self.pre_game_dice {
|
// Ignore if this player already rolled.
|
||||||
[None, _] => 0,
|
if self.pre_game_dice[idx].is_some() {
|
||||||
[Some(_), None] => 1,
|
|
||||||
_ => return, // both already rolled (shouldn't happen)
|
|
||||||
};
|
|
||||||
if mp_player != expected {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let idx = mp_player as usize;
|
|
||||||
let single = self.dice_roller.roll().values.0;
|
let single = self.dice_roller.roll().values.0;
|
||||||
self.pre_game_dice[idx] = Some(single);
|
self.pre_game_dice[idx] = Some(single);
|
||||||
|
|
||||||
|
|
@ -75,9 +66,21 @@ impl TrictracBackend {
|
||||||
self.broadcast_state();
|
self.broadcast_state();
|
||||||
} else {
|
} else {
|
||||||
// Highest die goes first.
|
// 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;
|
self.ceremony_started = false;
|
||||||
let _ = self.game.consume(&GameEvent::BeginGame { goes_first });
|
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();
|
self.broadcast_state();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -132,8 +135,8 @@ impl TrictracBackend {
|
||||||
impl BackEndArchitecture<PlayerAction, GameDelta, ViewState> for TrictracBackend {
|
impl BackEndArchitecture<PlayerAction, GameDelta, ViewState> for TrictracBackend {
|
||||||
fn new(_rule_variation: u16) -> Self {
|
fn new(_rule_variation: u16) -> Self {
|
||||||
let mut game = GameState::new(false);
|
let mut game = GameState::new(false);
|
||||||
game.init_player("Host");
|
game.init_player("Blancs");
|
||||||
game.init_player("Guest");
|
game.init_player("Noirs");
|
||||||
|
|
||||||
let view_state = ViewState::from_game_state(&game, HOST_PLAYER_ID, GUEST_PLAYER_ID);
|
let view_state = ViewState::from_game_state(&game, HOST_PLAYER_ID, GUEST_PLAYER_ID);
|
||||||
|
|
||||||
|
|
@ -171,7 +174,9 @@ impl BackEndArchitecture<PlayerAction, GameDelta, ViewState> for TrictracBackend
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the ceremony once both players have arrived.
|
// 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
|
||||||
{
|
{
|
||||||
self.ceremony_started = true;
|
self.ceremony_started = true;
|
||||||
|
|
@ -284,8 +289,8 @@ impl BackEndArchitecture<PlayerAction, GameDelta, ViewState> for TrictracBackend
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use backbone_lib::traits::BackEndArchitecture;
|
|
||||||
use crate::trictrac::types::{SerStage, SerTurnStage};
|
use crate::trictrac::types::{SerStage, SerTurnStage};
|
||||||
|
use backbone_lib::traits::BackEndArchitecture;
|
||||||
|
|
||||||
fn make_backend() -> TrictracBackend {
|
fn make_backend() -> TrictracBackend {
|
||||||
TrictracBackend::new(0)
|
TrictracBackend::new(0)
|
||||||
|
|
@ -309,10 +314,17 @@ mod tests {
|
||||||
if b.get_view_state().stage != SerStage::PreGameRoll {
|
if b.get_view_state().stage != SerStage::PreGameRoll {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
match b.get_view_state().active_mp_player {
|
let pgr = b.get_view_state().pre_game_roll.clone().unwrap_or_default();
|
||||||
Some(0) => b.inform_rpc(0, PlayerAction::PreGameRoll),
|
let host_needs = pgr.host_die.is_none();
|
||||||
Some(1) => b.inform_rpc(1, PlayerAction::PreGameRoll),
|
let guest_needs = pgr.guest_die.is_none();
|
||||||
_ => break,
|
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();
|
b.drain_commands();
|
||||||
}
|
}
|
||||||
|
|
@ -330,7 +342,10 @@ mod tests {
|
||||||
let has_reset = cmds
|
let has_reset = cmds
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| matches!(c, BackendCommand::ResetViewState));
|
.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.
|
// Stage should now be PreGameRoll, not InGame.
|
||||||
assert_eq!(b.get_view_state().stage, SerStage::PreGameRoll);
|
assert_eq!(b.get_view_state().stage, SerStage::PreGameRoll);
|
||||||
|
|
@ -349,19 +364,25 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ceremony_wrong_order_ignored() {
|
fn ceremony_any_order_allowed() {
|
||||||
let mut b = make_backend();
|
let mut b = make_backend();
|
||||||
b.player_arrival(0);
|
b.player_arrival(0);
|
||||||
b.player_arrival(1);
|
b.player_arrival(1);
|
||||||
b.drain_commands();
|
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);
|
b.inform_rpc(1, PlayerAction::PreGameRoll);
|
||||||
let cmds = b.drain_commands();
|
let states = drain_deltas(&mut b);
|
||||||
assert!(
|
assert!(
|
||||||
cmds.is_empty(),
|
!states.is_empty(),
|
||||||
"guest PreGameRoll should be ignored when it is host's turn"
|
"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]
|
#[test]
|
||||||
|
|
@ -385,7 +406,10 @@ mod tests {
|
||||||
complete_ceremony(&mut b);
|
complete_ceremony(&mut b);
|
||||||
|
|
||||||
// Roll for whoever won the ceremony (either player could go first).
|
// 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);
|
b.inform_rpc(first_player, PlayerAction::Roll);
|
||||||
let states = drain_deltas(&mut b);
|
let states = drain_deltas(&mut b);
|
||||||
assert!(!states.is_empty(), "expected a state broadcast after roll");
|
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 };
|
let wrong_player = if active == Some(0) { 1u16 } else { 0u16 };
|
||||||
b.inform_rpc(wrong_player, PlayerAction::Roll);
|
b.inform_rpc(wrong_player, PlayerAction::Roll);
|
||||||
let cmds = b.drain_commands();
|
let cmds = b.drain_commands();
|
||||||
assert!(
|
assert!(cmds.is_empty(), "wrong player roll should be ignored");
|
||||||
cmds.is_empty(),
|
|
||||||
"wrong player roll should be ignored"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ pub fn bot_decide(game: &GameState, pgr: Option<&PreGameRollState>) -> Option<Pl
|
||||||
}
|
}
|
||||||
match game.turn_stage {
|
match game.turn_stage {
|
||||||
TurnStage::RollDice => Some(PlayerAction::Roll),
|
TurnStage::RollDice => Some(PlayerAction::Roll),
|
||||||
TurnStage::HoldOrGoChoice => Some(PlayerAction::Go),
|
// TurnStage::HoldOrGoChoice => Some(PlayerAction::Go),
|
||||||
TurnStage::Move => {
|
TurnStage::Move | TurnStage::HoldOrGoChoice => {
|
||||||
let rules = MoveRules::new(&Color::Black, &game.board, game.dice);
|
let rules = MoveRules::new(&Color::Black, &game.board, game.dice);
|
||||||
let sequences = rules.get_possible_moves_sequences(true, vec![]);
|
let sequences = rules.get_possible_moves_sequences(true, vec![]);
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub struct GameDelta {
|
||||||
|
|
||||||
/// State of the pre-game ceremony where each player rolls one die to decide
|
/// State of the pre-game ceremony where each player rolls one die to decide
|
||||||
/// who goes first. Present only when `stage == SerStage::PreGameRoll`.
|
/// who goes first. Present only when `stage == SerStage::PreGameRoll`.
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct PreGameRollState {
|
pub struct PreGameRollState {
|
||||||
/// Die value (1–6) rolled by the host; `None` = not yet rolled this round.
|
/// Die value (1–6) rolled by the host; `None` = not yet rolled this round.
|
||||||
pub host_die: Option<u8>,
|
pub host_die: Option<u8>,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ French terms follow the mapping in [vocabulary.md](refs/vocabulary.md).
|
||||||
## 1. Board and Starting Position
|
## 1. Board and Starting Position
|
||||||
|
|
||||||
- 24 triangular fields (_flèches_ / _cases_), numbered 1–24 from each player's perspective.
|
- 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_).
|
- 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.
|
- 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).
|
- Checkers always move in the same direction (White: 1→24; Black: mirror of that).
|
||||||
|
|
@ -98,7 +98,7 @@ Ways to hit:
|
||||||
|
|
||||||
### 5d. Exit
|
### 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.
|
- The exit rail counts as one additional field value.
|
||||||
- **Exact exit**: die value brings the checker directly to the exit rail — allowed.
|
- **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.
|
- **Overflow** (_nombre excédant_): die value would carry the farthest checker past the rail — must exit.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue