commit 9d4e40985a36e439acea0722dbab1a5666d794de Author: Kirill Samoylenkov Date: Wed Oct 29 03:46:30 2025 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb78f5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.vscode + +.deployment + +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..32ea255 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# the_snake + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..9e7bfe4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +norecursedirs = env/* +filterwarnings = + ignore::DeprecationWarning +addopts = --tb=short -vv -p no:cacheprovider +testpaths = tests/ +python_files = test_*.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ad96991 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +flake8==5.0.4 +flake8-docstrings==1.7.0 +pep8-naming==0.13.3 +pycodestyle==2.9.1 +pygame==2.5.2 +pytest==7.1.3 +pytest-timeout==2.1.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b509030 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,14 @@ +[flake8] +max-line-lenght = 79 +max-complexity = 10 +ignore = + W503, + F811, + D100, D107, + D203, D205, D213, + D400, D401, + N806, N818 +exclude = + tests/ + venv/ + env/ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3eb98fc --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,140 @@ +import os +import sys +from multiprocessing import Process +from pathlib import Path +from typing import Any + +from pygame.time import Clock +import pytest +import pytest_timeout + +BASE_DIR = Path(__file__).resolve(strict=True).parent.parent +sys.path.append(str(BASE_DIR)) + +# Hide the pygame screen +os.environ['SDL_VIDEODRIVER'] = 'dummy' + +TIMEOUT_ASSERT_MSG = ( + 'Проект работает некорректно, проверка прервана.\n' + 'Вероятные причины ошибки:\n' + '1. Исполняемый код (например, вызов функции `main()`) оказался в ' + 'глобальной зоне видимости. Как исправить: вызов функции `main` поместите ' + 'внутрь конструкции `if __name__ == "__main__":`.\n' + '2. В цикле `while True` внутри функции `main` отсутствует вызов метода ' + '`tick` объекта `clock`. Не изменяйте прекод в этой части.' +) + + +def import_the_snake(): + import the_snake # noqa + + +@pytest.fixture(scope='session') +def snake_import_test(): + check_import_process = Process(target=import_the_snake) + check_import_process.start() + pid = check_import_process.pid + check_import_process.join(timeout=1) + if check_import_process.is_alive(): + os.kill(pid, 9) + raise AssertionError(TIMEOUT_ASSERT_MSG) + + +@pytest.fixture(scope='session') +def _the_snake(snake_import_test): + try: + import the_snake + except ImportError as error: + raise AssertionError( + 'При импорте модуль `the_snake` произошла ошибка:\n' + f'{type(error).__name__}: {error}' + ) + for class_name in ('GameObject', 'Snake', 'Apple'): + assert hasattr(the_snake, class_name), ( + f'Убедитесь, что в модуле `the_snake` определен класс `{class_name}`.' + ) + return the_snake + + +def write_timeout_reasons(text, stream=None): + """Write possible reasons of tests timeout to stream. + + The function to replace pytest_timeout traceback output with possible + reasons of tests timeout. + Appears only when `thread` method is used. + """ + if stream is None: + stream = sys.stderr + text = TIMEOUT_ASSERT_MSG + stream.write(text) + + +pytest_timeout.write = write_timeout_reasons + + +def _create_game_object(class_name, module): + try: + return getattr(module, class_name)() + except TypeError as error: + raise AssertionError( + f'При создании объекта класса `{class_name}` произошла ошибка:\n' + f'`{type(error).__name__}: {error}`\n' + f'Если в конструктор класса `{class_name}` помимо параметра ' + '`self` передаются какие-то ещё параметры - убедитесь, что для ' + 'них установлены значения по умолчанию. Например:\n' + '`def __init__(self, <параметр>=<значение_по_умолчанию>):`' + ) + + +@pytest.fixture +def game_object(_the_snake): + return _create_game_object('GameObject', _the_snake) + + +@pytest.fixture +def snake(_the_snake): + return _create_game_object('Snake', _the_snake) + + +@pytest.fixture +def apple(_the_snake): + return _create_game_object('Apple', _the_snake) + + +class StopInfiniteLoop(Exception): + pass + + +def loop_breaker_decorator(func): + call_counter = 0 + + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + nonlocal call_counter + call_counter += 1 + if call_counter > 1: + raise StopInfiniteLoop + return result + return wrapper + + +@pytest.fixture +def modified_clock(_the_snake): + class _Clock: + def __init__(self, clock_obj: Clock) -> None: + self.clock = clock_obj + + @loop_breaker_decorator + def tick(self, *args, **kwargs): + return self.clock.tick(*args, **kwargs) + + def __getattribute__(self, name: str) -> Any: + if name in ['tick', 'clock']: + return super().__getattribute__(name) + return self.clock.__getattribute__(name) + + original_clock = _the_snake.clock + modified_clock_obj = _Clock(original_clock) + _the_snake.clock = modified_clock_obj + yield + _the_snake.clock = original_clock diff --git a/tests/test_code_structure.py b/tests/test_code_structure.py new file mode 100644 index 0000000..a23dee3 --- /dev/null +++ b/tests/test_code_structure.py @@ -0,0 +1,132 @@ +import pygame +import pytest + + +EXPECTED_GAME_OBJECT_ATTRS = ( + ('атрибут', 'position'), + ('атрибут', 'body_color'), + ('метод', 'draw'), +) + + +@pytest.mark.parametrize( + 'attr_type, attr_name', + EXPECTED_GAME_OBJECT_ATTRS, + ids=[elem[1] for elem in EXPECTED_GAME_OBJECT_ATTRS] +) +def test_game_object_attributes(game_object, attr_type, attr_name): + assert hasattr(game_object, attr_name), ( + f'Убедитесь, что у объектов класса `GameObject` определен {attr_type} ' + f'`{attr_name}`.' + ) + + +EXPECTED_APPLE_ATTRS = ( + ('атрибут', 'position'), + ('атрибут', 'body_color'), + ('метод', 'draw'), + ('метод', 'randomize_position'), +) + + +def test_apple_inherits_from_game_object(_the_snake): + assert issubclass(_the_snake.Apple, _the_snake.GameObject), ( + 'Класс `Apple` должен наследоваться от класса `GameObject`.' + ) + + +@pytest.mark.parametrize( + 'attr_type, attr_name', + EXPECTED_APPLE_ATTRS, + ids=[elem[1] for elem in EXPECTED_APPLE_ATTRS] +) +def test_apple_attributes(apple, attr_type, attr_name): + assert hasattr(apple, attr_name), ( + f'Убедитесь, что у объектов класса `Apple` определен {attr_type} ' + f'`{attr_name}`.' + ) + + +EXPECTED_SNAKE_ATTRS = ( + ('атрибут', 'position'), + ('атрибут', 'body_color'), + ('атрибут', 'positions'), + ('атрибут', 'direction'), + ('метод', 'draw'), + ('метод', 'get_head_position'), + ('метод', 'move'), + ('метод', 'reset'), + ('метод', 'update_direction'), +) + + +def test_snake_inherits_from_game_object(_the_snake): + assert issubclass(_the_snake.Snake, _the_snake.GameObject), ( + 'Класс `Snake` должен наследоваться от класса `GameObject`.' + ) + + +@pytest.mark.parametrize( + 'attr_type, attr_name', + EXPECTED_SNAKE_ATTRS, + ids=[elem[1] for elem in EXPECTED_SNAKE_ATTRS] +) +def test_snake_attributes(snake, attr_type, attr_name): + assert hasattr(snake, attr_name), ( + f'Убедитесь, что у объектов класса `Snake` определен {attr_type} ' + f'`{attr_name}`.' + ) + + +EXPECTED_MODULE_ELEMENTS = ( + ('константа', 'SCREEN_WIDTH'), + ('константа', 'SCREEN_HEIGHT'), + ('константа', 'GRID_SIZE'), + ('константа', 'GRID_WIDTH'), + ('константа', 'GRID_HEIGHT'), + ('константа', 'BOARD_BACKGROUND_COLOR'), + ('константа', 'UP'), + ('константа', 'DOWN'), + ('константа', 'LEFT'), + ('константа', 'RIGHT'), + ('переменная', 'screen'), + ('переменная', 'clock'), + ('функция', 'main'), + ('функция', 'handle_keys'), +) + + +@pytest.mark.parametrize( + 'element_type, element_name', + EXPECTED_MODULE_ELEMENTS, + ids=[elem[1] for elem in EXPECTED_MODULE_ELEMENTS] +) +def test_elements_exist(element_type, element_name, _the_snake): + assert hasattr(_the_snake, element_name), ( + f'Убедитесь, что в модуле `the_snake` определена {element_type} ' + f'`{element_name}`.' + ) + + +@pytest.mark.parametrize( + 'expected_type, var_name', + ( + (pygame.Surface, 'screen'), + (pygame.time.Clock, 'clock'), + ), +) +def test_vars_type(expected_type, var_name, _the_snake): + assert isinstance(getattr(_the_snake, var_name, None), expected_type), ( + 'Убедитесь, что в модуле `the_snake` есть переменная ' + f'`{var_name}` типа `{expected_type.__name__}`.' + ) + + +@pytest.mark.parametrize( + 'func_name', + ('handle_keys', 'main'), +) +def test_vars_are_functions(func_name, _the_snake): + assert callable(getattr(_the_snake, func_name, None)), ( + f'Убедитесь, что переменная `{func_name}` - это функция.' + ) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..69f0436 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,18 @@ +import pytest + +from conftest import StopInfiniteLoop + + +@pytest.mark.timeout(1, method='thread') +@pytest.mark.usefixtures('modified_clock') +def test_main_run_without_exceptions(_the_snake): + try: + _the_snake.main() + except StopInfiniteLoop: + pass + except Exception as error: + raise AssertionError( + 'При запуске функции `main` возникло исключение: ' + f'`{type(error).__name__}: {error}`\n\n' + 'Убедитесь, что функция работает корректно.' + ) diff --git a/the_snake.py b/the_snake.py new file mode 100644 index 0000000..6d93e4c --- /dev/null +++ b/the_snake.py @@ -0,0 +1,106 @@ +from random import choice, randint + +import pygame + +# Константы для размеров поля и сетки: +SCREEN_WIDTH, SCREEN_HEIGHT = 640, 480 +GRID_SIZE = 20 +GRID_WIDTH = SCREEN_WIDTH // GRID_SIZE +GRID_HEIGHT = SCREEN_HEIGHT // GRID_SIZE + +# Направления движения: +UP = (0, -1) +DOWN = (0, 1) +LEFT = (-1, 0) +RIGHT = (1, 0) + +# Цвет фона - черный: +BOARD_BACKGROUND_COLOR = (0, 0, 0) + +# Цвет границы ячейки +BORDER_COLOR = (93, 216, 228) + +# Цвет яблока +APPLE_COLOR = (255, 0, 0) + +# Цвет змейки +SNAKE_COLOR = (0, 255, 0) + +# Скорость движения змейки: +SPEED = 20 + +# Настройка игрового окна: +screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), 0, 32) + +# Заголовок окна игрового поля: +pygame.display.set_caption('Змейка') + +# Настройка времени: +clock = pygame.time.Clock() + + +# Тут опишите все классы игры. +... + + +def main(): + # Инициализация PyGame: + pygame.init() + # Тут нужно создать экземпляры классов. + ... + + # while True: + # clock.tick(SPEED) + + # Тут опишите основную логику игры. + # ... + + +if __name__ == '__main__': + main() + + +# Метод draw класса Apple +# def draw(self): +# rect = pygame.Rect(self.position, (GRID_SIZE, GRID_SIZE)) +# pygame.draw.rect(screen, self.body_color, rect) +# pygame.draw.rect(screen, BORDER_COLOR, rect, 1) + +# # Метод draw класса Snake +# def draw(self): +# for position in self.positions[:-1]: +# rect = (pygame.Rect(position, (GRID_SIZE, GRID_SIZE))) +# pygame.draw.rect(screen, self.body_color, rect) +# pygame.draw.rect(screen, BORDER_COLOR, rect, 1) + +# # Отрисовка головы змейки +# head_rect = pygame.Rect(self.positions[0], (GRID_SIZE, GRID_SIZE)) +# pygame.draw.rect(screen, self.body_color, head_rect) +# pygame.draw.rect(screen, BORDER_COLOR, head_rect, 1) + +# # Затирание последнего сегмента +# if self.last: +# last_rect = pygame.Rect(self.last, (GRID_SIZE, GRID_SIZE)) +# pygame.draw.rect(screen, BOARD_BACKGROUND_COLOR, last_rect) + +# Функция обработки действий пользователя +# def handle_keys(game_object): +# for event in pygame.event.get(): +# if event.type == pygame.QUIT: +# pygame.quit() +# raise SystemExit +# elif event.type == pygame.KEYDOWN: +# if event.key == pygame.K_UP and game_object.direction != DOWN: +# game_object.next_direction = UP +# elif event.key == pygame.K_DOWN and game_object.direction != UP: +# game_object.next_direction = DOWN +# elif event.key == pygame.K_LEFT and game_object.direction != RIGHT: +# game_object.next_direction = LEFT +# elif event.key == pygame.K_RIGHT and game_object.direction != LEFT: +# game_object.next_direction = RIGHT + +# Метод обновления направления после нажатия на кнопку +# def update_direction(self): +# if self.next_direction: +# self.direction = self.next_direction +# self.next_direction = None