Compare commits
No commits in common. "7f63df294600b4bf8ad65e70f73062aa84f73865" and "c6031b0ace63228cc7c00db71ea9eed01ee02bcb" have entirely different histories.
7f63df2946
...
c6031b0ace
5 changed files with 93 additions and 395 deletions
|
|
@ -54,6 +54,7 @@ input[type="text"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Language switcher ──────────────────────────────────────────────── */
|
/* ── Language switcher ──────────────────────────────────────────────── */
|
||||||
|
|
@ -107,6 +108,7 @@ input[type="text"] {
|
||||||
font-size: 0.9rem;
|
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%;
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-score-header {
|
.player-score-header {
|
||||||
|
|
@ -178,28 +180,6 @@ input[type="text"] {
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Board + side panel ─────────────────────────────────────────────── */
|
|
||||||
.board-and-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
min-width: 160px;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Status bar ─────────────────────────────────────────────────────── */
|
/* ── Status bar ─────────────────────────────────────────────────────── */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -209,7 +189,7 @@ input[type="text"] {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dice bar ───────────────────────────────────────────────────────── */
|
/* ── Dice bars ──────────────────────────────────────────────────────── */
|
||||||
.dice-bar {
|
.dice-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -324,7 +304,6 @@ input[type="text"] {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-row {
|
.board-row {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use trictrac_store::CheckerMove;
|
|
||||||
|
|
||||||
use crate::trictrac::types::{SerTurnStage, ViewState};
|
use crate::trictrac::types::{SerTurnStage, ViewState};
|
||||||
|
|
||||||
|
|
@ -36,146 +35,6 @@ fn displayed_value(
|
||||||
val
|
val
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fields whose checkers may be selected as the next origin given already-staged moves.
|
|
||||||
fn valid_origins_for(seqs: &[(CheckerMove, CheckerMove)], staged: &[(u8, u8)]) -> Vec<u8> {
|
|
||||||
let mut v: Vec<u8> = match staged.len() {
|
|
||||||
0 => seqs.iter()
|
|
||||||
.map(|(m1, _)| m1.get_from() as u8)
|
|
||||||
.filter(|&f| f != 0)
|
|
||||||
.collect(),
|
|
||||||
1 => {
|
|
||||||
let (f0, t0) = staged[0];
|
|
||||||
seqs.iter()
|
|
||||||
.filter(|(m1, _)| m1.get_from() as u8 == f0 && m1.get_to() as u8 == t0)
|
|
||||||
.map(|(_, m2)| m2.get_from() as u8)
|
|
||||||
.filter(|&f| f != 0)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
};
|
|
||||||
v.sort_unstable();
|
|
||||||
v.dedup();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pixel center of a board field in the SVG overlay coordinate space.
|
|
||||||
/// Geometry is derived from CSS: field 60px wide, 180px tall, board padding 4px,
|
|
||||||
/// board-row gap 4px, board-bar 20px, board-center-bar 12px.
|
|
||||||
fn field_center(f: usize, is_white: bool) -> Option<(f32, f32)> {
|
|
||||||
if f == 0 || f > 24 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (qi, right, top): (usize, bool, bool) = if is_white {
|
|
||||||
match f {
|
|
||||||
13..=18 => (f - 13, false, true),
|
|
||||||
19..=24 => (f - 19, true, true),
|
|
||||||
7..=12 => (12 - f, false, false),
|
|
||||||
1..=6 => (6 - f, true, false),
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match f {
|
|
||||||
1..=6 => (f - 1, false, true),
|
|
||||||
7..=12 => (f - 7, true, true),
|
|
||||||
19..=24 => (24 - f, false, false),
|
|
||||||
13..=18 => (18 - f, true, false),
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Left-quarter field i center x: 4 + i*62 + 30 = 34 + 62i
|
|
||||||
// Right-quarter field i center x: 4 + 370 + 4 + 20 + 4 + i*62 + 30 = 432 + 62i
|
|
||||||
let x = if right { 432.0 + qi as f32 * 62.0 } else { 34.0 + qi as f32 * 62.0 };
|
|
||||||
// Top row center y: 4 + 90 = 94; bot row: 4 + 180 + 4 + 12 + 4 + 90 = 294
|
|
||||||
let y = if top { 94.0 } else { 294.0 };
|
|
||||||
Some((x, y))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SVG `<g>` element drawing one arrow (shadow + gold) from `fp` to `tp`.
|
|
||||||
fn arrow_svg(fp: (f32, f32), tp: (f32, f32)) -> AnyView {
|
|
||||||
let (x1, y1) = fp;
|
|
||||||
let (x2, y2) = tp;
|
|
||||||
let dx = x2 - x1;
|
|
||||||
let dy = y2 - y1;
|
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
if len < 10.0 {
|
|
||||||
return view! { <g /> }.into_any();
|
|
||||||
}
|
|
||||||
let nx = dx / len;
|
|
||||||
let ny = dy / len;
|
|
||||||
let px = -ny;
|
|
||||||
let py = nx;
|
|
||||||
|
|
||||||
// Shrink line ends so arrows don't overlap the checker stack
|
|
||||||
let lx1 = x1 + nx * 20.0;
|
|
||||||
let ly1 = y1 + ny * 20.0;
|
|
||||||
let lx2 = x2 - nx * 15.0;
|
|
||||||
let ly2 = y2 - ny * 15.0;
|
|
||||||
|
|
||||||
// Arrowhead triangle at (x2, y2)
|
|
||||||
let ah = 15.0_f32;
|
|
||||||
let aw = 7.0_f32;
|
|
||||||
let bx = x2 - nx * ah;
|
|
||||||
let bary = y2 - ny * ah;
|
|
||||||
let pts = format!(
|
|
||||||
"{:.1},{:.1} {:.1},{:.1} {:.1},{:.1}",
|
|
||||||
x2, y2,
|
|
||||||
bx + px * aw, bary + py * aw,
|
|
||||||
bx - px * aw, bary - py * aw,
|
|
||||||
);
|
|
||||||
let shadow_pts = format!(
|
|
||||||
"{:.1},{:.1} {:.1},{:.1} {:.1},{:.1}",
|
|
||||||
x2, y2,
|
|
||||||
bx + px * (aw + 1.5), bary + py * (aw + 1.5),
|
|
||||||
bx - px * (aw + 1.5), bary - py * (aw + 1.5),
|
|
||||||
);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<g>
|
|
||||||
// Drop-shadow for readability on coloured fields
|
|
||||||
<line
|
|
||||||
x1=format!("{lx1:.1}") y1=format!("{ly1:.1}")
|
|
||||||
x2=format!("{lx2:.1}") y2=format!("{ly2:.1}")
|
|
||||||
style="stroke:rgba(0,0,0,0.45);stroke-width:5;stroke-linecap:round"
|
|
||||||
/>
|
|
||||||
<polygon points=shadow_pts style="fill:rgba(0,0,0,0.45)" />
|
|
||||||
// Gold arrow
|
|
||||||
<line
|
|
||||||
x1=format!("{lx1:.1}") y1=format!("{ly1:.1}")
|
|
||||||
x2=format!("{lx2:.1}") y2=format!("{ly2:.1}")
|
|
||||||
style="stroke:rgba(255,215,0,0.9);stroke-width:3;stroke-linecap:round"
|
|
||||||
/>
|
|
||||||
<polygon points=pts style="fill:rgba(255,215,0,0.9)" />
|
|
||||||
</g>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Valid destinations for a selected origin given already-staged moves.
|
|
||||||
/// May include 0 (exit); callers handle that case.
|
|
||||||
fn valid_dests_for(seqs: &[(CheckerMove, CheckerMove)], staged: &[(u8, u8)], origin: u8) -> Vec<u8> {
|
|
||||||
let mut v: Vec<u8> = match staged.len() {
|
|
||||||
0 => seqs.iter()
|
|
||||||
.filter(|(m1, _)| m1.get_from() as u8 == origin)
|
|
||||||
.map(|(m1, _)| m1.get_to() as u8)
|
|
||||||
.collect(),
|
|
||||||
1 => {
|
|
||||||
let (f0, t0) = staged[0];
|
|
||||||
seqs.iter()
|
|
||||||
.filter(|(m1, m2)| {
|
|
||||||
m1.get_from() as u8 == f0
|
|
||||||
&& m1.get_to() as u8 == t0
|
|
||||||
&& m2.get_from() as u8 == origin
|
|
||||||
})
|
|
||||||
.map(|(_, m2)| m2.get_to() as u8)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
};
|
|
||||||
v.sort_unstable();
|
|
||||||
v.dedup();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Board(
|
pub fn Board(
|
||||||
view_state: ViewState,
|
view_state: ViewState,
|
||||||
|
|
@ -184,8 +43,6 @@ pub fn Board(
|
||||||
selected_origin: RwSignal<Option<u8>>,
|
selected_origin: RwSignal<Option<u8>>,
|
||||||
/// Moves staged so far this turn (max 2). Each entry is (from, to), 0 = empty move.
|
/// Moves staged so far this turn (max 2). Each entry is (from, to), 0 = empty move.
|
||||||
staged_moves: RwSignal<Vec<(u8, u8)>>,
|
staged_moves: RwSignal<Vec<(u8, u8)>>,
|
||||||
/// All valid two-move sequences for this turn (empty when not in move stage).
|
|
||||||
valid_sequences: Vec<(CheckerMove, CheckerMove)>,
|
|
||||||
) -> 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)
|
||||||
|
|
@ -194,98 +51,47 @@ pub fn Board(
|
||||||
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
);
|
);
|
||||||
let is_white = player_id == 0;
|
let is_white = player_id == 0;
|
||||||
let hovered_moves = use_context::<RwSignal<Vec<(CheckerMove, CheckerMove)>>>();
|
|
||||||
|
|
||||||
// `valid_sequences` is cloned per field (the Vec is small; Send-safe unlike Rc).
|
|
||||||
let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> {
|
let fields_from = |nums: &[u8], is_top_row: bool| -> Vec<AnyView> {
|
||||||
nums.iter()
|
nums.iter()
|
||||||
.map(|&field_num| {
|
.map(|&field_num| {
|
||||||
// Each reactive closure gets its own owned clone — Vec<(CheckerMove,CheckerMove)>
|
|
||||||
// is Send, which Leptos requires for reactive attribute functions.
|
|
||||||
let seqs_c = valid_sequences.clone();
|
|
||||||
let seqs_k = valid_sequences.clone();
|
|
||||||
view! {
|
view! {
|
||||||
<div
|
<div
|
||||||
class=move || {
|
class=move || {
|
||||||
let staged = staged_moves.get();
|
let moves = staged_moves.get();
|
||||||
let val = displayed_value(board, &staged, is_white, field_num);
|
let val = displayed_value(board, &moves, is_white, field_num);
|
||||||
let is_mine = if is_white { val > 0 } else { val < 0 };
|
let is_mine = if is_white { val > 0 } else { val < 0 };
|
||||||
let can_stage = is_move_stage && staged.len() < 2;
|
let can_stage = is_move_stage && moves.len() < 2;
|
||||||
let sel = selected_origin.get();
|
let sel = selected_origin.get();
|
||||||
|
|
||||||
let mut cls = "field".to_string();
|
let mut cls = "field".to_string();
|
||||||
|
if can_stage && (sel.is_some() || is_mine) {
|
||||||
if seqs_c.is_empty() {
|
cls.push_str(" clickable");
|
||||||
// No restriction (dice not rolled or not move stage)
|
}
|
||||||
if can_stage && (sel.is_some() || is_mine) {
|
if sel == Some(field_num) { cls.push_str(" selected"); }
|
||||||
cls.push_str(" clickable");
|
if can_stage && sel.is_some() && sel != Some(field_num) {
|
||||||
}
|
cls.push_str(" dest");
|
||||||
if sel == Some(field_num) { cls.push_str(" selected"); }
|
|
||||||
if can_stage && sel.is_some() && sel != Some(field_num) {
|
|
||||||
cls.push_str(" dest");
|
|
||||||
}
|
|
||||||
} else if can_stage {
|
|
||||||
if let Some(origin) = sel {
|
|
||||||
if origin == field_num {
|
|
||||||
cls.push_str(" selected clickable");
|
|
||||||
} else {
|
|
||||||
let dests = valid_dests_for(&seqs_c, &staged, origin);
|
|
||||||
// Only highlight non-exit destinations (field 0 = exit has no tile)
|
|
||||||
if dests.iter().any(|&d| d == field_num && d != 0) {
|
|
||||||
cls.push_str(" clickable dest");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let origins = valid_origins_for(&seqs_c, &staged);
|
|
||||||
if origins.iter().any(|&o| o == field_num) {
|
|
||||||
cls.push_str(" clickable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cls
|
cls
|
||||||
}
|
}
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
if !is_move_stage { return; }
|
if !is_move_stage { return; }
|
||||||
let staged = staged_moves.get_untracked();
|
if staged_moves.get_untracked().len() >= 2 { return; }
|
||||||
if staged.len() >= 2 { return; }
|
|
||||||
|
let moves = staged_moves.get_untracked();
|
||||||
|
let val = displayed_value(board, &moves, is_white, field_num);
|
||||||
|
let is_mine = if is_white { val > 0 } else { val < 0 };
|
||||||
|
|
||||||
match selected_origin.get_untracked() {
|
match selected_origin.get_untracked() {
|
||||||
Some(origin) if origin == field_num => {
|
Some(origin) if origin == field_num => {
|
||||||
selected_origin.set(None);
|
selected_origin.set(None);
|
||||||
}
|
}
|
||||||
Some(origin) => {
|
Some(origin) => {
|
||||||
let valid = if seqs_k.is_empty() {
|
staged_moves.update(|v| v.push((origin, field_num)));
|
||||||
true
|
selected_origin.set(None);
|
||||||
} else {
|
|
||||||
valid_dests_for(&seqs_k, &staged, origin)
|
|
||||||
.iter()
|
|
||||||
.any(|&d| d == field_num)
|
|
||||||
};
|
|
||||||
if valid {
|
|
||||||
staged_moves.update(|v| v.push((origin, field_num)));
|
|
||||||
selected_origin.set(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if seqs_k.is_empty() {
|
|
||||||
let val = displayed_value(board, &staged, is_white, field_num);
|
|
||||||
if is_white && val > 0 || !is_white && val < 0 {
|
|
||||||
selected_origin.set(Some(field_num));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let origins = valid_origins_for(&seqs_k, &staged);
|
|
||||||
if origins.iter().any(|&o| o == field_num) {
|
|
||||||
let dests = valid_dests_for(&seqs_k, &staged, field_num);
|
|
||||||
if !dests.is_empty() && dests.iter().all(|&d| d == 0) {
|
|
||||||
// All destinations are exits: auto-stage
|
|
||||||
staged_moves.update(|v| v.push((field_num, 0)));
|
|
||||||
} else {
|
|
||||||
selected_origin.set(Some(field_num));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
None if is_mine => selected_origin.set(Some(field_num)),
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -338,34 +144,6 @@ pub fn Board(
|
||||||
<div class="board-bar"></div>
|
<div class="board-bar"></div>
|
||||||
<div class="board-quarter">{fields_from(br, false)}</div>
|
<div class="board-quarter">{fields_from(br, false)}</div>
|
||||||
</div>
|
</div>
|
||||||
// SVG overlay: arrows for hovered jan moves
|
|
||||||
<svg
|
|
||||||
width="776" height="388"
|
|
||||||
style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible"
|
|
||||||
>
|
|
||||||
{move || {
|
|
||||||
let Some(hm) = hovered_moves else { return vec![]; };
|
|
||||||
let pairs = hm.get();
|
|
||||||
if pairs.is_empty() { return vec![]; }
|
|
||||||
// Collect unique individual (from, to) moves; skip empty/exit.
|
|
||||||
let mut moves: Vec<(usize, usize)> = pairs.iter()
|
|
||||||
.flat_map(|(m1, m2)| [
|
|
||||||
(m1.get_from(), m1.get_to()),
|
|
||||||
(m2.get_from(), m2.get_to()),
|
|
||||||
])
|
|
||||||
.filter(|&(f, t)| f != 0 && t != 0)
|
|
||||||
.collect();
|
|
||||||
moves.sort_unstable();
|
|
||||||
moves.dedup();
|
|
||||||
moves.into_iter()
|
|
||||||
.filter_map(|(from, to)| {
|
|
||||||
let p1 = field_center(from, is_white)?;
|
|
||||||
let p2 = field_center(to, is_white)?;
|
|
||||||
Some(arrow_svg(p1, p2))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}}
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, MoveRules};
|
use trictrac_store::CheckerMove;
|
||||||
|
|
||||||
use crate::app::{GameUiState, NetCommand};
|
use crate::app::{GameUiState, NetCommand};
|
||||||
use crate::i18n::*;
|
use crate::i18n::*;
|
||||||
|
|
@ -75,10 +75,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
SerTurnStage::Move | SerTurnStage::HoldOrGoChoice
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Hovered jan moves (shown as arrows on the board) ──────────────────────
|
|
||||||
let hovered_jan_moves: RwSignal<Vec<(CheckerMove, CheckerMove)>> = RwSignal::new(vec![]);
|
|
||||||
provide_context(hovered_jan_moves);
|
|
||||||
|
|
||||||
// ── Staged move state ──────────────────────────────────────────────────────
|
// ── Staged move state ──────────────────────────────────────────────────────
|
||||||
let selected_origin: RwSignal<Option<u8>> = RwSignal::new(None);
|
let selected_origin: RwSignal<Option<u8>> = RwSignal::new(None);
|
||||||
let staged_moves: RwSignal<Vec<(u8, u8)>> = RwSignal::new(Vec::new());
|
let staged_moves: RwSignal<Vec<(u8, u8)>> = RwSignal::new(Vec::new());
|
||||||
|
|
@ -115,27 +111,6 @@ 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;
|
||||||
|
|
||||||
// ── Valid move sequences for this turn ─────────────────────────────────────
|
|
||||||
// Computed once per ViewState snapshot; used by Board (highlighting) and the
|
|
||||||
// empty-move button (visibility).
|
|
||||||
let valid_sequences: Vec<(CheckerMove, CheckerMove)> = if is_move_stage && dice != (0, 0) {
|
|
||||||
let mut store_board = StoreBoard::new();
|
|
||||||
store_board.set_positions(&Color::White, vs.board);
|
|
||||||
let store_dice = StoreDice { values: dice };
|
|
||||||
let color = if player_id == 0 { Color::White } else { Color::Black };
|
|
||||||
let rules = MoveRules::new(&color, &store_board, store_dice);
|
|
||||||
let raw = rules.get_possible_moves_sequences(true, vec![]);
|
|
||||||
if player_id == 0 {
|
|
||||||
raw
|
|
||||||
} else {
|
|
||||||
raw.into_iter().map(|(m1, m2)| (m1.mirror(), m2.mirror())).collect()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
// Clone for the empty-move button reactive closure (Board consumes the original).
|
|
||||||
let valid_seqs_empty = valid_sequences.clone();
|
|
||||||
|
|
||||||
// ── Jan split: viewer_jans / opponent_jans ─────────────────────────────────
|
// ── Jan split: viewer_jans / opponent_jans ─────────────────────────────────
|
||||||
let (my_jans, opp_jans) = split_jans(&vs.dice_jans, is_my_turn && !show_roll);
|
let (my_jans, opp_jans) = split_jans(&vs.dice_jans, is_my_turn && !show_roll);
|
||||||
|
|
||||||
|
|
@ -182,99 +157,78 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
// ── Opponent score (above board) ─────────────────────────────────
|
// ── Opponent score (above board) ─────────────────────────────────
|
||||||
<PlayerScorePanel score=opp_score jans=opp_jans is_you=false />
|
<PlayerScorePanel score=opp_score jans=opp_jans is_you=false />
|
||||||
|
|
||||||
// ── Board + side panel ───────────────────────────────────────────
|
// ── Status ───────────────────────────────────────────────────────
|
||||||
<div class="board-and-panel">
|
<div class="status-bar">
|
||||||
<Board
|
<span>{move || {
|
||||||
view_state=vs
|
let n = staged_moves.get().len();
|
||||||
player_id=player_id
|
if is_move_stage {
|
||||||
selected_origin=selected_origin
|
t_string!(i18n, select_move, n = n + 1)
|
||||||
staged_moves=staged_moves
|
} else {
|
||||||
valid_sequences=valid_sequences
|
String::from(match (&stage, is_my_turn, &turn_stage) {
|
||||||
/>
|
(SerStage::Ended, _, _) => t_string!(i18n, game_over),
|
||||||
|
(SerStage::PreGame, _, _) => t_string!(i18n, waiting_for_opponent),
|
||||||
// ── Side panel ───────────────────────────────────────────────
|
(SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll),
|
||||||
<div class="side-panel">
|
(SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go),
|
||||||
// Status message
|
(SerStage::InGame, true, _) => t_string!(i18n, your_turn),
|
||||||
<div class="status-bar">
|
(SerStage::InGame, false, _) => t_string!(i18n, opponent_turn),
|
||||||
<span>{move || {
|
})
|
||||||
let n = staged_moves.get().len();
|
}
|
||||||
if is_move_stage {
|
}}</span>
|
||||||
t_string!(i18n, select_move, n = n + 1)
|
|
||||||
} else {
|
|
||||||
String::from(match (&stage, is_my_turn, &turn_stage) {
|
|
||||||
(SerStage::Ended, _, _) => t_string!(i18n, game_over),
|
|
||||||
(SerStage::PreGame, _, _) => t_string!(i18n, waiting_for_opponent),
|
|
||||||
(SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll),
|
|
||||||
(SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go),
|
|
||||||
(SerStage::InGame, true, _) => t_string!(i18n, your_turn),
|
|
||||||
(SerStage::InGame, false, _) => t_string!(i18n, opponent_turn),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Dice (always shown when rolled, used state depends on whose turn)
|
|
||||||
{show_dice.then(|| view! {
|
|
||||||
<div class="dice-bar">
|
|
||||||
{move || {
|
|
||||||
let (d0, d1) = if is_move_stage {
|
|
||||||
matched_dice_used(&staged_moves.get(), dice)
|
|
||||||
} else {
|
|
||||||
(true, true)
|
|
||||||
};
|
|
||||||
view! {
|
|
||||||
<Die value=dice.0 used=d0 />
|
|
||||||
<Die value=dice.1 used=d1 />
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
})}
|
|
||||||
|
|
||||||
// Action buttons
|
|
||||||
<div class="action-buttons">
|
|
||||||
{show_roll.then(|| view! {
|
|
||||||
<button class="btn btn-primary" on:click=move |_| {
|
|
||||||
cmd_tx_roll.unbounded_send(NetCommand::Action(PlayerAction::Roll)).ok();
|
|
||||||
}>{t!(i18n, roll_dice)}</button>
|
|
||||||
})}
|
|
||||||
{show_hold_go.then(|| view! {
|
|
||||||
<button class="btn btn-primary" on:click=move |_| {
|
|
||||||
cmd_tx_go.unbounded_send(NetCommand::Action(PlayerAction::Go)).ok();
|
|
||||||
}>{t!(i18n, go)}</button>
|
|
||||||
})}
|
|
||||||
{move || {
|
|
||||||
// Show the empty-move button only when (0,0) is a valid
|
|
||||||
// first or second move given what has already been staged.
|
|
||||||
let staged = staged_moves.get();
|
|
||||||
let show = is_move_stage && staged.len() < 2 && (
|
|
||||||
valid_seqs_empty.is_empty() || match staged.len() {
|
|
||||||
0 => valid_seqs_empty.iter().any(|(m1, _)| m1.get_from() == 0),
|
|
||||||
1 => {
|
|
||||||
let (f0, t0) = staged[0];
|
|
||||||
valid_seqs_empty.iter()
|
|
||||||
.filter(|(m1, _)| {
|
|
||||||
m1.get_from() as u8 == f0
|
|
||||||
&& m1.get_to() as u8 == t0
|
|
||||||
})
|
|
||||||
.any(|(_, m2)| m2.get_from() == 0)
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
show.then(|| view! {
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
on:click=move |_| {
|
|
||||||
selected_origin.set(None);
|
|
||||||
staged_moves.update(|v| v.push((0, 0)));
|
|
||||||
}
|
|
||||||
>{t!(i18n, empty_move)}</button>
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// ── Opponent dice (top) ──────────────────────────────────────────
|
||||||
|
{(!is_my_turn && show_dice).then(|| view! {
|
||||||
|
<div class="dice-bar dice-bar-opponent">
|
||||||
|
<Die value=dice.0 used=true />
|
||||||
|
<Die value=dice.1 used=true />
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
|
||||||
|
// ── Board ────────────────────────────────────────────────────────
|
||||||
|
<Board
|
||||||
|
view_state=vs
|
||||||
|
player_id=player_id
|
||||||
|
selected_origin=selected_origin
|
||||||
|
staged_moves=staged_moves
|
||||||
|
/>
|
||||||
|
|
||||||
|
// ── Player action bar (bottom) ───────────────────────────────────
|
||||||
|
{is_my_turn.then(|| view! {
|
||||||
|
<div class="dice-bar dice-bar-player">
|
||||||
|
{move || {
|
||||||
|
let (d0, d1) = if is_move_stage {
|
||||||
|
matched_dice_used(&staged_moves.get(), dice)
|
||||||
|
} else {
|
||||||
|
(false, false)
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<Die value=dice.0 used=d0 />
|
||||||
|
<Die value=dice.1 used=d1 />
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{show_roll.then(|| view! {
|
||||||
|
<button class="btn btn-primary" on:click=move |_| {
|
||||||
|
cmd_tx_roll.unbounded_send(NetCommand::Action(PlayerAction::Roll)).ok();
|
||||||
|
}>{t!(i18n, roll_dice)}</button>
|
||||||
|
})}
|
||||||
|
{show_hold_go.then(|| view! {
|
||||||
|
<button class="btn btn-primary" on:click=move |_| {
|
||||||
|
cmd_tx_go.unbounded_send(NetCommand::Action(PlayerAction::Go)).ok();
|
||||||
|
}>{t!(i18n, go)}</button>
|
||||||
|
})}
|
||||||
|
{is_move_stage.then(|| view! {
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
disabled=move || 2 <= staged_moves.get().len()
|
||||||
|
on:click=move |_| {
|
||||||
|
selected_origin.set(None);
|
||||||
|
staged_moves.update(|v| v.push((0, 0)));
|
||||||
|
}
|
||||||
|
>{t!(i18n, empty_move)}</button>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
|
||||||
// ── Player score (below board) ────────────────────────────────────
|
// ── Player score (below board) ────────────────────────────────────
|
||||||
<PlayerScorePanel score=my_score jans=my_jans is_you=true />
|
<PlayerScorePanel score=my_score jans=my_jans is_you=true />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ fn jan_row(idx: usize, entry: JanEntry, expanded: RwSignal<Option<usize>>) -> im
|
||||||
};
|
};
|
||||||
|
|
||||||
let moves = entry.moves.clone();
|
let moves = entry.moves.clone();
|
||||||
let moves_hover = entry.moves.clone();
|
|
||||||
// RwSignal is Copy so it can be captured by both closures independently.
|
|
||||||
let hovered = use_context::<RwSignal<Vec<(CheckerMove, CheckerMove)>>>();
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -71,16 +68,6 @@ fn jan_row(idx: usize, entry: JanEntry, expanded: RwSignal<Option<usize>>) -> im
|
||||||
*s = if *s == Some(idx) { None } else { Some(idx) };
|
*s = if *s == Some(idx) { None } else { Some(idx) };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
on:mouseenter=move |_| {
|
|
||||||
if let Some(h) = hovered {
|
|
||||||
h.set(moves_hover.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on:mouseleave=move |_| {
|
|
||||||
if let Some(h) = hovered {
|
|
||||||
h.set(vec![]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span class="jan-label">{label}</span>
|
<span class="jan-label">{label}</span>
|
||||||
<span class="jan-tag">{double_tag}</span>
|
<span class="jan-tag">{double_tag}</span>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
mod board;
|
mod board;
|
||||||
pub use board::{Board, CheckerMove};
|
pub use board::CheckerMove;
|
||||||
|
|
||||||
mod dice;
|
mod dice;
|
||||||
pub use dice::{Dice, DiceRoller};
|
pub use dice::{Dice, DiceRoller};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue