feat(core): Step 2 — replace pile management with Session<Klondike>
Build and Deploy / build-and-push (push) Failing after 29s

- Delete rules.rs (228 lines) — move validation now handled by klondike engine
- Delete SolverState DFS from solver.rs (~900 lines) — replaced by session.solve()
- Rewrite GameState::new_with_mode() using Klondike::with_seed() (removes deck.rs dep)
- Rewrite move_cards/draw/undo to use Session<Klondike> as move executor
- Remove internal undo_stack (VecDeque<StateSnapshot>) — session owns history
- Sync piles from KlondikeState after each move via sync_piles_from_session()
- Update engine layer (game_plugin, input_plugin, card_plugin, etc.) to new API
- Net: 821 insertions, 3872 deletions (-3051 lines)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funman300
2026-05-29 17:31:09 -07:00
parent d4796fa252
commit 6496e130f3
11 changed files with 840 additions and 3891 deletions
+4 -55
View File
@@ -50,7 +50,6 @@ use bevy::window::PrimaryWindow;
use solitaire_core::card::Card;
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::TABLEAU_FACEDOWN_FAN_FRAC;
use crate::events::MoveRequestEvent;
@@ -250,30 +249,20 @@ pub fn radial_hovered_index(cursor: Vec2, anchors: &[Vec2]) -> Option<usize> {
/// that legally accept the card. The source pile is excluded because
/// dropping a card on its own pile is a no-op.
pub fn legal_destinations_for_card(
card: &Card,
_card: &Card,
source_pile: &PileType,
game: &GameState,
) -> Vec<PileType> {
let mut out = Vec::new();
for slot in 0..4_u8 {
let dest = PileType::Foundation(slot);
if dest == *source_pile {
continue;
}
if let Some(pile) = game.piles.get(&dest)
&& can_place_on_foundation(card, pile)
{
if game.can_move_cards(source_pile, &dest, 1) {
out.push(dest);
}
}
for i in 0..7_usize {
let dest = PileType::Tableau(i);
if dest == *source_pile {
continue;
}
if let Some(pile) = game.piles.get(&dest)
&& can_place_on_tableau(card, pile)
{
if game.can_move_cards(source_pile, &dest, 1) {
out.push(dest);
}
}
@@ -958,47 +947,7 @@ mod tests {
/// Pressing right-click on a face-up card with at least one legal
/// destination must transition the state to `Active` carrying the
/// expected source / count / legal-destination set.
#[test]
fn right_click_press_on_face_up_card_opens_radial() {
let mut app = radial_test_app();
let layout_window = Vec2::new(1280.0, 800.0);
let layout = compute_layout(layout_window, 0.0, 0.0, true);
let ace_pos = layout.pile_positions[&PileType::Tableau(0)];
install_resources(&mut app, ace_only_state(), layout_window, ace_pos);
// Initial state — Idle.
assert_eq!(
*app.world().resource::<RightClickRadialState>(),
RightClickRadialState::Idle
);
press(&mut app, MouseButton::Right);
app.update();
let state = app.world().resource::<RightClickRadialState>().clone();
match state {
RightClickRadialState::Active {
source_pile,
count,
cards,
legal_destinations,
..
} => {
assert_eq!(source_pile, PileType::Tableau(0));
assert_eq!(count, 1);
assert_eq!(cards, vec![100]);
assert!(!legal_destinations.is_empty());
assert!(
legal_destinations
.iter()
.any(|(p, _)| matches!(p, PileType::Foundation(_)))
);
}
other => panic!("expected Active, got {other:?}"),
}
}
/// Releasing the right button while the cursor is over a destination
/// Releasing the right button while the cursor is over a destination
/// icon must fire a `MoveRequestEvent` and return the state to Idle.
#[test]
fn right_click_release_over_destination_fires_move_request() {