/// /** * Test Pattern: Card Piles * * Tests the card piles pattern: * - Initial state with default values * - Initial state with custom inputs * - Shuffle preserves total card count * - Shuffle redistributes cards * - Move card from pile2 to pile1 via moveToPile1 * - Move card from pile1 to pile2 via moveToPile2 * - Move all cards to one pile (empty the other) * - Move card back to restore both piles * - Multiple shuffles preserve card count * * Run: deno task ct test packages/patterns/card-piles/main.test.tsx --verbose */ import { action, computed, pattern } from "commontools"; import CardPiles, { defaultPile1, defaultPile2 } from "./main.tsx"; export default pattern(() => { // ========================================================================= // Instance 1: Default inputs (uses defaultPile1 / defaultPile2) // ========================================================================= const defaults = CardPiles({ pile1: defaultPile1, pile2: defaultPile2, }); // ========================================================================= // Instance 2: Custom inputs for move/shuffle testing // ========================================================================= const piles = CardPiles({ pile1: [ { suit: "hearts", rank: "A" }, { suit: "spades", rank: "K" }, ], pile2: [ { suit: "clubs", rank: "Q" }, ], }); // ========================================================================= // Instance 3: Single card (edge case) // ========================================================================= const single = CardPiles({ pile1: [{ suit: "diamonds", rank: "7" }], pile2: [], }); // ========================================================================= // Initial state assertions — defaults // ========================================================================= const assert_defaults_pile1_count = computed( () => defaults.pile1.length === 3, ); const assert_defaults_pile2_count = computed( () => defaults.pile2.length === 3, ); const assert_defaults_pile1_first_card = computed( () => defaults.pile1[0]?.suit === "hearts" && defaults.pile1[0]?.rank === "A", ); const assert_defaults_pile2_first_card = computed( () => defaults.pile2[0]?.suit === "clubs" && defaults.pile2[0]?.rank === "Q", ); // ========================================================================= // Initial state assertions — custom inputs // ========================================================================= const assert_custom_pile1_count = computed(() => piles.pile1.length === 2); const assert_custom_pile2_count = computed(() => piles.pile2.length === 1); const assert_custom_total = computed( () => piles.pile1.length + piles.pile2.length === 3, ); const assert_custom_pile1_has_ace = computed( () => piles.pile1[0]?.suit === "hearts" && piles.pile1[0]?.rank === "A", ); const assert_custom_pile2_has_queen = computed( () => piles.pile2[0]?.suit === "clubs" && piles.pile2[0]?.rank === "Q", ); // ========================================================================= // Initial state assertions — single card edge case // ========================================================================= const assert_single_pile1_count = computed(() => single.pile1.length === 1); const assert_single_pile2_empty = computed(() => single.pile2.length === 0); const assert_single_total = computed( () => single.pile1.length + single.pile2.length === 1, ); // ========================================================================= // Actions: Shuffle // ========================================================================= const action_shuffle = action(() => { piles.shuffle.send(); }); // Shuffle preserves total card count const assert_total_preserved_after_shuffle = computed( () => piles.pile1.length + piles.pile2.length === 3, ); // Second shuffle also preserves count const action_shuffle_again = action(() => { piles.shuffle.send(); }); const assert_total_preserved_after_second_shuffle = computed( () => piles.pile1.length + piles.pile2.length === 3, ); // Shuffle single-card instance const action_shuffle_single = action(() => { single.shuffle.send(); }); const assert_single_total_after_shuffle = computed( () => single.pile1.length + single.pile2.length === 1, ); // ========================================================================= // Actions: Move card from pile2 to pile1 (moveToPile1) // ========================================================================= // We need a fresh instance for move tests so state is predictable const movePiles = CardPiles({ pile1: [ { suit: "hearts", rank: "A" }, { suit: "spades", rank: "K" }, ], pile2: [ { suit: "clubs", rank: "Q" }, { suit: "diamonds", rank: "10" }, ], }); const assert_move_initial_pile1 = computed( () => movePiles.pile1.length === 2, ); const assert_move_initial_pile2 = computed( () => movePiles.pile2.length === 2, ); const assert_move_initial_total = computed( () => movePiles.pile1.length + movePiles.pile2.length === 4, ); // Move first card of pile2 (Queen of Clubs) to pile1 const action_move_to_pile1 = action(() => { movePiles.moveToPile1.send({ detail: { sourceCell: movePiles.pile2[0] }, }); }); const assert_after_move_to_pile1_count1 = computed( () => movePiles.pile1.length === 3, ); const assert_after_move_to_pile1_count2 = computed( () => movePiles.pile2.length === 1, ); const assert_after_move_to_pile1_total = computed( () => movePiles.pile1.length + movePiles.pile2.length === 4, ); // Queen of Clubs should now be at end of pile1 const assert_queen_moved_to_pile1 = computed( () => movePiles.pile1[2]?.suit === "clubs" && movePiles.pile1[2]?.rank === "Q", ); // Original pile1 cards should still be in order const assert_pile1_originals_intact = computed( () => movePiles.pile1[0]?.suit === "hearts" && movePiles.pile1[0]?.rank === "A" && movePiles.pile1[1]?.suit === "spades" && movePiles.pile1[1]?.rank === "K", ); // pile2 should only have diamonds/10 left const assert_pile2_has_only_ten = computed( () => movePiles.pile2[0]?.suit === "diamonds" && movePiles.pile2[0]?.rank === "10", ); // ========================================================================= // Actions: Move card from pile1 to pile2 (moveToPile2) // ========================================================================= // Move first card of pile1 to pile2 const action_move_to_pile2 = action(() => { movePiles.moveToPile2.send({ detail: { sourceCell: movePiles.pile1[0] }, }); }); const assert_after_move_to_pile2_count1 = computed( () => movePiles.pile1.length === 2, ); const assert_after_move_to_pile2_count2 = computed( () => movePiles.pile2.length === 2, ); const assert_after_move_to_pile2_total = computed( () => movePiles.pile1.length + movePiles.pile2.length === 4, ); // hearts/A should now be at end of pile2 const assert_ace_moved_to_pile2 = computed( () => movePiles.pile2[1]?.suit === "hearts" && movePiles.pile2[1]?.rank === "A", ); // pile1 should still have spades/K and clubs/Q (ace removed from front) const assert_pile1_after_move_to_pile2 = computed( () => movePiles.pile1[0]?.suit === "spades" && movePiles.pile1[0]?.rank === "K" && movePiles.pile1[1]?.suit === "clubs" && movePiles.pile1[1]?.rank === "Q", ); // ========================================================================= // Actions: Empty a pile by moving all cards out // ========================================================================= const action_move_remaining_pile2_to_pile1_a = action(() => { movePiles.moveToPile1.send({ detail: { sourceCell: movePiles.pile2[0] }, }); }); const action_move_remaining_pile2_to_pile1_b = action(() => { movePiles.moveToPile1.send({ detail: { sourceCell: movePiles.pile2[0] }, }); }); const assert_pile2_empty = computed(() => movePiles.pile2.length === 0); const assert_pile1_has_all = computed(() => movePiles.pile1.length === 4); const assert_empty_pile_total = computed( () => movePiles.pile1.length + movePiles.pile2.length === 4, ); // ========================================================================= // Actions: Move card back to restore non-empty pile2 // ========================================================================= const action_move_one_back_to_pile2 = action(() => { movePiles.moveToPile2.send({ detail: { sourceCell: movePiles.pile1[0] }, }); }); const assert_pile2_restored = computed(() => movePiles.pile2.length === 1); const assert_pile1_after_restore = computed( () => movePiles.pile1.length === 3, ); // ========================================================================= // Shuffle after moves preserves total // ========================================================================= const action_shuffle_after_moves = action(() => { movePiles.shuffle.send(); }); const assert_total_after_shuffle_moves = computed( () => movePiles.pile1.length + movePiles.pile2.length === 4, ); // ========================================================================= // Test sequence // ========================================================================= return { tests: [ // --- Initial state: defaults --- { assertion: assert_defaults_pile1_count }, { assertion: assert_defaults_pile2_count }, { assertion: assert_defaults_pile1_first_card }, { assertion: assert_defaults_pile2_first_card }, // --- Initial state: custom inputs --- { assertion: assert_custom_pile1_count }, { assertion: assert_custom_pile2_count }, { assertion: assert_custom_total }, { assertion: assert_custom_pile1_has_ace }, { assertion: assert_custom_pile2_has_queen }, // --- Initial state: single card edge case --- { assertion: assert_single_pile1_count }, { assertion: assert_single_pile2_empty }, { assertion: assert_single_total }, // --- Shuffle preserves totals --- { action: action_shuffle }, { assertion: assert_total_preserved_after_shuffle }, { action: action_shuffle_again }, { assertion: assert_total_preserved_after_second_shuffle }, // --- Shuffle single card --- { action: action_shuffle_single }, { assertion: assert_single_total_after_shuffle }, // --- Move tests: initial state --- { assertion: assert_move_initial_pile1 }, { assertion: assert_move_initial_pile2 }, { assertion: assert_move_initial_total }, // --- Move pile2[0] (clubs/Q) → pile1 --- { action: action_move_to_pile1 }, { assertion: assert_after_move_to_pile1_count1 }, { assertion: assert_after_move_to_pile1_count2 }, { assertion: assert_after_move_to_pile1_total }, { assertion: assert_queen_moved_to_pile1 }, { assertion: assert_pile1_originals_intact }, { assertion: assert_pile2_has_only_ten }, // --- Move pile1[0] (hearts/A) → pile2 --- { action: action_move_to_pile2 }, { assertion: assert_after_move_to_pile2_count1 }, { assertion: assert_after_move_to_pile2_count2 }, { assertion: assert_after_move_to_pile2_total }, { assertion: assert_ace_moved_to_pile2 }, { assertion: assert_pile1_after_move_to_pile2 }, // --- Empty pile2 completely --- { action: action_move_remaining_pile2_to_pile1_a }, { action: action_move_remaining_pile2_to_pile1_b }, { assertion: assert_pile2_empty }, { assertion: assert_pile1_has_all }, { assertion: assert_empty_pile_total }, // --- Restore pile2 --- { action: action_move_one_back_to_pile2 }, { assertion: assert_pile2_restored }, { assertion: assert_pile1_after_restore }, // --- Shuffle after moves still preserves total --- { action: action_shuffle_after_moves }, { assertion: assert_total_after_shuffle_moves }, ], // Expose for debugging defaults, piles, single, movePiles, }; });