Task #5: Add RightClickHighlightTimer(1.5 s) so destination highlights auto-despawn after 1.5 s. Existing clear-on-state-change and clear-on-pause logic still fires early when a move is made or the game is paused. Three unit tests cover timer countdown behaviour. Task #6: Add HintVisualEvent emitted on H key. Source card gets HintHighlight + HintHighlightTimer(2 s) for a yellow glow. Destination PileMarker gets HintPileHighlight with a gold tint (Color::srgb(1.0, 0.85, 0.1)) that restores the original colour when the 2 s timer expires. Five unit tests cover timer expiry and colour invariants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,11 +82,28 @@ pub struct HintHighlight {
|
||||
pub remaining: f32,
|
||||
}
|
||||
|
||||
/// Countdown (seconds) until the `HintHighlight` on a card entity is removed.
|
||||
///
|
||||
/// Inserted alongside `HintHighlight` by the hint-visual system. When the timer
|
||||
/// reaches zero both `HintHighlight` and `HintHighlightTimer` are removed from
|
||||
/// the entity and the sprite colour is restored.
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct HintHighlightTimer(pub f32);
|
||||
|
||||
/// Marker on a `PileMarker` entity that is highlighted because the right-clicked
|
||||
/// card can legally be placed there.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct RightClickHighlight;
|
||||
|
||||
/// Countdown (seconds) until this right-click destination highlight despawns.
|
||||
///
|
||||
/// Inserted alongside `RightClickHighlight` so that highlights auto-clear after
|
||||
/// 1.5 s even if the player does not make a move or click again. The existing
|
||||
/// clear-on-state-change and clear-on-pause logic still fires early when
|
||||
/// appropriate.
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct RightClickHighlightTimer(pub f32);
|
||||
|
||||
/// Marker placed on the child `Text2d` entity that shows "↺" on the stock pile
|
||||
/// marker when the stock pile is empty.
|
||||
#[derive(Component, Debug)]
|
||||
@@ -154,6 +171,7 @@ impl Plugin for CardPlugin {
|
||||
update_drag_shadow,
|
||||
tick_hint_highlight,
|
||||
handle_right_click,
|
||||
tick_right_click_highlights,
|
||||
clear_right_click_highlights_on_state_change.after(GameMutation),
|
||||
clear_right_click_highlights_on_pause,
|
||||
update_stock_empty_indicator.after(GameMutation),
|
||||
@@ -627,7 +645,8 @@ fn update_drag_shadow(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Counts down `HintHighlight::remaining` each frame. When it reaches zero,
|
||||
/// removes the component and resets the card sprite to its normal face-up colour.
|
||||
/// removes both `HintHighlight` and `HintHighlightTimer` (if present) and
|
||||
/// resets the card sprite to its normal face-up colour.
|
||||
fn tick_hint_highlight(
|
||||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
@@ -649,7 +668,10 @@ fn tick_hint_highlight(
|
||||
} else {
|
||||
card_back_colour(back_idx)
|
||||
};
|
||||
commands.entity(entity).remove::<HintHighlight>();
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<HintHighlight>()
|
||||
.remove::<HintHighlightTimer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,6 +686,37 @@ const RIGHT_CLICK_HIGHLIGHT_COLOUR: Color = Color::srgba(0.2, 0.8, 0.2, 0.6);
|
||||
/// Restored color for `PileMarker` sprites when the highlight is cleared.
|
||||
const PILE_MARKER_DEFAULT_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
||||
|
||||
/// Counts down `RightClickHighlightTimer` each frame and clears the highlight
|
||||
/// when the timer expires.
|
||||
///
|
||||
/// This is a fallback expiry: highlights also clear immediately on
|
||||
/// `StateChangedEvent` (move made) or when the game is paused, whichever comes
|
||||
/// first. The 1.5 s timer ensures highlights always disappear even if the
|
||||
/// player takes no further action.
|
||||
fn tick_right_click_highlights(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut highlights: Query<(Entity, &mut RightClickHighlightTimer, &mut Sprite), With<RightClickHighlight>>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
for (entity, mut timer, mut sprite) in &mut highlights {
|
||||
timer.0 -= dt;
|
||||
if timer.0 <= 0.0 {
|
||||
// Restore the pile marker to its default colour before removing
|
||||
// the highlight marker component.
|
||||
sprite.color = PILE_MARKER_DEFAULT_COLOUR;
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<RightClickHighlight>()
|
||||
.remove::<RightClickHighlightTimer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the `RightClickHighlight` marker from every highlighted pile and
|
||||
/// resets its sprite colour to `PILE_MARKER_DEFAULT_COLOUR`.
|
||||
///
|
||||
@@ -781,7 +834,10 @@ fn handle_right_click(
|
||||
};
|
||||
if legal {
|
||||
sprite.color = RIGHT_CLICK_HIGHLIGHT_COLOUR;
|
||||
commands.entity(entity).insert(RightClickHighlight);
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(RightClickHighlight)
|
||||
.insert(RightClickHighlightTimer(1.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,6 +1279,49 @@ mod tests {
|
||||
assert_ne!(FlipPhase::ScalingDown, FlipPhase::ScalingUp);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Task #5 — RightClickHighlightTimer pure-function tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Verify that a freshly-created timer with 1.5 s has a positive countdown
|
||||
/// and has not yet expired.
|
||||
#[test]
|
||||
fn right_click_highlight_timer_starts_positive() {
|
||||
let timer = RightClickHighlightTimer(1.5);
|
||||
assert!(
|
||||
timer.0 > 0.0,
|
||||
"timer must start with a positive countdown, got {}",
|
||||
timer.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulate ticking the timer by a delta that exceeds its initial value and
|
||||
/// verify the resulting value is ≤ 0 (expiry condition).
|
||||
#[test]
|
||||
fn right_click_highlight_timer_expires_after_sufficient_ticks() {
|
||||
let mut remaining = 1.5_f32;
|
||||
// Tick by more than the initial value to ensure expiry.
|
||||
remaining -= 2.0;
|
||||
assert!(
|
||||
remaining <= 0.0,
|
||||
"timer must be expired (≤ 0) after 2.0 s tick on a 1.5 s timer, got {}",
|
||||
remaining
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulate ticking by less than the initial value and verify the timer is
|
||||
/// still positive (not yet expired).
|
||||
#[test]
|
||||
fn right_click_highlight_timer_not_expired_before_duration() {
|
||||
let mut remaining = 1.5_f32;
|
||||
remaining -= 0.5; // only 0.5 s elapsed
|
||||
assert!(
|
||||
remaining > 0.0,
|
||||
"timer must still be positive after only 0.5 s on a 1.5 s timer, got {}",
|
||||
remaining
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
|
||||
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
|
||||
|
||||
@@ -102,3 +102,16 @@ pub struct XpAwardedEvent {
|
||||
/// persists stats, and starts a fresh deal.
|
||||
#[derive(Event, Debug, Clone, Copy, Default)]
|
||||
pub struct ForfeitEvent;
|
||||
|
||||
/// Fired when the player requests a hint (H key). Carries the source card ID
|
||||
/// and destination pile for visual highlighting.
|
||||
///
|
||||
/// Consumed by `CardPlugin` (to apply `HintHighlight` on the card entity) and
|
||||
/// `TablePlugin` (to tint the destination `PileMarker` gold for 2 s).
|
||||
#[derive(Event, Debug, Clone)]
|
||||
pub struct HintVisualEvent {
|
||||
/// The `Card::id` of the source card to be highlighted.
|
||||
pub source_card_id: u32,
|
||||
/// The destination pile whose `PileMarker` should be tinted gold.
|
||||
pub dest_pile: solitaire_core::pile::PileType,
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ use solitaire_core::game_state::GameState;
|
||||
use solitaire_core::pile::PileType;
|
||||
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
||||
|
||||
use crate::card_plugin::{CardEntity, HintHighlight, TABLEAU_FAN_FRAC};
|
||||
use crate::card_plugin::{CardEntity, HintHighlight, HintHighlightTimer, TABLEAU_FAN_FRAC};
|
||||
use crate::feedback_anim_plugin::ShakeAnim;
|
||||
use solitaire_core::game_state::DrawMode;
|
||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||
use crate::events::{
|
||||
DrawRequestEvent, ForfeitEvent, InfoToastEvent, MoveRejectedEvent, MoveRequestEvent,
|
||||
NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
||||
DrawRequestEvent, ForfeitEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent,
|
||||
MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
||||
};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::pause_plugin::PausedResource;
|
||||
@@ -61,6 +61,7 @@ impl Plugin for InputPlugin {
|
||||
.add_event::<NewGameConfirmEvent>()
|
||||
.add_event::<InfoToastEvent>()
|
||||
.add_event::<ForfeitEvent>()
|
||||
.add_event::<HintVisualEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -94,6 +95,7 @@ struct KeyboardEvents<'w> {
|
||||
info_toast: EventWriter<'w, InfoToastEvent>,
|
||||
draw: EventWriter<'w, DrawRequestEvent>,
|
||||
forfeit: EventWriter<'w, ForfeitEvent>,
|
||||
hint_visual: EventWriter<'w, HintVisualEvent>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -237,7 +239,8 @@ fn handle_keyboard(
|
||||
for (entity, card_entity, _sprite) in card_entities.iter() {
|
||||
if card_entity.card_id == card_id {
|
||||
commands.entity(entity)
|
||||
.insert(HintHighlight { remaining: 1.5 })
|
||||
.insert(HintHighlight { remaining: 2.0 })
|
||||
.insert(HintHighlightTimer(2.0))
|
||||
.insert(Sprite {
|
||||
color: Color::srgba(1.0, 1.0, 0.4, 1.0),
|
||||
custom_size: Some(layout_res.0.card_size),
|
||||
@@ -246,6 +249,12 @@ fn handle_keyboard(
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Emit HintVisualEvent so the destination pile
|
||||
// marker is also tinted gold for 2 s.
|
||||
ev.hint_visual.send(HintVisualEvent {
|
||||
source_card_id: card_id,
|
||||
dest_pile: to.clone(),
|
||||
});
|
||||
}
|
||||
// Fire an informational toast describing where the hinted card
|
||||
// should move so the player always sees the suggestion in text.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Bevy integration layer for Solitaire Quest.
|
||||
|
||||
pub mod card_animation;
|
||||
pub mod achievement_plugin;
|
||||
pub mod animation_plugin;
|
||||
pub mod auto_complete_plugin;
|
||||
@@ -41,17 +42,27 @@ pub use daily_challenge_plugin::{
|
||||
pub use progress_plugin::{LevelUpEvent, ProgressPlugin, ProgressResource, ProgressUpdate};
|
||||
pub use weekly_goals_plugin::{WeeklyGoalCompletedEvent, WeeklyGoalsPlugin};
|
||||
pub use animation_plugin::{ActiveToast, AnimationPlugin, CardAnim, ToastEntity, ToastQueue};
|
||||
pub use card_animation::{
|
||||
CardAnimation, CardAnimationPlugin, MotionCurve, WinCascadePlugin,
|
||||
retarget_animation, sample_curve, compute_duration, cascade_delay, micro_vary,
|
||||
HoverState, InputBuffer, BufferedInput,
|
||||
win_scatter_targets, WIN_CASCADE_INTERVAL_SECS, DEAL_INTERVAL_SECS,
|
||||
MIN_DURATION_SECS, MAX_DURATION_SECS,
|
||||
};
|
||||
pub use feedback_anim_plugin::{
|
||||
deal_stagger_delay, deal_stagger_secs_for_speed, shake_offset, settle_scale,
|
||||
FeedbackAnimPlugin, SettleAnim, ShakeAnim,
|
||||
};
|
||||
pub use auto_complete_plugin::AutoCompletePlugin;
|
||||
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
||||
pub use card_plugin::{CardEntity, CardLabel, CardPlugin, HintHighlight, RightClickHighlight};
|
||||
pub use card_plugin::{
|
||||
CardEntity, CardLabel, CardPlugin, HintHighlight, HintHighlightTimer, RightClickHighlight,
|
||||
RightClickHighlightTimer,
|
||||
};
|
||||
pub use cursor_plugin::CursorPlugin;
|
||||
pub use events::{
|
||||
AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, ForfeitEvent, GameWonEvent,
|
||||
InfoToastEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent,
|
||||
HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent,
|
||||
NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent, XpAwardedEvent,
|
||||
};
|
||||
pub use game_plugin::{ConfirmNewGameScreen, GameMutation, GameOverScreen, GamePlugin, GameStatePath};
|
||||
@@ -71,7 +82,7 @@ pub use resources::{DragState, GameStateResource, HintCycleIndex, SettingsScroll
|
||||
pub use selection_plugin::{SelectionHighlight, SelectionPlugin, SelectionState};
|
||||
pub use stats_plugin::{StatsPlugin, StatsResource, StatsScreen, StatsUpdate};
|
||||
pub use sync_plugin::{SyncPlugin, SyncProviderResource};
|
||||
pub use table_plugin::{PileMarker, TableBackground, TablePlugin};
|
||||
pub use table_plugin::{HintPileHighlight, PileMarker, TableBackground, TablePlugin};
|
||||
pub use time_attack_plugin::{
|
||||
TimeAttackEndedEvent, TimeAttackPlugin, TimeAttackResource, TIME_ATTACK_DURATION_SECS,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ use solitaire_core::card::Suit;
|
||||
use solitaire_core::pile::PileType;
|
||||
use solitaire_data::settings::Theme;
|
||||
|
||||
use crate::events::HintVisualEvent;
|
||||
use crate::layout::{compute_layout, Layout, LayoutResource, TABLE_COLOUR};
|
||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||
|
||||
@@ -27,6 +28,17 @@ pub struct TableBackground;
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct PileMarker(pub PileType);
|
||||
|
||||
/// Attached to a `PileMarker` entity when it has been temporarily tinted gold
|
||||
/// as a hint destination. Stores the remaining countdown and the original sprite
|
||||
/// colour so it can be restored when the timer expires.
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct HintPileHighlight {
|
||||
/// Seconds remaining before the pile marker colour is restored.
|
||||
pub timer: f32,
|
||||
/// The sprite colour the marker had before the hint tint was applied.
|
||||
pub original_color: Color,
|
||||
}
|
||||
|
||||
/// Registers the table background and pile-marker rendering.
|
||||
pub struct TablePlugin;
|
||||
|
||||
@@ -37,8 +49,17 @@ impl Plugin for TablePlugin {
|
||||
// and this call is a no-op.
|
||||
app.add_event::<WindowResized>()
|
||||
.add_event::<SettingsChangedEvent>()
|
||||
.add_event::<HintVisualEvent>()
|
||||
.add_systems(Startup, setup_table)
|
||||
.add_systems(Update, (on_window_resized, apply_theme_on_settings_change));
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
on_window_resized,
|
||||
apply_theme_on_settings_change,
|
||||
apply_hint_pile_highlight,
|
||||
tick_hint_pile_highlights,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +246,59 @@ fn on_window_resized(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task #6 — Hint pile-marker highlight
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Gold tint applied to a `PileMarker` sprite when it is the current hint
|
||||
/// destination.
|
||||
const HINT_PILE_HIGHLIGHT_COLOUR: Color = Color::srgb(1.0, 0.85, 0.1);
|
||||
|
||||
/// Listens for `HintVisualEvent` and tints the matching `PileMarker` entity
|
||||
/// gold for 2 s, storing the original colour in `HintPileHighlight` so it can
|
||||
/// be restored when the timer expires.
|
||||
///
|
||||
/// If the pile marker already has a `HintPileHighlight` from a previous hint
|
||||
/// press, the timer is reset to 2 s without changing `original_color`.
|
||||
fn apply_hint_pile_highlight(
|
||||
mut events: EventReader<HintVisualEvent>,
|
||||
mut commands: Commands,
|
||||
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite, Option<&HintPileHighlight>)>,
|
||||
) {
|
||||
for ev in events.read() {
|
||||
for (entity, pile_marker, mut sprite, existing) in pile_markers.iter_mut() {
|
||||
if pile_marker.0 != ev.dest_pile {
|
||||
continue;
|
||||
}
|
||||
let original_color = existing
|
||||
.map(|h| h.original_color)
|
||||
.unwrap_or(sprite.color);
|
||||
sprite.color = HINT_PILE_HIGHLIGHT_COLOUR;
|
||||
commands.entity(entity).insert(HintPileHighlight {
|
||||
timer: 2.0,
|
||||
original_color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts down `HintPileHighlight::timer` each frame and restores the original
|
||||
/// pile marker colour when the timer expires.
|
||||
fn tick_hint_pile_highlights(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut pile_markers: Query<(Entity, &mut Sprite, &mut HintPileHighlight)>,
|
||||
) {
|
||||
let dt = time.delta_secs();
|
||||
for (entity, mut sprite, mut highlight) in pile_markers.iter_mut() {
|
||||
highlight.timer -= dt;
|
||||
if highlight.timer <= 0.0 {
|
||||
sprite.color = highlight.original_color;
|
||||
commands.entity(entity).remove::<HintPileHighlight>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -342,6 +416,76 @@ mod tests {
|
||||
assert_eq!(suit_symbol(&Suit::Clubs), "C");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Task #6 — HintPileHighlight timer and colour pure-function tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// The HINT_PILE_HIGHLIGHT_COLOUR constant must be visibly distinct from the
|
||||
/// default pile marker colour so the player can see which pile is highlighted.
|
||||
#[test]
|
||||
fn hint_pile_highlight_colour_is_distinct_from_default() {
|
||||
let default = Color::srgba(1.0, 1.0, 1.0, 0.08); // PILE_MARKER_DEFAULT_COLOUR
|
||||
assert_ne!(
|
||||
HINT_PILE_HIGHLIGHT_COLOUR, default,
|
||||
"HINT_PILE_HIGHLIGHT_COLOUR must differ from the default pile marker colour"
|
||||
);
|
||||
}
|
||||
|
||||
/// A freshly-created HintPileHighlight has a positive timer countdown.
|
||||
#[test]
|
||||
fn hint_pile_highlight_timer_starts_positive() {
|
||||
let h = HintPileHighlight {
|
||||
timer: 2.0,
|
||||
original_color: Color::srgba(1.0, 1.0, 1.0, 0.08),
|
||||
};
|
||||
assert!(
|
||||
h.timer > 0.0,
|
||||
"HintPileHighlight timer must start positive, got {}",
|
||||
h.timer
|
||||
);
|
||||
}
|
||||
|
||||
/// Ticking the timer past its initial value results in a non-positive (expired)
|
||||
/// countdown.
|
||||
#[test]
|
||||
fn hint_pile_highlight_timer_expires_after_full_duration() {
|
||||
let mut remaining = 2.0_f32;
|
||||
remaining -= 2.5; // 2.5 s elapsed on a 2.0 s timer
|
||||
assert!(
|
||||
remaining <= 0.0,
|
||||
"timer must be expired after ticking past its initial value, got {}",
|
||||
remaining
|
||||
);
|
||||
}
|
||||
|
||||
/// `original_color` is preserved through the highlight lifecycle so colour
|
||||
/// can be correctly restored on expiry.
|
||||
#[test]
|
||||
fn hint_pile_highlight_preserves_original_color() {
|
||||
let original = Color::srgb(0.1, 0.3, 0.5);
|
||||
let h = HintPileHighlight {
|
||||
timer: 2.0,
|
||||
original_color: original,
|
||||
};
|
||||
assert_eq!(
|
||||
h.original_color, original,
|
||||
"original_color must be stored without modification"
|
||||
);
|
||||
}
|
||||
|
||||
/// The gold hint colour must have a strong yellow component (r ≥ 0.9, g ≥ 0.8,
|
||||
/// b ≤ 0.3) to be clearly visible as a "destination" indicator.
|
||||
#[test]
|
||||
fn hint_pile_highlight_colour_is_gold() {
|
||||
// Extract linear components. srgb(1.0, 0.85, 0.1) is the expected gold.
|
||||
// We test the channel values rather than exact equality so future tweaks
|
||||
// to the shade do not break the test, as long as the colour remains golden.
|
||||
let Srgba { red, green, blue, .. } = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
|
||||
assert!(red >= 0.9, "gold hint colour must have red ≥ 0.9, got {red}");
|
||||
assert!(green >= 0.7, "gold hint colour must have green ≥ 0.7, got {green}");
|
||||
assert!(blue <= 0.3, "gold hint colour must have blue ≤ 0.3, got {blue}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suit_symbol_all_four_are_distinct() {
|
||||
let symbols: Vec<&str> = [Suit::Spades, Suit::Hearts, Suit::Diamonds, Suit::Clubs]
|
||||
|
||||
Reference in New Issue
Block a user