Compare commits
3 commits
31ee129262
...
d3a20eb6b6
| Author | SHA1 | Date | |
|---|---|---|---|
| d3a20eb6b6 | |||
| 01fa837b84 | |||
| bef0ecbd8c |
7 changed files with 366 additions and 195 deletions
|
|
@ -70,18 +70,85 @@ input[type="text"] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Score panel ────────────────────────────────────────────────────── */
|
/* ── Player score panel ─────────────────────────────────────────────── */
|
||||||
.score-panel {
|
.player-score-panel {
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
background: #f5edd8;
|
background: #f5edd8;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-score-header {
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bars {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #555;
|
||||||
|
width: 3.5rem;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar {
|
||||||
|
width: 140px;
|
||||||
|
height: 10px;
|
||||||
|
background: rgba(0,0,0,0.12);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar-points { background: #4a7a3a; }
|
||||||
|
.score-bar-holes { background: #7a4a2a; }
|
||||||
|
|
||||||
|
.score-bar-value {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #444;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bredouille-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
background: #c07800;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.05em 0.35em;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-jans {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.1);
|
||||||
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
.score-row { display: flex; gap: 1rem; align-items: center; }
|
|
||||||
.score-name { font-weight: bold; min-width: 80px; }
|
|
||||||
|
|
||||||
/* ── Status bar ─────────────────────────────────────────────────────── */
|
/* ── Status bar ─────────────────────────────────────────────────────── */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
|
|
@ -195,7 +262,7 @@ input[type="text"] {
|
||||||
/* ── Fields ─────────────────────────────────────────────────────────── */
|
/* ── Fields ─────────────────────────────────────────────────────────── */
|
||||||
.field {
|
.field {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 110px;
|
height: 180px;
|
||||||
background: #d4a843;
|
background: #d4a843;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -228,25 +295,34 @@ input[type="text"] {
|
||||||
.top-row .field-num { bottom: auto; top: 2px; }
|
.top-row .field-num { bottom: auto; top: 2px; }
|
||||||
|
|
||||||
/* ── Checkers ───────────────────────────────────────────────────────── */
|
/* ── Checkers ───────────────────────────────────────────────────────── */
|
||||||
.checkers {
|
.checker-stack {
|
||||||
width: 46px;
|
display: flex;
|
||||||
height: 46px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checker {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 1rem;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: 2px solid rgba(0,0,0,0.3);
|
border: 2px solid rgba(0,0,0,0.3);
|
||||||
box-shadow: inset 0 2px 4px rgba(255,255,255,0.3), 0 2px 4px rgba(0,0,0,0.3);
|
box-shadow: inset 0 2px 4px rgba(255,255,255,0.3), 0 1px 3px rgba(0,0,0,0.3);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkers.white {
|
.checker + .checker { margin-top: 2px; }
|
||||||
|
|
||||||
|
.checker.white {
|
||||||
background: radial-gradient(circle at 35% 35%, #ffffff, #cccccc);
|
background: radial-gradient(circle at 35% 35%, #ffffff, #cccccc);
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkers.black {
|
.checker.black {
|
||||||
background: radial-gradient(circle at 35% 35%, #555555, #111111);
|
background: radial-gradient(circle at 35% 35%, #555555, #111111);
|
||||||
color: #eee;
|
color: #eee;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ use crate::trictrac::types::{SerTurnStage, ViewState};
|
||||||
const TOP_LEFT_W: [u8; 6] = [13, 14, 15, 16, 17, 18];
|
const TOP_LEFT_W: [u8; 6] = [13, 14, 15, 16, 17, 18];
|
||||||
const TOP_RIGHT_W: [u8; 6] = [19, 20, 21, 22, 23, 24];
|
const TOP_RIGHT_W: [u8; 6] = [19, 20, 21, 22, 23, 24];
|
||||||
const BOT_LEFT_W: [u8; 6] = [12, 11, 10, 9, 8, 7];
|
const BOT_LEFT_W: [u8; 6] = [12, 11, 10, 9, 8, 7];
|
||||||
const BOT_RIGHT_W: [u8; 6] = [ 6, 5, 4, 3, 2, 1];
|
const BOT_RIGHT_W: [u8; 6] = [6, 5, 4, 3, 2, 1];
|
||||||
|
|
||||||
/// 180° rotation of white's layout: black's pieces (field 24) appear at the bottom.
|
/// 180° rotation of white's layout: black's pieces (field 24) appear at the bottom.
|
||||||
const TOP_LEFT_B: [u8; 6] = [ 1, 2, 3, 4, 5, 6];
|
const TOP_LEFT_B: [u8; 6] = [1, 2, 3, 4, 5, 6];
|
||||||
const TOP_RIGHT_B: [u8; 6] = [ 7, 8, 9, 10, 11, 12];
|
const TOP_RIGHT_B: [u8; 6] = [7, 8, 9, 10, 11, 12];
|
||||||
const BOT_LEFT_B: [u8; 6] = [24, 23, 22, 21, 20, 19];
|
const BOT_LEFT_B: [u8; 6] = [24, 23, 22, 21, 20, 19];
|
||||||
const BOT_RIGHT_B: [u8; 6] = [18, 17, 16, 15, 14, 13];
|
const BOT_RIGHT_B: [u8; 6] = [18, 17, 16, 15, 14, 13];
|
||||||
|
|
||||||
|
|
@ -25,8 +25,12 @@ fn displayed_value(
|
||||||
let mut val = base_board[(field_num - 1) as usize];
|
let mut val = base_board[(field_num - 1) as usize];
|
||||||
let delta: i8 = if is_white { 1 } else { -1 };
|
let delta: i8 = if is_white { 1 } else { -1 };
|
||||||
for &(from, to) in staged_moves {
|
for &(from, to) in staged_moves {
|
||||||
if from == field_num { val -= delta; }
|
if from == field_num {
|
||||||
if to == field_num { val += delta; }
|
val -= delta;
|
||||||
|
}
|
||||||
|
if to == field_num {
|
||||||
|
val += delta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val
|
val
|
||||||
}
|
}
|
||||||
|
|
@ -42,11 +46,15 @@ pub fn Board(
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let board = view_state.board;
|
let board = view_state.board;
|
||||||
let is_move_stage = view_state.active_mp_player == Some(player_id)
|
let is_move_stage = view_state.active_mp_player == Some(player_id)
|
||||||
&& matches!(view_state.turn_stage, SerTurnStage::Move | SerTurnStage::HoldOrGoChoice);
|
&& matches!(
|
||||||
|
view_state.turn_stage,
|
||||||
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
|
);
|
||||||
let is_white = player_id == 0;
|
let is_white = player_id == 0;
|
||||||
|
|
||||||
let fields_from = |nums: &[u8]| -> Vec<AnyView> {
|
let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> {
|
||||||
nums.iter().map(|&field_num| {
|
nums.iter()
|
||||||
|
.map(|&field_num| {
|
||||||
view! {
|
view! {
|
||||||
<div
|
<div
|
||||||
class=move || {
|
class=move || {
|
||||||
|
|
@ -94,7 +102,20 @@ pub fn Board(
|
||||||
let count = val.unsigned_abs();
|
let count = val.unsigned_abs();
|
||||||
(count > 0).then(|| {
|
(count > 0).then(|| {
|
||||||
let color = if val > 0 { "white" } else { "black" };
|
let color = if val > 0 { "white" } else { "black" };
|
||||||
view! { <span class=format!("checkers {color}")>{count}</span> }
|
let display_n = (count as usize).min(4);
|
||||||
|
// outermost index: last for top rows, first for bottom rows.
|
||||||
|
let outer_idx = if is_top_row { display_n - 1 } else { 0 };
|
||||||
|
let chips: Vec<AnyView> = (0..display_n).map(|i| {
|
||||||
|
let label = if i == outer_idx && count >= 5 {
|
||||||
|
count.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<div class=format!("checker {color}")>{label}</div>
|
||||||
|
}.into_any()
|
||||||
|
}).collect();
|
||||||
|
view! { <div class="checker-stack">{chips}</div> }
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -113,15 +134,15 @@ pub fn Board(
|
||||||
view! {
|
view! {
|
||||||
<div class="board">
|
<div class="board">
|
||||||
<div class="board-row top-row">
|
<div class="board-row top-row">
|
||||||
<div class="board-quarter">{fields_from(tl)}</div>
|
<div class="board-quarter">{fields_from(tl, true)}</div>
|
||||||
<div class="board-bar"></div>
|
<div class="board-bar"></div>
|
||||||
<div class="board-quarter">{fields_from(tr)}</div>
|
<div class="board-quarter">{fields_from(tr, true)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="board-center-bar"></div>
|
<div class="board-center-bar"></div>
|
||||||
<div class="board-row bot-row">
|
<div class="board-row bot-row">
|
||||||
<div class="board-quarter">{fields_from(bl)}</div>
|
<div class="board-quarter">{fields_from(bl, false)}</div>
|
||||||
<div class="board-bar"></div>
|
<div class="board-bar"></div>
|
||||||
<div class="board-quarter">{fields_from(br)}</div>
|
<div class="board-quarter">{fields_from(br, false)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use trictrac_store::{CheckerMove, Jan};
|
use trictrac_store::CheckerMove;
|
||||||
|
|
||||||
use crate::app::{GameUiState, NetCommand};
|
use crate::app::{GameUiState, NetCommand};
|
||||||
use crate::trictrac::types::{JanEntry, PlayerAction, SerStage, SerTurnStage};
|
use crate::trictrac::types::{JanEntry, PlayerAction, SerStage, SerTurnStage};
|
||||||
|
|
||||||
use super::board::Board;
|
use super::board::Board;
|
||||||
use super::die::Die;
|
use super::die::Die;
|
||||||
use super::score_panel::ScorePanel;
|
use super::score_panel::PlayerScorePanel;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
/// Returns (d0_used, d1_used) by matching each staged move's distance to a die.
|
/// Returns (d0_used, d1_used) by matching each staged move's distance to a die.
|
||||||
|
|
@ -34,77 +34,31 @@ fn matched_dice_used(staged: &[(u8, u8)], dice: (u8, u8)) -> (bool, bool) {
|
||||||
(d0, d1)
|
(d0, d1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn jan_label(jan: &Jan) -> &'static str {
|
/// Split `dice_jans` into (viewer_jans, opponent_jans).
|
||||||
match jan {
|
/// Entries where the active player scores (total >= 0) go to the active player.
|
||||||
Jan::FilledQuarter => "Remplissage",
|
/// Entries where the active player loses (total < 0) go to the opponent, with signs flipped.
|
||||||
Jan::TrueHitSmallJan => "Battage à vrai (petit jan)",
|
fn split_jans(
|
||||||
Jan::TrueHitBigJan => "Battage à vrai (grand jan)",
|
dice_jans: &[JanEntry],
|
||||||
Jan::TrueHitOpponentCorner => "Battage coin adverse",
|
viewer_is_active: bool,
|
||||||
Jan::FirstPlayerToExit => "Premier sorti",
|
) -> (Vec<JanEntry>, Vec<JanEntry>) {
|
||||||
Jan::SixTables => "Six tables",
|
let mut mine = Vec::new();
|
||||||
Jan::TwoTables => "Deux tables",
|
let mut theirs = Vec::new();
|
||||||
Jan::Mezeas => "Mezeas",
|
for e in dice_jans {
|
||||||
Jan::FalseHitSmallJan => "Battage à faux (petit jan)",
|
if viewer_is_active {
|
||||||
Jan::FalseHitBigJan => "Battage à faux (grand jan)",
|
if e.total >= 0 {
|
||||||
Jan::ContreTwoTables => "Contre deux tables",
|
mine.push(e.clone());
|
||||||
Jan::ContreMezeas => "Contre mezeas",
|
} else {
|
||||||
Jan::HelplessMan => "Dame impuissante",
|
theirs.push(JanEntry { total: -e.total, points_per: -e.points_per, ..e.clone() });
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if e.total >= 0 {
|
||||||
fn format_move_pair(m1: CheckerMove, m2: CheckerMove) -> String {
|
theirs.push(e.clone());
|
||||||
let fmt = |m: CheckerMove| -> String {
|
} else {
|
||||||
let (f, t) = (m.get_from(), m.get_to());
|
mine.push(JanEntry { total: -e.total, points_per: -e.points_per, ..e.clone() });
|
||||||
if f == 0 && t == 0 { "—".to_string() }
|
|
||||||
else if t == 0 { format!("{f}↑") } // exit
|
|
||||||
else { format!("{f}→{t}") }
|
|
||||||
};
|
|
||||||
format!("{} + {}", fmt(m1), fmt(m2))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn jan_row(idx: usize, entry: JanEntry, expanded: RwSignal<Option<usize>>) -> impl IntoView {
|
|
||||||
let row_class = if entry.total >= 0 { "jan-row jan-positive" } else { "jan-row jan-negative" };
|
|
||||||
let label = jan_label(&entry.jan);
|
|
||||||
let double_tag = if entry.is_double { "double" } else { "simple" };
|
|
||||||
let ways_tag = format!("×{}", entry.ways);
|
|
||||||
let pts_str = if entry.total >= 0 { format!("+{}", entry.total) } else { format!("{}", entry.total) };
|
|
||||||
|
|
||||||
let can_expand = entry.ways > 1;
|
|
||||||
let moves = entry.moves.clone();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class=row_class
|
|
||||||
class:jan-expandable=can_expand
|
|
||||||
on:click=move |_| {
|
|
||||||
if can_expand {
|
|
||||||
expanded.update(|s| {
|
|
||||||
*s = if *s == Some(idx) { None } else { Some(idx) };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<span class="jan-label">{label}</span>
|
|
||||||
<span class="jan-tag">{double_tag}</span>
|
|
||||||
<span class="jan-tag">{ways_tag}</span>
|
|
||||||
<span class="jan-pts">{pts_str}</span>
|
|
||||||
</div>
|
|
||||||
{can_expand.then(|| {
|
|
||||||
let move_lines: Vec<_> = moves.iter()
|
|
||||||
.map(|&(m1, m2)| {
|
|
||||||
let text = format_move_pair(m1, m2);
|
|
||||||
view! { <div class="jan-move-line">{text}</div> }
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
view! {
|
|
||||||
<div class="jan-moves" class:hidden=move || expanded.get() != Some(idx)>
|
|
||||||
{move_lines}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
(mine, theirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -113,7 +67,10 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let player_id = state.player_id;
|
let player_id = state.player_id;
|
||||||
let is_my_turn = vs.active_mp_player == Some(player_id);
|
let is_my_turn = vs.active_mp_player == Some(player_id);
|
||||||
let is_move_stage = is_my_turn
|
let is_move_stage = is_my_turn
|
||||||
&& matches!(vs.turn_stage, SerTurnStage::Move | SerTurnStage::HoldOrGoChoice);
|
&& matches!(
|
||||||
|
vs.turn_stage,
|
||||||
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
|
);
|
||||||
|
|
||||||
// ── Staged move state ──────────────────────────────────────────────────────
|
// ── Staged move state ──────────────────────────────────────────────────────
|
||||||
let selected_origin: RwSignal<Option<u8>> = RwSignal::new(None);
|
let selected_origin: RwSignal<Option<u8>> = RwSignal::new(None);
|
||||||
|
|
@ -162,6 +119,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice;
|
let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice;
|
||||||
let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice;
|
let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice;
|
||||||
|
|
||||||
|
// ── Jan split: viewer_jans / opponent_jans ─────────────────────────────────
|
||||||
|
let (my_jans, opp_jans) = split_jans(&vs.dice_jans, is_my_turn);
|
||||||
|
|
||||||
|
// ── Scores: index = mp_player_id ──────────────────────────────────────────
|
||||||
|
let my_score = vs.scores[player_id as usize].clone();
|
||||||
|
let opp_score = vs.scores[1 - player_id as usize].clone();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
// ── Top bar ──────────────────────────────────────────────────────
|
// ── Top bar ──────────────────────────────────────────────────────
|
||||||
|
|
@ -170,10 +134,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
<a class="quit-link" href="#" on:click=move |e| {
|
<a class="quit-link" href="#" on:click=move |e| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
cmd_tx_quit.unbounded_send(NetCommand::Disconnect).ok();
|
cmd_tx_quit.unbounded_send(NetCommand::Disconnect).ok();
|
||||||
}>"Quit"</a>
|
}>Quit</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScorePanel scores=vs.scores.clone() player_id=player_id />
|
// ── Opponent score (above board) ─────────────────────────────────
|
||||||
|
<PlayerScorePanel score=opp_score jans=opp_jans is_you=false />
|
||||||
|
|
||||||
// ── Status ───────────────────────────────────────────────────────
|
// ── Status ───────────────────────────────────────────────────────
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
|
|
@ -195,15 +160,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
// ── Jan panel ────────────────────────────────────────────────────
|
|
||||||
{(!vs.dice_jans.is_empty()).then(|| {
|
|
||||||
let expanded: RwSignal<Option<usize>> = RwSignal::new(None);
|
|
||||||
let rows: Vec<_> = vs.dice_jans.iter().enumerate().map(|(i, entry)| {
|
|
||||||
jan_row(i, entry.clone(), expanded)
|
|
||||||
}).collect();
|
|
||||||
view! { <div class="jan-panel">{rows}</div> }
|
|
||||||
})}
|
|
||||||
|
|
||||||
// ── Board ────────────────────────────────────────────────────────
|
// ── Board ────────────────────────────────────────────────────────
|
||||||
<Board
|
<Board
|
||||||
view_state=vs
|
view_state=vs
|
||||||
|
|
@ -252,6 +208,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
// ── Player score (below board) ────────────────────────────────────
|
||||||
|
<PlayerScorePanel score=my_score jans=my_jans is_you=true />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ mod game_screen;
|
||||||
mod login_screen;
|
mod login_screen;
|
||||||
mod score_panel;
|
mod score_panel;
|
||||||
|
|
||||||
pub use die::Die;
|
|
||||||
|
|
||||||
pub use connecting_screen::ConnectingScreen;
|
pub use connecting_screen::ConnectingScreen;
|
||||||
pub use game_screen::GameScreen;
|
pub use game_screen::GameScreen;
|
||||||
pub use login_screen::LoginScreen;
|
pub use login_screen::LoginScreen;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,135 @@
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use trictrac_store::{CheckerMove, Jan};
|
||||||
|
|
||||||
use crate::trictrac::types::PlayerScore;
|
use crate::trictrac::types::{JanEntry, PlayerScore};
|
||||||
|
|
||||||
|
fn jan_label(jan: &Jan) -> &'static str {
|
||||||
|
match jan {
|
||||||
|
Jan::FilledQuarter => "Remplissage",
|
||||||
|
Jan::TrueHitSmallJan => "Battage à vrai (petit jan)",
|
||||||
|
Jan::TrueHitBigJan => "Battage à vrai (grand jan)",
|
||||||
|
Jan::TrueHitOpponentCorner => "Battage coin adverse",
|
||||||
|
Jan::FirstPlayerToExit => "Premier sorti",
|
||||||
|
Jan::SixTables => "Six tables",
|
||||||
|
Jan::TwoTables => "Deux tables",
|
||||||
|
Jan::Mezeas => "Mezeas",
|
||||||
|
Jan::FalseHitSmallJan => "Battage à faux (petit jan)",
|
||||||
|
Jan::FalseHitBigJan => "Battage à faux (grand jan)",
|
||||||
|
Jan::ContreTwoTables => "Contre deux tables",
|
||||||
|
Jan::ContreMezeas => "Contre mezeas",
|
||||||
|
Jan::HelplessMan => "Dame impuissante",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_move_pair(m1: CheckerMove, m2: CheckerMove) -> String {
|
||||||
|
let fmt = |m: CheckerMove| -> String {
|
||||||
|
let (f, t) = (m.get_from(), m.get_to());
|
||||||
|
if f == 0 && t == 0 {
|
||||||
|
"—".to_string()
|
||||||
|
} else if t == 0 {
|
||||||
|
format!("{f}↑")
|
||||||
|
} else {
|
||||||
|
format!("{f}→{t}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
format!("{} & {}", fmt(m1), fmt(m2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jan_row(idx: usize, entry: JanEntry, expanded: RwSignal<Option<usize>>) -> impl IntoView {
|
||||||
|
let row_class = if entry.total >= 0 {
|
||||||
|
"jan-row jan-expandable jan-positive"
|
||||||
|
} else {
|
||||||
|
"jan-row jan-expandable jan-negative"
|
||||||
|
};
|
||||||
|
let label = jan_label(&entry.jan);
|
||||||
|
let double_tag = if entry.is_double { "double" } else { "simple" };
|
||||||
|
let ways_tag = format!("×{}", entry.ways);
|
||||||
|
let pts_str = if entry.total >= 0 {
|
||||||
|
format!("+{}", entry.total)
|
||||||
|
} else {
|
||||||
|
format!("{}", entry.total)
|
||||||
|
};
|
||||||
|
|
||||||
|
let moves = entry.moves.clone();
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn ScorePanel(scores: [PlayerScore; 2], player_id: u16) -> impl IntoView {
|
|
||||||
let rows: Vec<_> = scores
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, score)| {
|
|
||||||
let label = if i as u16 == player_id { " (you)" } else { "" };
|
|
||||||
view! {
|
view! {
|
||||||
<div class="score-row">
|
<div>
|
||||||
<span class="score-name">{score.name}{label}</span>
|
<div
|
||||||
<span class="score-points">"Points: "{score.points}</span>
|
class=row_class
|
||||||
<span class="score-holes">"Holes: "{score.holes}</span>
|
on:click=move |_| {
|
||||||
|
expanded.update(|s| {
|
||||||
|
*s = if *s == Some(idx) { None } else { Some(idx) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="jan-label">{label}</span>
|
||||||
|
<span class="jan-tag">{double_tag}</span>
|
||||||
|
<span class="jan-tag">{ways_tag}</span>
|
||||||
|
<span class="jan-pts">{pts_str}</span>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
let move_lines: Vec<_> = moves.iter()
|
||||||
|
.map(|&(m1, m2)| {
|
||||||
|
let text = format_move_pair(m1, m2);
|
||||||
|
view! { <div class="jan-move-line">{text}</div> }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
view! {
|
||||||
|
<div class="jan-moves" class:hidden=move || expanded.get() != Some(idx)>
|
||||||
|
{move_lines}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One player's score panel: name, progress bars (points & holes), bredouille indicator,
|
||||||
|
/// and the list of jans scored by this player in the last roll.
|
||||||
|
/// `jans` should already be filtered and sign-corrected for this player's perspective.
|
||||||
|
#[component]
|
||||||
|
pub fn PlayerScorePanel(score: PlayerScore, jans: Vec<JanEntry>, is_you: bool) -> impl IntoView {
|
||||||
|
let label = if is_you { " (vous)" } else { "" };
|
||||||
|
let points_pct = format!("{}%", (score.points as u32 * 100 / 12).min(100));
|
||||||
|
let holes_pct = format!("{}%", (score.holes as u32 * 100 / 12).min(100));
|
||||||
|
let points_val = format!("{}/12", score.points);
|
||||||
|
let holes_val = format!("{}/12", score.holes);
|
||||||
|
let can_bredouille = score.can_bredouille;
|
||||||
|
|
||||||
|
let expanded: RwSignal<Option<usize>> = RwSignal::new(None);
|
||||||
|
let jan_rows: Vec<_> = jans
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, entry)| jan_row(i, entry, expanded))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="score-panel">{rows}</div>
|
<div class="player-score-panel">
|
||||||
|
<div class="player-score-header">
|
||||||
|
<span class="player-name">{score.name}{label}</span>
|
||||||
|
</div>
|
||||||
|
<div class="score-bars">
|
||||||
|
<div class="score-bar-row">
|
||||||
|
<span class="score-bar-label">"Points"</span>
|
||||||
|
<div class="score-bar">
|
||||||
|
<div class="score-bar-fill score-bar-points" style=format!("width:{points_pct}")></div>
|
||||||
|
</div>
|
||||||
|
<span class="score-bar-value">{points_val}</span>
|
||||||
|
{can_bredouille.then(|| view! {
|
||||||
|
<span class="bredouille-badge" title="Peut faire bredouille">"B"</span>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="score-bar-row">
|
||||||
|
<span class="score-bar-label">"Trous"</span>
|
||||||
|
<div class="score-bar">
|
||||||
|
<div class="score-bar-fill score-bar-holes" style=format!("width:{holes_pct}")></div>
|
||||||
|
</div>
|
||||||
|
<span class="score-bar-value">{holes_val}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(!jan_rows.is_empty()).then(|| view! {
|
||||||
|
<div class="player-jans">{jan_rows}</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ impl ViewState {
|
||||||
turn_stage: SerTurnStage::RollDice,
|
turn_stage: SerTurnStage::RollDice,
|
||||||
active_mp_player: None,
|
active_mp_player: None,
|
||||||
scores: [
|
scores: [
|
||||||
PlayerScore { name: host_name.to_string(), points: 0, holes: 0 },
|
PlayerScore { name: host_name.to_string(), points: 0, holes: 0, can_bredouille: false },
|
||||||
PlayerScore { name: guest_name.to_string(), points: 0, holes: 0 },
|
PlayerScore { name: guest_name.to_string(), points: 0, holes: 0, can_bredouille: false },
|
||||||
],
|
],
|
||||||
dice: (0, 0),
|
dice: (0, 0),
|
||||||
dice_jans: Vec::new(),
|
dice_jans: Vec::new(),
|
||||||
|
|
@ -120,8 +120,9 @@ impl ViewState {
|
||||||
name: p.name.clone(),
|
name: p.name.clone(),
|
||||||
points: p.points,
|
points: p.points,
|
||||||
holes: p.holes,
|
holes: p.holes,
|
||||||
|
can_bredouille: p.can_bredouille,
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| PlayerScore { name: String::new(), points: 0, holes: 0 })
|
.unwrap_or_else(|| PlayerScore { name: String::new(), points: 0, holes: 0, can_bredouille: false })
|
||||||
};
|
};
|
||||||
|
|
||||||
// is_double for scoring: dice show the same value (both dice identical).
|
// is_double for scoring: dice show the same value (both dice identical).
|
||||||
|
|
@ -176,6 +177,7 @@ pub struct PlayerScore {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub points: u8,
|
pub points: u8,
|
||||||
pub holes: u8,
|
pub holes: u8,
|
||||||
|
pub can_bredouille: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Serialisable mirrors of store enums ──────────────────────────────────────
|
// ── Serialisable mirrors of store enums ──────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -987,7 +987,12 @@ impl GameState {
|
||||||
player.color, self.board, dice, player.dice_roll_count
|
player.color, self.board, dice, player.dice_roll_count
|
||||||
);
|
);
|
||||||
let points_rules = PointsRules::new(&player.color, &self.board, *dice);
|
let points_rules = PointsRules::new(&player.color, &self.board, *dice);
|
||||||
Ok(points_rules.get_result_jans(player.dice_roll_count))
|
let (jans, points) = points_rules.get_result_jans(player.dice_roll_count);
|
||||||
|
Ok(if player.color == Color::White {
|
||||||
|
(jans, points)
|
||||||
|
} else {
|
||||||
|
(jans.mirror(), points)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if someone has won the game
|
/// Determines if someone has won the game
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue