feat(engine): tighter tableau fan for face-down cards

Face-down cards in tableau columns now use a TABLEAU_FACEDOWN_FAN_FRAC
of 0.12 instead of the full 0.25, so the visible face-up portion of
each column is easier to read at a glance — matching standard Klondike
rendering where only the playable cards are prominently spread.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-27 03:18:31 +00:00
parent bc021acfd0
commit 0b0e0180c0
+44 -8
View File
@@ -26,9 +26,12 @@ use crate::layout::{Layout, LayoutResource};
use crate::resources::GameStateResource; use crate::resources::GameStateResource;
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource}; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
/// Fraction of card height used as vertical offset between stacked tableau cards. /// Fraction of card height used as vertical offset between face-up tableau cards.
pub const TABLEAU_FAN_FRAC: f32 = 0.25; pub const TABLEAU_FAN_FRAC: f32 = 0.25;
/// Tighter fan for face-down cards in the tableau — just enough to show the stack.
const TABLEAU_FACEDOWN_FAN_FRAC: f32 = 0.12;
/// Fraction of card height used as a tiny offset between stacked cards in /// Fraction of card height used as a tiny offset between stacked cards in
/// non-tableau piles, so stacking is visible. /// non-tableau piles, so stacking is visible.
const STACK_FAN_FRAC: f32 = 0.003; const STACK_FAN_FRAC: f32 = 0.003;
@@ -196,16 +199,24 @@ fn card_positions(game: &GameState, layout: &Layout) -> Vec<(Card, Vec2, f32)> {
continue; continue;
}; };
let is_tableau = matches!(pile_type, PileType::Tableau(_)); let is_tableau = matches!(pile_type, PileType::Tableau(_));
let fan_y = if is_tableau {
-layout.card_size.y * TABLEAU_FAN_FRAC
} else {
0.0
};
for (i, card) in pile.cards.iter().enumerate() { // Tableau uses a two-speed fan: face-down cards are packed tighter
let pos = Vec2::new(base.x, base.y + fan_y * i as f32); // than face-up cards so the visible (playable) portion stands out.
// Non-tableau piles stack with a negligible offset.
let cards = &pile.cards;
let mut y_offset = 0.0_f32;
for (i, card) in cards.iter().enumerate() {
let pos = Vec2::new(base.x, base.y + y_offset);
let z = 1.0 + (i as f32) * STACK_FAN_FRAC; let z = 1.0 + (i as f32) * STACK_FAN_FRAC;
out.push((card.clone(), pos, z)); out.push((card.clone(), pos, z));
if is_tableau {
let step = if card.face_up {
TABLEAU_FAN_FRAC
} else {
TABLEAU_FACEDOWN_FAN_FRAC
};
y_offset -= layout.card_size.y * step;
}
} }
} }
out out
@@ -501,4 +512,29 @@ mod tests {
assert!(w[0] > w[1]); assert!(w[0] > w[1]);
} }
} }
#[test]
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0));
let positions = card_positions(&g, &layout);
// Tableau(6) has 7 cards: 6 face-down + 1 face-up on top.
// Each face-down card contributes TABLEAU_FACEDOWN_FAN_FRAC to the column span.
// Total span should be 6 * FACEDOWN < 6 * TABLEAU_FAN_FRAC (the old uniform value).
let col6_base = layout.pile_positions[&PileType::Tableau(6)];
let mut col6_ys: Vec<f32> = positions
.iter()
.filter(|(_, pos, _)| (pos.x - col6_base.x).abs() < 1e-3)
.map(|(_, pos, _)| pos.y)
.collect();
col6_ys.sort_by(|a, b| b.partial_cmp(a).unwrap());
assert_eq!(col6_ys.len(), 7);
let actual_span = col6_ys[0] - col6_ys[6];
let uniform_span = 6.0 * TABLEAU_FAN_FRAC * layout.card_size.y;
assert!(
actual_span < uniform_span,
"tighter face-down fan should reduce column span ({actual_span:.1} >= uniform {uniform_span:.1})"
);
}
} }