def mark_subfleet_of_biggest_remaining_ships(self) -> None: """Determine the size of the largest ship remaining in the Puzzle fleet and mark the entire subfleet of those ships onto the Puzzle board. If the board offers more available "slots" in which all subfleet ships can be marked, then branch the puzzle solving by marking the subfleet in each possible slot combination. If no ships are remaining in the fleet, that means a new puzzle solution has been found. """ if self.fleet.has_ships_remaining(): ship_size = self.fleet.longest_ship_size max_possible_subfleet = self.board.get_possible_ships_of_size( ship_size) if len(max_possible_subfleet) == self.fleet.size_of_subfleet( ship_size): with contextlib.suppress(InvalidShipPlacementException): self.mark_ship_group(max_possible_subfleet) else: for possible_subfleet in itertools.combinations( max_possible_subfleet, self.fleet.size_of_subfleet(ship_size)): puzzle_branch = Puzzle(Board.get_copy_of(self.board), Fleet.get_copy_of(self.fleet)) with contextlib.suppress(InvalidShipPlacementException): puzzle_branch.mark_ship_group(set(possible_subfleet)) else: self.__class__.solutions.append(self.board.repr(False))
def test_get_copy_of(self): for fleet in self.sample_fleets: with self.subTest(): fleet_orig = copy.deepcopy(fleet) fleet_copy = Fleet.get_copy_of(fleet) self.assertFalse(fleet_copy is fleet) self.assertEqual(fleet_copy, fleet) self.assertEqual(fleet_copy, fleet_orig)
def test_try_to_cover_all_ship_fields_to_be(self, mocked_decide_how_to_proceed): board_repr = ("╔═════════════════════╗\n" "║ x x . x x ║(1)\n" "║ x x x x . ║(1)\n" "║ x x x x x ║(2)\n" "║ x . x x x ║(2)\n" "║ x x x x x ║(3)\n" "╚═════════════════════╝\n" " (2) (1) (4) (1) (4) ") fleet = Fleet({4: 1, 3: 1, 2: 1}) puzzle = Puzzle(parse_board(board_repr), Fleet.get_copy_of(fleet)) positions = {Position(1, 5)} positions_orig = set(positions) puzzle.try_to_cover_all_ship_fields_to_be(positions) self.assertEqual(positions, positions_orig) mocked_decide_how_to_proceed.assert_not_called() puzzle = Puzzle(parse_board(board_repr), Fleet.get_copy_of(fleet)) positions = { Position(2, 3), Position(4, 3), Position(3, 5), Position(5, 5) } positions_orig = set(positions) puzzle.try_to_cover_all_ship_fields_to_be(positions) self.assertEqual( puzzle, Puzzle( parse_board("╔═════════════════════╗\n" "║ x . . . x ║(1)\n" "║ . . O . . ║(0)\n" "║ . . O . O ║(0)\n" "║ . . O . O ║(0)\n" "║ x . O . O ║(1)\n" "╚═════════════════════╝\n" " (2) (1) (0) (1) (1) "), Fleet({2: 1}), ), ) self.assertEqual(positions, positions_orig) mocked_decide_how_to_proceed.assert_called_once() mocked_decide_how_to_proceed.reset_mock() puzzle = Puzzle(parse_board(board_repr), Fleet.get_copy_of(fleet)) positions = {Position(5, 3), Position(5, 5)} positions_orig = set({Position(5, 3), Position(5, 5)}) puzzle.try_to_cover_all_ship_fields_to_be(positions) self.assertEqual(mocked_decide_how_to_proceed.call_count, 6) mocked_decide_how_to_proceed.assert_has_calls( [ unittest.mock.call( Puzzle( parse_board("╔═════════════════════╗\n" "║ x . . . x ║(1)\n" "║ . . O . . ║(0)\n" "║ . . O . O ║(0)\n" "║ . . O . O ║(0)\n" "║ x . O . O ║(1)\n" "╚═════════════════════╝\n" " (2) (1) (0) (1) (1) "), Fleet({2: 1}), )), unittest.mock.call( Puzzle( parse_board("╔═════════════════════╗\n" "║ x x . x x ║(1)\n" "║ x x x . . ║(1)\n" "║ x . . . O ║(1)\n" "║ . . O . O ║(0)\n" "║ x . O . O ║(1)\n" "╚═════════════════════╝\n" " (2) (1) (2) (1) (1) "), Fleet({4: 1}), )), unittest.mock.call( Puzzle( parse_board("╔═════════════════════╗\n" "║ x . . x x ║(1)\n" "║ x . x . . ║(1)\n" "║ x . x . O ║(1)\n" "║ . . . . O ║(1)\n" "║ . O O . O ║(0)\n" "╚═════════════════════╝\n" " (2) (0) (3) (1) (1) "), Fleet({4: 1}), )), unittest.mock.call( Puzzle( parse_board("╔═════════════════════╗\n" "║ x . . . x ║(1)\n" "║ . . O . . ║(0)\n" "║ x . O . . ║(1)\n" "║ . . O . O ║(0)\n" "║ x . O . O ║(1)\n" "╚═════════════════════╝\n" " (2) (1) (0) (1) (2) "), Fleet({3: 1}), )), unittest.mock.call( Puzzle( parse_board("╔═════════════════════╗\n" "║ x x . x x ║(1)\n" "║ x . . . . ║(1)\n" "║ x . O . . ║(1)\n" "║ . . O . O ║(0)\n" "║ x . O . O ║(1)\n" "╚═════════════════════╝\n" " (2) (1) (1) (1) (2) "), Fleet({4: 1}), )), unittest.mock.call( Puzzle( parse_board("╔═════════════════════╗\n" "║ x x . . x ║(1)\n" "║ x x x . . ║(1)\n" "║ x x x . x ║(2)\n" "║ x . . . . ║(2)\n" "║ . . O O O ║(0)\n" "╚═════════════════════╝\n" " (2) (1) (3) (0) (3) "), Fleet({ 4: 1, 2: 1 }), )), ], any_order=True, ) self.assertEqual(positions, positions_orig)
def test_mark_ship_group(self, mocked_decide_how_to_proceed): board_repr = ("╔═════════════════════╗\n" "║ x x x x x ║(1)\n" "║ x x x x . ║(1)\n" "║ x x x x x ║(2)\n" "║ x . x x x ║(2)\n" "║ x x x x x ║(3)\n" "╚═════════════════════╝\n" " (2) (1) (4) (1) (4) ") fleet = Fleet({4: 1, 3: 2, 1: 2}) puzzle = Puzzle(parse_board(board_repr), Fleet.get_copy_of(fleet)) ship_group = { Ship(Position(1, 3), 4, Series.COLUMN), Ship(Position(2, 3), 4, Series.COLUMN), } with self.assertRaises(InvalidShipPlacementException): puzzle.mark_ship_group(ship_group) # test when board is overmarked puzzle = Puzzle(parse_board(board_repr), Fleet.get_copy_of(fleet)) ship_group = { Ship(Position(5, 2), 1, Series.ROW), Ship(Position(5, 4), 1, Series.ROW), } ship_group_orig = set(ship_group) puzzle.mark_ship_group(ship_group) self.assertEqual( puzzle.board, parse_board("╔═════════════════════╗\n" "║ x . x . x ║(1)\n" "║ x . x . . ║(1)\n" "║ x . x . x ║(2)\n" "║ . . . . . ║(2)\n" "║ . O . O . ║(1)\n" "╚═════════════════════╝\n" " (2) (0) (4) (0) (4) "), ) self.assertEqual(ship_group, ship_group_orig) mocked_decide_how_to_proceed.assert_not_called() mocked_decide_how_to_proceed.reset_mock() puzzle = Puzzle(parse_board(board_repr), Fleet.get_copy_of(fleet)) fleet = Fleet({4: 1, 3: 2, 1: 2}) ship_group = { Ship(Position(3, 3), 3, Series.COLUMN), Ship(Position(3, 5), 3, Series.COLUMN), } ship_group_orig = set(ship_group) puzzle.mark_ship_group(ship_group) self.assertEqual( puzzle, Puzzle( parse_board("╔═════════════════════╗\n" "║ x x x x x ║(1)\n" "║ x . . . . ║(1)\n" "║ . . O . O ║(0)\n" "║ . . O . O ║(0)\n" "║ x . O . O ║(1)\n" "╚═════════════════════╝\n" " (2) (1) (1) (1) (1) "), Fleet({ 4: 1, 1: 2 }), ), ) self.assertEqual(ship_group, ship_group_orig) mocked_decide_how_to_proceed.assert_called_once()
def find_puzzles( # pylint: disable=W9015 ship_group: Set[Ship], covered_positions: Set[Position], positions_to_cover: List[Position], available_coverings: Dict[Ship, Set[Position]], puzzle: Puzzle, ) -> None: """Build a list of possible Puzzle objects created by branching a given Puzzle object and covering all given positions with a single ship from a given ship set. Depth-First Search (DFS) algorithm is used. Args: ship_group (Set[battleships.ship.Ship]): Group of previously selected ships from available_coverings. covered_positions (Set[battleships.grid.Position]): Set of positions covered by ships in ship_group. positions_to_cover (List[battleships.grid.Position]): Positions remaining to be covered by ships. available_coverings(Dict[ battleships.ship.Ship, Set[battleships.grid.Position ]]): Mapping of remaining ships and sets of positions that each ship covers. Does not contain any of the ships in ship_group. The sets of ships do not contain any of the positions in covered_positions. puzzle (battleships.puzzle.Puzzle): Current Puzzle object. """ if not positions_to_cover: puzzles.append(puzzle) return remaining_position = positions_to_cover[0] ship_candidates = [ ship for ship, positions in available_coverings.items() if remaining_position in positions ] if not ship_candidates: return for ship_candidate in ship_candidates: if puzzle.board.can_fit_ship( ship_candidate ) and not puzzle.ship_group_exceeds_fleet([ship_candidate]): ship_candidate_positions = available_coverings[ ship_candidate] new_positions_to_cover = [ position for position in positions_to_cover if position not in ship_candidate_positions ] new_coverings = { ship: {*ship_positions} for ship, ship_positions in available_coverings.items() if ship != ship_candidate } new_puzzle = Puzzle(Board.get_copy_of(puzzle.board), Fleet.get_copy_of(puzzle.fleet)) new_puzzle.board.mark_ship_and_surrounding_sea( ship_candidate) new_puzzle.board.mark_sea_in_series_with_no_rem_ship_fields( ) new_puzzle.fleet.remove_ship_of_size(ship_candidate.size) find_puzzles( ship_group.union({ship_candidate}), covered_positions.union(ship_candidate_positions), new_positions_to_cover, new_coverings, new_puzzle, )