/// /** * Test Pattern: Battleship Pass-and-Play * * Tests the core game mechanics of the pass-and-play Battleship game: * - Initial state and transition flow * - Player ready transitions * - Firing shots (hits and misses) * - Turn switching * - Pass device flow * - Ship sinking detection * - Win condition * - Game reset * * Ship Positions (from createDefaultShips1 and createDefaultShips2): * * Player 1's ships (Player 2 targets these): * - Carrier (5): row 0, cols 0-4 (horizontal) * - Battleship (4): row 2, cols 1-4 (horizontal) * - Cruiser (3): rows 4-6, col 3 (vertical) * - Submarine (3): row 5, cols 7-9 (horizontal) * - Destroyer (2): rows 8-9, col 5 (vertical) * * Player 2's ships (Player 1 targets these): * - Carrier (5): rows 1-5, col 2 (vertical) * - Battleship (4): row 0, cols 6-9 (horizontal) * - Cruiser (3): row 3, cols 0-2 (horizontal) * - Submarine (3): rows 7-9, col 4 (vertical) * - Destroyer (2): row 9, cols 8-9 (horizontal) * * Run: deno task ct test packages/patterns/battleship/pass-and-play/main.test.tsx --verbose */ import { action, computed, pattern } from "commontools"; import Battleship, { type SquareState } from "./main.tsx"; export default pattern(() => { // Instantiate the battleship pattern const game = Battleship({}); // ========================================================================== // Actions using the exported handlers via closure (action() approach) // ========================================================================== // Player ready action - transitions from transition screen to game view const action_player_ready = action(() => { game.playerReady.send(); }); // Pass device action const action_pass_device = action(() => { game.passDevice.send(); }); // Reset game action const action_reset_game = action(() => { game.resetGame.send(); }); // Fire at a miss location (row 9, col 0 - empty for Player 2's board) const action_fire_miss = action(() => { game.fireShot.send({ row: 9, col: 0 }); }); // Player 2 fires at Player 1's carrier (row 0, col 0) const action_p2_fire_hit_carrier = action(() => { game.fireShot.send({ row: 0, col: 0 }); }); // ========================================================================== // Assertions - access game state properties directly through OpaqueCell proxy // ========================================================================== // Initial state assertions const assert_initial_phase_playing = computed( () => game.game.phase === "playing", ); const assert_initial_turn_player1 = computed( () => game.game.currentTurn === 1, ); const assert_initial_viewingAs_null = computed( () => game.game.viewingAs === null, ); const assert_initial_winner_null = computed( () => game.game.winner === null, ); const assert_initial_not_awaiting_pass = computed( () => game.game.awaitingPass === false, ); // After playerReady - Player 1 is now viewing const assert_viewingAs_player1 = computed( () => game.game.viewingAs === 1, ); const assert_still_turn_player1 = computed( () => game.game.currentTurn === 1, ); // After firing a miss const assert_shot_recorded_miss = computed(() => { return game.game.player2.shots[9][0] === "miss"; }); const assert_turn_switched_to_player2 = computed( () => game.game.currentTurn === 2, ); const assert_awaiting_pass_after_shot = computed( () => game.game.awaitingPass === true, ); // After passDevice const assert_viewingAs_null_after_pass = computed( () => game.game.viewingAs === null, ); const assert_not_awaiting_pass_after_pass = computed( () => game.game.awaitingPass === false, ); // After player 2 ready const assert_viewingAs_player2 = computed( () => game.game.viewingAs === 2, ); // After player 2 fires a hit const assert_shot_recorded_hit = computed(() => { // Player 2 fires at Player 1's carrier at row 0, col 0 return game.game.player1.shots[0][0] === "hit"; }); const assert_turn_back_to_player1 = computed( () => game.game.currentTurn === 1, ); // After reset const assert_reset_phase_playing = computed( () => game.game.phase === "playing", ); const assert_reset_turn_player1 = computed( () => game.game.currentTurn === 1, ); const assert_reset_viewingAs_null = computed( () => game.game.viewingAs === null, ); const assert_reset_shots_cleared = computed(() => { // All shots should be empty after reset const p1Clear = game.game.player1.shots.every((row: SquareState[]) => row.every((cell: SquareState) => cell === "empty") ); const p2Clear = game.game.player2.shots.every((row: SquareState[]) => row.every((cell: SquareState) => cell === "empty") ); return p1Clear && p2Clear; }); // ========================================================================== // Test Sequence - array of { assertion } or { action } objects // ========================================================================== return { tests: [ // === Test 1: Initial state === { assertion: assert_initial_phase_playing }, { assertion: assert_initial_turn_player1 }, { assertion: assert_initial_viewingAs_null }, { assertion: assert_initial_winner_null }, { assertion: assert_initial_not_awaiting_pass }, // === Test 2: Player 1 ready === { action: action_player_ready }, { assertion: assert_viewingAs_player1 }, { assertion: assert_still_turn_player1 }, // === Test 3: Player 1 fires a miss === { action: action_fire_miss }, { assertion: assert_shot_recorded_miss }, { assertion: assert_turn_switched_to_player2 }, { assertion: assert_awaiting_pass_after_shot }, // === Test 4: Pass device === { action: action_pass_device }, { assertion: assert_viewingAs_null_after_pass }, { assertion: assert_not_awaiting_pass_after_pass }, // === Test 5: Player 2 ready === { action: action_player_ready }, { assertion: assert_viewingAs_player2 }, // === Test 6: Player 2 fires a hit === { action: action_p2_fire_hit_carrier }, { assertion: assert_shot_recorded_hit }, { assertion: assert_turn_back_to_player1 }, // === Test 7: Pass and reset === { action: action_pass_device }, { action: action_reset_game }, { assertion: assert_reset_phase_playing }, { assertion: assert_reset_turn_player1 }, { assertion: assert_reset_viewingAs_null }, { assertion: assert_reset_shots_cleared }, ], game, }; });