2024-04-26 13:25:54 +00:00
|
|
|
|
"""
|
|
|
|
|
https://stepik.org/lesson/727588/step/1?unit=728924
|
|
|
|
|
|
|
|
|
|
Посвящение в ООП
|
|
|
|
|
|
|
|
|
|
Вы прошли серию испытаний и совершили множество подвигов, чтобы лицом к лицу столкнуться с настоящим вызовом, достойным лишь избранных!
|
|
|
|
|
Для подтверждения своих знаний и навыков вам предлагается пройти этап посвящения в объектно-ориентированное программирование.
|
|
|
|
|
И вот задание, которое выпало на вашу долю.
|
|
|
|
|
|
|
|
|
|
Руководство компании целыми днями не знает куда себя деть. Поэтому они решили дать задание своим программистам написать программу игры "Морской бой".
|
|
|
|
|
Но эта игра будет немного отличаться от классической. Для тех, кто не знаком с этой древней, как мир, игрой, напомню ее краткое описание.
|
|
|
|
|
|
|
|
|
|
Каждый игрок у себя на бумаге рисует игровое поле 10 х 10 клеток и расставляет на нем десять кораблей: однопалубных - 4; двухпалубных - 3; трехпалубных - 2; четырехпалубный - 1.
|
|
|
|
|
Корабли расставляются случайным образом, но так, чтобы не выходили за пределы игрового поля и не соприкасались друг с другом (в том числе и по диагонали).
|
|
|
|
|
|
|
|
|
|
Затем, игроки по очереди называют клетки, куда производят выстрелы.
|
|
|
|
|
И отмечают эти выстрелы на другом таком же поле в 10 х 10 клеток, которое представляет поле соперника.
|
|
|
|
|
Соперник при этом должен честно отвечать: "промах", если ни один корабль не был задет и "попал", если произошло попадание.
|
|
|
|
|
Выигрывает тот игрок, который первым поразит все корабли соперника.
|
|
|
|
|
|
|
|
|
|
Но это была игра из глубокого прошлого.
|
|
|
|
|
Теперь же, в компьютерную эру, корабли на игровом поле могут перемещаться в направлении своей ориентации на одну клетку после каждого хода соперника, если в них не было ни одного попадания.
|
|
|
|
|
|
|
|
|
|
Итак, лично вам поручается сделать важный фрагмент этой игры - расстановку и управление кораблями в этой игре. А само задание звучит так.
|
|
|
|
|
|
|
|
|
|
Техническое задание
|
|
|
|
|
|
|
|
|
|
В программе необходимо объявить два класса:
|
|
|
|
|
Ship - для представления кораблей;
|
|
|
|
|
GamePole - для описания игрового поля.
|
|
|
|
|
|
|
|
|
|
Класс Ship
|
|
|
|
|
Класс Ship должен описывать корабли набором следующих параметров:
|
|
|
|
|
x, y - координаты начала расположения корабля (целые числа);
|
|
|
|
|
length - длина корабля (число палуб: целое значение: 1, 2, 3 или 4);
|
|
|
|
|
tp - ориентация корабля (1 - горизонтальная; 2 - вертикальная).
|
|
|
|
|
|
|
|
|
|
Объекты класса Ship должны создаваться командами:
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> ship = Ship(length := 1)
|
|
|
|
|
>>> ship = Ship(length := 1, tp := 2)
|
|
|
|
|
>>> ship = Ship(length := 1, tp := 2, x := 1, y := 1)
|
2024-04-26 13:25:54 +00:00
|
|
|
|
|
|
|
|
|
По умолчанию (если не указывается) параметр tp = 1, а координаты x, y равны None.
|
|
|
|
|
|
|
|
|
|
В каждом объекте класса Ship должны формироваться следующие локальные атрибуты:
|
|
|
|
|
_x, _y - координаты корабля (целые значения в диапазоне [0; size), где size - размер игрового поля);
|
|
|
|
|
_length - длина корабля (число палуб);
|
|
|
|
|
_tp - ориентация корабля;
|
|
|
|
|
_is_move - возможно ли перемещение корабля (изначально равно True);
|
|
|
|
|
_cells - изначально список длиной length, состоящий из единиц (например, при length=3, _cells = [1, 1, 1]).
|
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
# доп проверки кораблей ---
|
|
|
|
|
>>> [*map(len, (Ship(1),Ship(2),Ship(3),Ship(4)))] == [*range(1, 5)]
|
|
|
|
|
True
|
|
|
|
|
>>> Ship(5)
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
2024-04-29 15:48:39 +00:00
|
|
|
|
ValueError: 5 is not a valid ShipSize
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> Ship(1, 1)._tp, Ship(1, 2)._tp
|
|
|
|
|
(1, 2)
|
|
|
|
|
>>> Ship(1, 3)._tp
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
2024-04-29 15:48:39 +00:00
|
|
|
|
ValueError: 3 is not a valid ShipOrientation
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> s = Ship(1)
|
|
|
|
|
>>> {s[0] == 1, len(s) == 1}
|
|
|
|
|
{True}
|
|
|
|
|
>>> s[0] = 2
|
|
|
|
|
>>> s[0]
|
|
|
|
|
2
|
|
|
|
|
>>> s[0] = 4
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
2024-04-29 15:48:39 +00:00
|
|
|
|
ValueError: 4 is not a valid DeckStatus
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> s2 = Ship(2)
|
|
|
|
|
>>> s2._cells = [1, 2]
|
|
|
|
|
>>> s2._cells
|
|
|
|
|
[1, 2]
|
|
|
|
|
>>> s._cells = [1, 2]
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
|
|
|
|
ValueError: _cells_ must be 1 elements long
|
|
|
|
|
>>> s[0] = 1
|
|
|
|
|
>>> s._is_move
|
|
|
|
|
True
|
|
|
|
|
>>> s[0] = 2
|
|
|
|
|
>>> s._is_move
|
|
|
|
|
False
|
2024-04-27 10:54:33 +00:00
|
|
|
|
>>> s = Ship(1)
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> s.get_start_coords()
|
2024-04-27 10:54:33 +00:00
|
|
|
|
(0, 0)
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> s.set_start_coords(2, 3)
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(2, 3)
|
2024-04-27 10:54:33 +00:00
|
|
|
|
>>> s.move(1)
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(3, 3)
|
|
|
|
|
>>> s.move(-1)
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(2, 3)
|
|
|
|
|
>>> s[0] = 2
|
|
|
|
|
>>> s.move(2)
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(2, 3)
|
|
|
|
|
>>> Ship(1).is_collide(Ship(2, 1, 2, 3))
|
|
|
|
|
False
|
|
|
|
|
>>> Ship(2, 1, 2, 3).is_collide(Ship(3, 2, 3, 2))
|
|
|
|
|
True
|
|
|
|
|
>>> Ship(2, 1, 0, 0).is_collide(Ship(1, 2, 2, 2))
|
2024-04-29 15:29:53 +00:00
|
|
|
|
False
|
2024-04-27 10:54:33 +00:00
|
|
|
|
>>> Ship(1).is_out_pole(10)
|
|
|
|
|
False
|
|
|
|
|
>>> Ship(3, 1, 8, 1).is_out_pole(10)
|
|
|
|
|
True
|
|
|
|
|
>>> Ship(3, 2, 1, 8).is_out_pole(10)
|
|
|
|
|
True
|
2024-04-28 13:39:17 +00:00
|
|
|
|
>>> s = Ship(4, 2)
|
2024-04-29 15:29:53 +00:00
|
|
|
|
>>> s.try_move(1, 6, [Ship(1, 1, 1, 6)])
|
2024-04-28 13:39:17 +00:00
|
|
|
|
True
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(0, 1)
|
2024-04-29 15:29:53 +00:00
|
|
|
|
>>> s.try_move(1, 6, [Ship(1, 1, 1, 6)])
|
2024-04-28 13:39:17 +00:00
|
|
|
|
False
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(0, 1)
|
2024-04-29 15:29:53 +00:00
|
|
|
|
>>> s.try_move(1, 6, [Ship(1, 1, 1, 2)])
|
2024-04-28 13:39:17 +00:00
|
|
|
|
False
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(0, 1)
|
|
|
|
|
>>> s[0] = 2
|
|
|
|
|
>>> s.try_move(1, 10, [Ship(1, 1, 3, 1)])
|
|
|
|
|
False
|
|
|
|
|
>>> s.get_start_coords()
|
|
|
|
|
(0, 1)
|
|
|
|
|
>>> s[0] = 1; s.computer_move(10, [Ship(1, 1, 3, 1)])
|
|
|
|
|
>>> _, y = s.get_start_coords(); 0 <= y <= 2
|
|
|
|
|
True
|
|
|
|
|
|
|
|
|
|
>>> s = Ship(4, 2)
|
|
|
|
|
>>> s.hit(1, 1)
|
|
|
|
|
False
|
|
|
|
|
>>> s._is_move
|
|
|
|
|
True
|
|
|
|
|
>>> s.hit(0, 4)
|
|
|
|
|
False
|
|
|
|
|
>>> s._is_move
|
|
|
|
|
True
|
|
|
|
|
>>> s.hit(0, 3)
|
|
|
|
|
True
|
|
|
|
|
>>> s._is_move
|
|
|
|
|
False
|
|
|
|
|
>>> s.is_alive
|
|
|
|
|
True
|
|
|
|
|
>>> {s.hit(0, x) for x in range(3)} ; s.is_alive
|
|
|
|
|
{True}
|
|
|
|
|
False
|
|
|
|
|
|
|
|
|
|
>>> sz = 5; pole = [[0 for _ in range(sz)] for _ in range(sz)]
|
|
|
|
|
>>> s1, s2 = Ship(2, 1), Ship(2, 2, 3, 1)
|
|
|
|
|
>>> s1.place_to_pole(pole)
|
|
|
|
|
True
|
|
|
|
|
>>> pole
|
|
|
|
|
[[1, 1, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
|
|
|
|
|
>>> s2.place_to_pole(pole)
|
|
|
|
|
True
|
|
|
|
|
>>> pole
|
|
|
|
|
[[1, 1, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
|
|
|
|
|
# ---
|
|
|
|
|
|
|
|
|
|
|
2024-04-26 13:25:54 +00:00
|
|
|
|
Список _cells будет сигнализировать о попадании соперником в какую-либо палубу корабля. Если стоит 1, то попадания не было, а если стоит значение 2, то произошло попадание в соответствующую палубу.
|
|
|
|
|
|
|
|
|
|
При попадании в корабль (хотя бы одну его палубу), флаг _is_move устанавливается в False и перемещение корабля по игровому полю прекращается.
|
|
|
|
|
|
|
|
|
|
В самом классе Ship должны быть реализованы следующие методы (конечно, возможны и другие, дополнительные):
|
|
|
|
|
set_start_coords(x, y) - установка начальных координат (запись значений в локальные атрибуты _x, _y);
|
|
|
|
|
get_start_coords() - получение начальных координат корабля в виде кортежа x, y;
|
|
|
|
|
move(go) - перемещение корабля в направлении его ориентации на go клеток (go = 1 - движение в одну сторону на клетку; go = -1 - движение в другую сторону на одну клетку); движение возможно только если флаг _is_move = True;
|
|
|
|
|
is_collide(ship) - проверка на столкновение с другим кораблем ship (столкновением считается, если другой корабль или пересекается с текущим или просто соприкасается, в том числе и по диагонали); метод возвращает True, если столкновение есть и False - в противном случае;
|
|
|
|
|
is_out_pole(size) - проверка на выход корабля за пределы игрового поля (size - размер игрового поля, обычно, size = 10); возвращается булево значение True, если корабль вышел из игрового поля и False - в противном случае;
|
|
|
|
|
|
|
|
|
|
С помощью магических методов __getitem__() и __setitem__() обеспечить доступ к коллекции _cells следующим образом:
|
|
|
|
|
value = ship[indx] # считывание значения из _cells по индексу indx (индекс отсчитывается от 0)
|
|
|
|
|
ship[indx] = value # запись нового значения в коллекцию _cells
|
|
|
|
|
Класс GamePole
|
|
|
|
|
Следующий класс GamePole должен обеспечивать работу с игровым полем. Объекты этого класса создаются командой:
|
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
>>> pole = GamePole(10)
|
|
|
|
|
>>> pole = GamePole()
|
|
|
|
|
|
2024-04-26 13:25:54 +00:00
|
|
|
|
где size - размеры игрового поля (обычно, size = 10).
|
|
|
|
|
|
|
|
|
|
В каждом объекте этого класса должны формироваться локальные атрибуты:
|
|
|
|
|
_size - размер игрового поля (целое положительное число);
|
|
|
|
|
_ships - список из кораблей (объектов класса Ship); изначально пустой список.
|
|
|
|
|
|
|
|
|
|
В самом классе GamePole должны быть реализованы следующие методы (возможны и другие, дополнительные методы):
|
|
|
|
|
init() - начальная инициализация игрового поля; здесь создается список из кораблей (объектов класса Ship): однопалубных - 4; двухпалубных - 3; трехпалубных - 2; четырехпалубный - 1 (ориентация этих кораблей должна быть случайной).
|
|
|
|
|
|
|
|
|
|
Корабли формируются в коллекции _ships следующим образом: однопалубных - 4; двухпалубных - 3; трехпалубных - 2; четырехпалубный - 1. Ориентация этих кораблей должна быть случайной. Для этого можно воспользоваться функцией randint следующим образом:
|
|
|
|
|
[Ship(4, tp=randint(1, 2)), Ship(3, tp=randint(1, 2)), Ship(3, tp=randint(1, 2)), ...]
|
|
|
|
|
Начальные координаты x, y не расставленных кораблей равны None.
|
|
|
|
|
|
|
|
|
|
После этого, выполняется их расстановка на игровом поле со случайными координатами так, чтобы корабли не пересекались между собой.
|
|
|
|
|
|
|
|
|
|
get_ships() - возвращает коллекцию _ships;
|
|
|
|
|
move_ships() - перемещает каждый корабль из коллекции _ships на одну клетку (случайным образом вперед или назад) в направлении ориентации корабля; если перемещение в выбранную сторону невозможно (другой корабль или пределы игрового поля), то попытаться переместиться в противоположную сторону, иначе (если перемещения невозможны), оставаться на месте;
|
|
|
|
|
show() - отображение игрового поля в консоли (корабли должны отображаться значениями из коллекции _cells каждого корабля, вода - значением 0);
|
|
|
|
|
|
|
|
|
|
get_pole() - получение текущего игрового поля в виде двумерного (вложенного) кортежа размерами size x size элементов.
|
|
|
|
|
|
2024-04-29 15:29:53 +00:00
|
|
|
|
>>> pole = GamePole(5)
|
|
|
|
|
>>> pole.get_pole()
|
|
|
|
|
((0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0))
|
|
|
|
|
>>> pole.show()
|
|
|
|
|
0 0 0 0 0
|
|
|
|
|
0 0 0 0 0
|
|
|
|
|
0 0 0 0 0
|
|
|
|
|
0 0 0 0 0
|
|
|
|
|
0 0 0 0 0
|
|
|
|
|
>>> pole = GamePole(10)
|
|
|
|
|
>>> pole.init()
|
|
|
|
|
>>> sum(map(sum, pole.get_pole()))
|
|
|
|
|
20
|
|
|
|
|
|
|
|
|
|
>>> [*map(lambda x: x.__class__.__name__, pole.get_ships())]
|
|
|
|
|
['Ship', 'Ship', 'Ship', 'Ship', 'Ship', 'Ship', 'Ship', 'Ship', 'Ship', 'Ship']
|
|
|
|
|
>>> pole.move_ships()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-04-26 13:25:54 +00:00
|
|
|
|
Пример отображения игрового поля:
|
|
|
|
|
0 0 1 0 1 1 1 0 0 0
|
|
|
|
|
1 0 0 0 0 0 0 0 0 0
|
|
|
|
|
0 0 0 0 0 0 0 0 0 0
|
|
|
|
|
0 0 0 0 0 0 1 0 0 1
|
|
|
|
|
0 0 0 0 1 0 1 0 0 1
|
|
|
|
|
0 0 0 0 0 0 1 0 0 0
|
|
|
|
|
1 1 0 0 0 0 0 0 0 0
|
|
|
|
|
0 0 0 0 0 0 1 0 0 0
|
|
|
|
|
0 1 1 1 1 0 0 0 0 0
|
|
|
|
|
0 0 0 0 0 0 0 1 1 0
|
|
|
|
|
|
|
|
|
|
Пример использования классов (эти строчки в программе не писать):
|
|
|
|
|
|
|
|
|
|
SIZE_GAME_POLE = 10
|
|
|
|
|
|
|
|
|
|
pole = GamePole(SIZE_GAME_POLE)
|
|
|
|
|
pole.init()
|
|
|
|
|
pole.show()
|
|
|
|
|
|
|
|
|
|
pole.move_ships()
|
|
|
|
|
print()
|
|
|
|
|
pole.show()
|
|
|
|
|
|
|
|
|
|
В программе требуется только объявить классы Ship и GamePole с соответствующим функционалом. На экран выводить ничего не нужно.
|
|
|
|
|
|
|
|
|
|
P.S. Для самых преданных поклонников программирования и ООП. Завершите эту программу, добавив еще один класс SeaBattle для управления игровым процессом в целом.
|
|
|
|
|
Игра должна осуществляться между человеком и компьютером. Выстрелы со стороны компьютера можно реализовать случайным образом в свободные клетки.
|
|
|
|
|
Сыграйте в эту игру и выиграйте у компьютера.
|
|
|
|
|
"""
|
|
|
|
|
|
2024-04-29 15:29:53 +00:00
|
|
|
|
from typing import List, Optional, Tuple
|
2024-04-27 09:27:40 +00:00
|
|
|
|
from enum import Enum
|
|
|
|
|
from functools import total_ordering
|
2024-04-27 10:54:33 +00:00
|
|
|
|
from collections import namedtuple
|
2024-04-28 13:39:17 +00:00
|
|
|
|
import random
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
|
|
|
|
|
@total_ordering
|
|
|
|
|
class EnumOrdering(Enum):
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
|
return self.value == other.value
|
|
|
|
|
return self.value == other
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def __lt__(self, other):
|
|
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
|
return self.value < other.value
|
|
|
|
|
return self.value < other
|
|
|
|
|
|
2024-04-29 15:29:53 +00:00
|
|
|
|
@classmethod
|
|
|
|
|
def values(cls):
|
|
|
|
|
return tuple(item.value for item in cls)
|
|
|
|
|
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
class IntEnumField:
|
|
|
|
|
def __init__(self, enum_cls: Enum):
|
|
|
|
|
self.enum_cls = enum_cls
|
|
|
|
|
|
|
|
|
|
def __set_name__(self, owner, name):
|
|
|
|
|
self.name_orig = name
|
|
|
|
|
self.name = name + "_"
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.name_orig
|
|
|
|
|
|
|
|
|
|
def __get__(self, instance, owner):
|
|
|
|
|
if instance is None:
|
|
|
|
|
return self
|
|
|
|
|
return getattr(instance, self.name).value
|
|
|
|
|
|
|
|
|
|
def __set__(self, instance, value):
|
|
|
|
|
setattr(instance, self.name, self.enum_cls(value))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ListEnumField:
|
|
|
|
|
def __init__(self, enum_cls: Enum, length_key):
|
|
|
|
|
self.enum_cls = enum_cls
|
|
|
|
|
self.length_key = length_key
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def __set_name__(self, owner, name):
|
|
|
|
|
self.name = name + "_"
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def __get__(self, instance, owner):
|
|
|
|
|
if instance is None:
|
|
|
|
|
return self
|
|
|
|
|
return [x.value for x in getattr(instance, self.name)]
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def get_item(self, instance, index):
|
|
|
|
|
return getattr(instance, self.name)[index]
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def set_item(self, instance, index, value):
|
|
|
|
|
getattr(instance, self.name)[index] = self.enum_cls(value)
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def __set__(self, instance, value):
|
2024-04-27 10:54:33 +00:00
|
|
|
|
length = getattr(instance, str(self.length_key))
|
2024-04-27 09:27:40 +00:00
|
|
|
|
new_value = list(map(self.enum_cls, value))
|
|
|
|
|
if len(new_value) != length:
|
|
|
|
|
raise ValueError(f"{self.name} must be {length} elements long")
|
|
|
|
|
setattr(instance, self.name, new_value)
|
|
|
|
|
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
|
|
|
|
class NonNegativeIntField:
|
|
|
|
|
def __set_name__(self, owner, name):
|
|
|
|
|
self.name = name + "_"
|
|
|
|
|
|
|
|
|
|
def __get__(self, instance, owner):
|
|
|
|
|
if instance is None:
|
|
|
|
|
return self
|
|
|
|
|
return getattr(instance, self.name)
|
|
|
|
|
|
|
|
|
|
def __set__(self, instance, value):
|
|
|
|
|
if not isinstance(value, int):
|
|
|
|
|
raise TypeError(f"{self.name} must be an integer")
|
|
|
|
|
if value < 0:
|
|
|
|
|
raise ValueError(f"{self.name} must be non-negative")
|
|
|
|
|
setattr(instance, self.name, value)
|
|
|
|
|
|
|
|
|
|
|
2024-04-29 15:48:39 +00:00
|
|
|
|
class ShipSize(EnumOrdering):
|
|
|
|
|
ONE_DECK = 1
|
|
|
|
|
TWO_DECKS = 2
|
|
|
|
|
THREE_DECKS = 3
|
|
|
|
|
FOUR_DECKS = 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ShipOrientation(EnumOrdering):
|
|
|
|
|
HORIZONTAL = 1
|
|
|
|
|
VERTICAL = 2
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
|
|
|
|
|
2024-04-29 15:48:39 +00:00
|
|
|
|
class DeckStatus(EnumOrdering):
|
|
|
|
|
OK = 1
|
|
|
|
|
DAMAGED = 2
|
2024-04-27 09:27:40 +00:00
|
|
|
|
|
2024-04-29 15:48:39 +00:00
|
|
|
|
|
|
|
|
|
class Ship:
|
2024-04-27 10:54:33 +00:00
|
|
|
|
_length: int = IntEnumField(ShipSize)
|
|
|
|
|
_tp: int = IntEnumField(ShipOrientation)
|
|
|
|
|
_cells: List[int] = ListEnumField(DeckStatus, _length)
|
|
|
|
|
_x: int = NonNegativeIntField()
|
|
|
|
|
_y: int = NonNegativeIntField()
|
|
|
|
|
|
|
|
|
|
Rect = namedtuple("Rect", ["left", "top", "right", "bottom"])
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
length: int,
|
|
|
|
|
tp: int = 1,
|
|
|
|
|
x: int = 0,
|
|
|
|
|
y: int = 0,
|
2024-04-29 15:29:53 +00:00
|
|
|
|
cells: Optional[List[DeckStatus]] = None,
|
2024-04-27 10:54:33 +00:00
|
|
|
|
):
|
2024-04-27 09:27:40 +00:00
|
|
|
|
self._length, self._tp, self._x, self._y = length, tp, x, y
|
2024-04-29 15:48:39 +00:00
|
|
|
|
self._cells = cells or [DeckStatus.OK] * self._length
|
2024-04-27 09:27:40 +00:00
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return f"{self.__class__.__name__}{(self._length, self._tp, self._x, self._y, self._cells)!r}"
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def __len__(self):
|
|
|
|
|
return self._length
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def __getitem__(self, key):
|
|
|
|
|
return self.__class__._cells.get_item(self, key).value
|
|
|
|
|
|
|
|
|
|
def __setitem__(self, key, value):
|
|
|
|
|
return self.__class__._cells.set_item(self, key, value)
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
@property
|
|
|
|
|
def _is_move(self) -> bool:
|
2024-04-29 15:48:39 +00:00
|
|
|
|
return all(cell == DeckStatus.OK for cell in self)
|
2024-04-28 13:39:17 +00:00
|
|
|
|
|
2024-04-27 10:54:33 +00:00
|
|
|
|
@property
|
|
|
|
|
def is_alive(self) -> bool:
|
2024-04-29 15:48:39 +00:00
|
|
|
|
return any(cell == DeckStatus.OK for cell in self)
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
def set_start_coords(self, x: int, y: int):
|
|
|
|
|
self._x, self._y = x, y
|
|
|
|
|
|
|
|
|
|
def get_start_coords(self):
|
|
|
|
|
return self._x, self._y
|
2024-04-27 10:54:33 +00:00
|
|
|
|
|
2024-04-28 13:39:17 +00:00
|
|
|
|
@property
|
|
|
|
|
def is_horizontal(self):
|
2024-04-29 15:48:39 +00:00
|
|
|
|
return self._tp == ShipOrientation.HORIZONTAL
|
2024-04-28 13:39:17 +00:00
|
|
|
|
|
2024-04-27 10:54:33 +00:00
|
|
|
|
def move(self, go: int):
|
|
|
|
|
if self._is_move:
|
2024-04-28 13:39:17 +00:00
|
|
|
|
if self.is_horizontal:
|
2024-04-27 10:54:33 +00:00
|
|
|
|
self._x += go
|
|
|
|
|
else:
|
|
|
|
|
self._y += go
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def rect(self):
|
|
|
|
|
x, y = self.get_start_coords()
|
2024-04-28 13:39:17 +00:00
|
|
|
|
if self.is_horizontal:
|
2024-04-27 10:54:33 +00:00
|
|
|
|
return self.Rect(x, y, x + self._length, y + 1)
|
|
|
|
|
else:
|
|
|
|
|
return self.Rect(x, y, x + 1, y + self._length)
|
|
|
|
|
|
|
|
|
|
def is_collide(self, other: "Ship") -> bool:
|
|
|
|
|
return (
|
|
|
|
|
self.rect.left <= other.rect.right
|
|
|
|
|
and self.rect.right >= other.rect.left
|
|
|
|
|
and self.rect.top <= other.rect.bottom
|
|
|
|
|
and self.rect.bottom >= other.rect.top
|
|
|
|
|
or any(
|
2024-04-29 15:29:53 +00:00
|
|
|
|
all(
|
|
|
|
|
not (getattr(a.rect, x) - getattr(b.rect, y))
|
|
|
|
|
for x, y in (("left", "right"), ("top", "bottom"))
|
|
|
|
|
)
|
2024-04-27 10:54:33 +00:00
|
|
|
|
for a, b in ((self, other), (other, self))
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def is_out_pole(self, size: int) -> bool:
|
|
|
|
|
return (
|
|
|
|
|
self.rect.left < 0
|
|
|
|
|
or self.rect.top < 0
|
|
|
|
|
or self.rect.right > size
|
|
|
|
|
or self.rect.bottom > size
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-28 13:39:17 +00:00
|
|
|
|
def try_move(self, go: int, pole_size: int, other_ships: List["Ship"]) -> bool:
|
|
|
|
|
if not self._is_move:
|
|
|
|
|
return False
|
2024-04-29 15:29:53 +00:00
|
|
|
|
|
2024-04-28 13:39:17 +00:00
|
|
|
|
backup = self.get_start_coords()
|
2024-04-29 15:29:53 +00:00
|
|
|
|
try:
|
|
|
|
|
self.move(go)
|
|
|
|
|
except ValueError:
|
|
|
|
|
self.set_start_coords(*backup)
|
|
|
|
|
return False
|
|
|
|
|
|
2024-04-28 13:39:17 +00:00
|
|
|
|
if not (
|
|
|
|
|
self.is_out_pole(pole_size)
|
|
|
|
|
or any(self.is_collide(ship) for ship in other_ships)
|
|
|
|
|
):
|
|
|
|
|
return True
|
|
|
|
|
self.set_start_coords(*backup)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def computer_move(self, pole_size: int, other_ships: List["Ship"]):
|
2024-04-29 15:29:53 +00:00
|
|
|
|
go = random.randint(-1, 1)
|
|
|
|
|
if not go:
|
|
|
|
|
return # решили не двигать корабль
|
|
|
|
|
moved = self.try_move(go, pole_size, other_ships)
|
|
|
|
|
if not moved:
|
|
|
|
|
go = -go
|
|
|
|
|
self.try_move(go, pole_size, other_ships)
|
2024-04-28 13:39:17 +00:00
|
|
|
|
|
|
|
|
|
def hit(self, x: int, y: int) -> bool:
|
|
|
|
|
left, top, right, bottom = self.rect
|
|
|
|
|
if left <= x < right and top <= y < bottom:
|
|
|
|
|
a, b = ((y, top), (x, left))[self.is_horizontal]
|
2024-04-29 15:48:39 +00:00
|
|
|
|
self[a - b] = DeckStatus.DAMAGED
|
2024-04-28 13:39:17 +00:00
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def place_to_pole(self, pole: List[List[int]]) -> bool:
|
|
|
|
|
if self.is_out_pole(len(pole)):
|
|
|
|
|
return False
|
|
|
|
|
rect = self.rect
|
|
|
|
|
for i in range(rect.top, rect.bottom):
|
|
|
|
|
for j in range(rect.left, rect.right):
|
|
|
|
|
a, b = ((i, rect.top), (j, rect.left))[self.is_horizontal]
|
|
|
|
|
pole[i][j] = self[a - b]
|
|
|
|
|
return True
|
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
|
2024-04-29 15:29:53 +00:00
|
|
|
|
class GamePoleError(Exception):
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ShipPlacementError(GamePoleError):
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
2024-04-27 09:27:40 +00:00
|
|
|
|
class GamePole:
|
2024-04-29 15:29:53 +00:00
|
|
|
|
def __init__(self, size: int = 10, ships: Optional[List[Ship]] = None):
|
|
|
|
|
self._size = size
|
|
|
|
|
self._ships = ships or []
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return f"{self.__class__.__name__}{(self._size, self._ships)!r}"
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
|
return len(self._ships)
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
|
|
return self._ships[key]
|
|
|
|
|
|
|
|
|
|
def get_pole_list(self) -> List[List[int]]:
|
|
|
|
|
pole = [[0] * self._size for _ in range(self._size)]
|
|
|
|
|
for ship in self._ships:
|
|
|
|
|
ship.place_to_pole(pole)
|
|
|
|
|
return pole
|
|
|
|
|
|
|
|
|
|
def get_pole(self) -> Tuple[Tuple[int, ...], ...]:
|
|
|
|
|
return tuple(tuple(row) for row in self.get_pole_list())
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return "\n".join(" ".join(map(str, row)) for row in self.get_pole_list())
|
|
|
|
|
|
|
|
|
|
def show(self):
|
|
|
|
|
print(self)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _check_free_box(pole_s, i, j) -> bool:
|
|
|
|
|
"""проверка, что в пределах 9 клеток вокруг координат (включая их самих) нет значений кроме 0"""
|
|
|
|
|
offsets = [(a, b) for a in range(-1, 2) for b in range(-1, 2)]
|
|
|
|
|
n, m = len(pole_s), len(pole_s[0])
|
|
|
|
|
for a, b in map(lambda a, b: (a + i, b + j), *zip(*offsets)):
|
|
|
|
|
if 0 <= a < n and 0 <= b < m:
|
|
|
|
|
if pole_s[a][b] != 0:
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _find_free_places_for_ship(
|
|
|
|
|
pole, ship_size: int, tp: int
|
|
|
|
|
) -> List[Tuple[int, int]]:
|
|
|
|
|
"""возвращает список возможных координат для нового корабля
|
|
|
|
|
координаты в виде кортежей в формате (x, y) | (индекс столбца, индекс строки)
|
|
|
|
|
"""
|
|
|
|
|
free_places = []
|
2024-04-29 15:48:39 +00:00
|
|
|
|
is_horizontal = tp == ShipOrientation.HORIZONTAL
|
2024-04-29 15:29:53 +00:00
|
|
|
|
pole_s = pole
|
|
|
|
|
if not is_horizontal:
|
|
|
|
|
pole_s = list(zip(*pole))
|
|
|
|
|
for i, row in enumerate(pole_s):
|
|
|
|
|
for j in range(len(row) - ship_size):
|
|
|
|
|
if all(
|
|
|
|
|
GamePole._check_free_box(pole_s, i, j + k) for k in range(ship_size)
|
|
|
|
|
):
|
|
|
|
|
pos = (j, i) if is_horizontal else (i, j)
|
|
|
|
|
free_places.append(pos)
|
|
|
|
|
return free_places
|
|
|
|
|
|
|
|
|
|
def _validete_new_ship(self, ship):
|
|
|
|
|
if ship.is_out_pole(self._size):
|
|
|
|
|
raise ShipPlacementError(f"Новый корабль выходит за пределы поля: {ship!r}")
|
|
|
|
|
for other in self._ships:
|
|
|
|
|
if ship.is_collide(other):
|
|
|
|
|
raise ShipPlacementError(
|
|
|
|
|
f"Новый корабль {ship!r} пересекается с {other!r}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def place_new_ship(self, pole, ship_size: int, tp: int):
|
|
|
|
|
free_places = GamePole._find_free_places_for_ship(pole, ship_size, tp)
|
|
|
|
|
if not free_places:
|
|
|
|
|
# должно быть так
|
|
|
|
|
# raise ShipPlacementError("Нет свободных мест для размещения корабля")
|
|
|
|
|
# но в тестах есть размер поля 8x8, поэтому просто игнорим
|
|
|
|
|
return
|
|
|
|
|
pos = random.choice(free_places)
|
|
|
|
|
ship = Ship(ship_size, tp, *pos)
|
|
|
|
|
self._validete_new_ship(ship)
|
|
|
|
|
self._ships.append(ship)
|
|
|
|
|
ship.place_to_pole(pole)
|
|
|
|
|
|
|
|
|
|
def init(self):
|
|
|
|
|
self._ships.clear()
|
|
|
|
|
pole = self.get_pole_list()
|
2024-04-29 15:48:39 +00:00
|
|
|
|
sizes = ShipSize.values()[::-1]
|
2024-04-29 15:29:53 +00:00
|
|
|
|
for ship_size, ship_count in zip(sizes, range(1, len(sizes) + 1)):
|
|
|
|
|
for _ in range(ship_count):
|
2024-04-29 15:48:39 +00:00
|
|
|
|
tp = random.choice(ShipOrientation.values())
|
2024-04-29 15:29:53 +00:00
|
|
|
|
self.place_new_ship(pole, ship_size, tp)
|
|
|
|
|
|
|
|
|
|
def get_ships(self) -> List[Ship]:
|
|
|
|
|
return self._ships
|
|
|
|
|
|
|
|
|
|
def move_ships(self):
|
|
|
|
|
for ship in self._ships:
|
|
|
|
|
ship.computer_move(self._size, self._ships)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pole = GamePole(10)
|
|
|
|
|
|
|
|
|
|
pole.init()
|
|
|
|
|
print(sum(map(sum, pole.get_pole())))
|
|
|
|
|
print(len(pole))
|
|
|
|
|
pole.show()
|
2024-04-27 09:27:40 +00:00
|
|
|
|
|
|
|
|
|
|
2024-04-26 13:25:54 +00:00
|
|
|
|
def tests():
|
|
|
|
|
code = (
|
|
|
|
|
b"b7*OBAUz;cXlZaLGARmkXlZaDJs?wPX>ceqEFdu{3Ug>_a3DP(Q)p>$C^IY|GAtl4EFdr`3JPI"
|
|
|
|
|
+ b"!b7gXLAaiJGa4uhLWo~D5Xdpd3ATuCgZe$>HXlZaRUvzLFJv|^YAYpD~AaiJGa4uhXAU!=GFd$)W"
|
|
|
|
|
+ b"WFT{BX>cxIc_2MKATTT-BGA3iwa~KAwb6jkz0r%%wII;9(7n*G(TC8r(7n*O(T^a|u+f6ifY7+mv"
|
|
|
|
|
+ b"C)Ikg3!LuvLMjD(6P~q(6!LI(Sp#hAkezdyU?)Ffzg4`upm=tX>cM6VRLh3a&#bbXlZaRUt?u#Y;"
|
|
|
|
|
+ b"zzzJs?{#EFdu~ATeDmAR^Gc(6!LA(6!Nk(7n-%(77PdfzZFuxY2>ozR<cLUt?u#Y;z(CVRLh3a&#"
|
|
|
|
|
+ b"bbXlZaRUukn+ZEtpEEFdD#z0kGLve32BfY80rzR<NG(6`XN(6G^m(6!LL(74dGAkeVUg3*A`xX`i"
|
|
|
|
|
+ b"DgVBP}upnP)b6;(5c4Z<83Ug>_a4vIYbYF9HVRCd|V{dPAWOFDnEFdx|3So0|WpZ>Nb7*OBE?;;c"
|
|
|
|
|
+ b"Jv|^XAYpD~AaiJGa4uhYAU!=GGAtk>(7n*L(6Z3A(SXps(7qthzR`lwfY7kevCzKJg3z$gyCBfK("
|
|
|
|
|
+ b"6!Nm(7w>LAaiAOUvqR}a&%u~Z*OvBb0{ey3So0|WpZ>Nb7*OBE@x$QUvqR}a&%u~Z*OvBb0{ewJv"
|
|
|
|
|
+ b"|^OF)Sc5DJ&o&(7n*L(6Z3A(SXps(7qthzR`lwfY7kevCzKJg3z$gyCBfK(6!Nm(7w>LAZKNCUvq"
|
|
|
|
|
+ b"R}a&%u~Z*OvBb0{ey3JP;*X>cxWZ+2xUF)0djF(5r4Q)p>$C^Re}F)Sc3EFdr`3Ue|bJs?wPX>ce"
|
|
|
|
|
+ b"rEFdy0ATTT-FewUiGax-6Q)p>$C^IY|GAtl4EFdx|3JPI!b7gXLAagM;X>(s=Z)|L7WMwFGGAS$|"
|
|
|
|
|
+ b"BGA3iwa~KAwb6jkz0kfO(SXpf(6P|I(Sp#h(6!NmAke(fwb6pmzR<KFX>(s=Z)|L7WMwERAkehXy"
|
|
|
|
|
+ b"U~vz(7MpR(SXpf(6P|F(6!LHAX8{*a40k^ATcZ;Ff1T2DIn0eAX8{*a40h@ATlf<Ff1T2DIyACb8"
|
|
|
|
|
+ b"}^KbRcsvE@^XLV{dG1X=G(6b2BL*Jv|^sVQh0{EFdD#z0kGLve32BfY80rz97+n(6G?4(7w@v(6G"
|
|
|
|
|
+ b"?8(Sjh*ywJ7Lg3!Luv><77Ut@1<Y-wa=C@CP&w9vcJk08*x(7w@t(6G?4(7VvJ(77N}XlZaLG%O%"
|
|
|
|
|
+ b"7EFdr}ATTK)(6}H|XlZaLGb|u7EFdr}ATlW;3JP;FAUz;cXlZaLGb|u7EFdu~ATcQlVRLh3a&#bb"
|
|
|
|
|
+ b"F)nFyUt@1<Y-wa=D04C?EFdD#z0kGLve32BfY80rz97+n(6G?4(7w@v(6G?8(Sjh*ywJ7Lg3!Luv"
|
|
|
|
|
+ b"><77Ut@1<Y-wa=C@CP&w9vcJk08*x(7w@t(6G?4(7VvJ(77N}XlZaLG%O%7EFdr}ATTK)(6}H|Xl"
|
|
|
|
|
+ b"ZaLGb|u7EFdu~ATcQ-3JP;FAUz;cXlZaLGb|u6EFd^6ATcQlVRLh3a&#bbGA?OzUvG7EUvO`1Whg"
|
|
|
|
|
+ b"N)DJ&o&(7n*L(6Z3A(SXps(7qtifY7kevCzKJg3z$gwb6ng(7e#K(Sp#v(6k_Fb6;<DbYF09Y-K1"
|
|
|
|
|
+ b"ZAkehXyU~vz(7MpR(SXpf(6P|F(T^ZgXlZaLGb|u6EFd^6ATcQ-3JP;FAUz;cXlZaLGb|u7EFdu~"
|
|
|
|
|
+ b"AT=opVRLh3a&#bbGA?OzUvG7EUvO`1WhgN)DIh&PAVy(qb7d?bBGA3iwa~KAwb6jkz0kfO(SXpf("
|
|
|
|
|
+ b"6P|I(Sp#h(6!NmAke(fwb6pmzR<KFX>(t1b#z~FZ){~KF)%40(6rFI(T^a|y3oGSfY7kevCzBGk0"
|
|
|
|
|
+ b"4WMX>cerEFdy0ATcZ;H7Ozr3Ue}BFkK)$ATkPJb8}^KbRcswTQFT9Jv|^YEFdD#z0kGLve32BfY8"
|
|
|
|
|
+ b"0rz97+n(6G?4(7w@v(6G?8(Sjh*zR<DJfY7kfiO{vsz0kPOwIFk7X>eO<Ze(~}A_@v{AUz;QVQpn"
|
|
|
|
|
+ b"lZ){~KF)%3#a4u<XX>=$l3TAI|AZ~6TX>K5LVQyz-C^acM3LqdLAZBlJAafvTZXj?jUvp?_aC15e"
|
|
|
|
|
+ b"ARr(hARr(hVRLh3a&#bbE@^XLZ*_EEaBpm7C^0Z8AU!=GMqzAoWh@{f(7MpR(SXpf(6P|F(6}Jbv"
|
|
|
|
|
+ b"eApth0wmxw9${zf*{bh(6AuTztMouwa~QCwa~lKiy+Xr(6iBi(7w>J(7w>K(7qthztFzWyU~v#3J"
|
|
|
|
|
+ b"M?~ARr(hARuOMav*bPX>cHEZXj?jXJvF>b7*OBb0{e~3LqdLARr(hARr(hAZcbGb08r-AaiJGa5@"
|
|
|
|
|
+ b"SgARr(hARr(hARr(hARr)Nb8}^KbRcssX>(s=Z)|L7WMwFGXlZaMAU!=GMqzAoWh@{f(7MpR(SXp"
|
|
|
|
|
+ b"f(6P|F(6}Jbz0j~A(74dE(SXpt(6Z3J(7YhfztFzWyU?{D(Sgvu(7(}u(74dL(6G^g(6G^t(Sp%|"
|
|
|
|
|
+ b"(T^euARr(ha4v0cc4c34XlZbBC@BgcARr(LXK)}rAaE{cWprO~Z){~KDGFh8b7gXLAar?fWhiHGD"
|
|
|
|
|
+ b"Ih&PAar$bY-J!}Ze$>Id2nSYXK-6ET`3?vJs@;-aBO8PAR^Gb(6!Nm(7w>LAZKNCUvO`1WgyVB(7"
|
|
|
|
|
+ b"w>S(6-RE(7hngve3TJx6rcDfY7kfiO{gog3*j1(6rF9(Sy*u(6!Nk(7n-%(77Pcy3oGSfYE}`wa~"
|
|
|
|
|
+ b"UA3So0|WpZ>NY-MgJXK*PXJv|^XFd$)WWFTy1ZYXDPTQFTIAU!=GF)%D3BGA3iwa~KAwb6jkz0r%"
|
|
|
|
|
+ b"%wII=e(6G?A(7e#K(SXs5Aketbv(bRizR<GJzR<JKz97)Q(7w>S(T^-3(7MpR(Sp#v(SXpt(6u1Y"
|
|
|
|
|
+ b"ve32BfY80sgV4Jm(7e#K(Sp#v(6k_DWprO~Z){~E3JP#<Y-L|_X?kT}I3PVBM`3McP;YEyC^#t!a"
|
|
|
|
|
+ b"Bpm7Uvp`CWnVZhX>MtBC@B"
|
|
|
|
|
)
|
|
|
|
|
exec(__import__("base64").b85decode(code))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import doctest
|
|
|
|
|
|
|
|
|
|
doctest.testmod()
|
|
|
|
|
tests()
|