From abbfac757371dc0fcf52434ece4bd0f6e7093ebd Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Sun, 29 Apr 2018 11:20:05 +0200 Subject: [PATCH] Add working snake --- .gitignore | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++ Pipfile | 12 ++++ Pipfile.lock | 46 ++++++++++++ config.py | 10 +++ main.py | 98 +++++++++++++++++++++++++ objects.py | 81 +++++++++++++++++++++ utils.py | 45 ++++++++++++ 7 files changed, 491 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 config.py create mode 100644 main.py create mode 100644 objects.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9b5378 --- /dev/null +++ b/.gitignore @@ -0,0 +1,199 @@ + +# Created by https://www.gitignore.io/api/python,pycharm+all,osx + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### Python ### +# 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/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# 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/ + + +# End of https://www.gitignore.io/api/python,pycharm+all,osx diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..f1d417c --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pygame = "*" + +[dev-packages] + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..93085e1 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,46 @@ +{ + "_meta": { + "hash": { + "sha256": "04d5136a2e3e1a7589c6313b58b439879c213afd077d2d6580213f4255dcef7d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pygame": { + "hashes": [ + "sha256:22d5195a72b9a300cb7e87d7bde1373930a02b1054e95bc431d26a321ae15d07", + "sha256:2a8292d0b5a67e0b96d2cae09bbd8252e99daffe95fd88a00166b92fb6497b40", + "sha256:2ee479b0120cc82f04f7c41a09d75fc85c9992ed1a26c9527fb2b7b0637e6739", + "sha256:4847550da37d3bc702e0201a035de005f184b19e86625a3c70ab1d318317ee91", + "sha256:4d4985d0e0e5470c5ef872d65a863f1722d85a136327eb3bb5dcafc2853f2e55", + "sha256:5cf670378c8aabdf1bdf283eeec690c93f95dfe8de77c66bc837c91274c8ca49", + "sha256:643de45518d11cb7784e3fe77aef43fe3cb7e0d6b495ad4361ade931f16c334b", + "sha256:66746c32d21d8193ab066fdf90da96ed993c4818adb13bf4eaa472f306f7f421", + "sha256:73c86fdbbfc5ae77d0271fb0964f4a3fc639cc2277ffeb60ee4e981fb0d27640", + "sha256:751021819bdc0cbe5cbd51904abb6ff9e9aee5b0e8955af02284d0e77d6c9ec2", + "sha256:792f8d1a455d5d1de9c8f2cc051c6dac2b90705afc4cf9fe4933aeb296b0ae3f", + "sha256:831c906d4a70aa58f9382e79671525805583b58ac8c895b4c4bd066e8858a1a1", + "sha256:834ee27388563f15e6c736aa4850e4cf95609f67056ee73ff646528bb658e79e", + "sha256:925e6be8e8c3f1cec016f5a68f369491ae34600e04633759e0e07780ce7bd083", + "sha256:96a8f61c729d82576ee3d5cfe3b0b91e7b52a4fbf3d618a3b659347eeb4fd937", + "sha256:97615fb075808b4709f6115e3a11a253bfac78614f430bededbc0ac76a5100af", + "sha256:c49ece0e4ba71be90ed4bf6a905c35e5c1a95c4c0b0b42eaff74ab69c5793847", + "sha256:e8626d4d4e7617ebf103d96a493b9cbe53f9b39cd46ecbff232e33ba4a082530", + "sha256:ea7da8dd7fbc12becccf691b4c76fbb381e369e124c30e21f9ed263b6e7e139a" + ], + "index": "pypi", + "version": "==1.9.3" + } + }, + "develop": {} +} diff --git a/config.py b/config.py new file mode 100644 index 0000000..f4eeddb --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +BACKGROUND_COLOR = 0, 0, 0 +APPLE_COLOR = 255, 0, 0 +SNAKE_COLOR = 0, 255, 0 +FONT_COLOR = 250, 250, 250 + +MAP_SIZE = 41, 31 +TILE_SIZE = 20 +RESOLUTION = MAP_SIZE[0] * TILE_SIZE, MAP_SIZE[1] * TILE_SIZE + +INITIAL_SNAKE_SIZE = 3 diff --git a/main.py b/main.py new file mode 100644 index 0000000..6a0ce39 --- /dev/null +++ b/main.py @@ -0,0 +1,98 @@ +import logging + +import pygame +from pygame import locals as pglocals + +from config import RESOLUTION, SNAKE_COLOR, BACKGROUND_COLOR, FONT_COLOR, INITIAL_SNAKE_SIZE +from objects import Snake, Apple +from utils import get_score_text, Direction + +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger(__name__) + +pygame.init() + + +def main(): + clock = pygame.time.Clock() + logger.debug('pygame initialized') + + if not pygame.font: + logger.warning('Fonts disabled') + if not pygame.mixer: + logger.warning('Sound disabled') + + screen = pygame.display.set_mode(RESOLUTION) # type: pygame.Surface + pygame.display.set_caption('Snake') + + snake = Snake() + screen.fill(SNAKE_COLOR, snake.head) + apple = Apple() + apple.display(screen) + + score = 0 + score_text, score_rect = get_score_text(score) + screen.blit(score_text, score_rect) + + pygame.display.flip() + + while True: + for event in pygame.event.get(): + if event.type == pglocals.QUIT: + logger.warning('Received QUIT event') + return + if event.type == pglocals.KEYDOWN: + if event.key == pglocals.K_DOWN: + snake.direction = Direction.DOWN + elif event.key == pglocals.K_LEFT: + snake.direction = Direction.LEFT + elif event.key == pglocals.K_UP: + snake.direction = Direction.UP + elif event.key == pglocals.K_RIGHT: + snake.direction = Direction.RIGHT + + dirty_rects = snake.move(screen, apple) + if snake.dead: + logger.info(f'Vous avez perdu ! Score : {score}') + break + + if snake.head.colliderect(apple.rect): + apple.renew() + score += apple.score + logger.info(f'Apple eaten, new score : {score}') + + screen.fill(BACKGROUND_COLOR, score_rect) + old_score_rect = score_rect + + score_text, score_rect = get_score_text(score) + screen.blit(score_text, score_rect) + dirty_rects.append(score_rect.union(old_score_rect)) + + dirty_rects.append(apple.display(screen)) + + pygame.display.update(dirty_rects) + + # Run faster as snake grows + clock.tick(10 - INITIAL_SNAKE_SIZE + len(snake.slots)) + + screen.fill(BACKGROUND_COLOR) + + font = pygame.font.Font(None, 60) + text = font.render(f"PERDU ! Score : {score}", 1, FONT_COLOR) + text_rect = text.get_rect() # type: pygame.Rect + text_rect.center = screen.get_rect().center + + screen.blit(text, text_rect) + pygame.display.flip() + + while True: + for event in pygame.event.get(): + if event.type == pglocals.QUIT: + logger.info('Received QUIT event') + return + + clock.tick(5) + + +if __name__ == '__main__': + main() diff --git a/objects.py b/objects.py new file mode 100644 index 0000000..446c1c1 --- /dev/null +++ b/objects.py @@ -0,0 +1,81 @@ +import logging + +import pygame + +from config import INITIAL_SNAKE_SIZE, RESOLUTION, TILE_SIZE, SNAKE_COLOR, BACKGROUND_COLOR, APPLE_COLOR +from utils import make_slot, Direction, random_slot + +logger = logging.getLogger(__name__) + + +class Snake: + def __init__(self, ): + self.slots = [make_slot(21, 16)] * INITIAL_SNAKE_SIZE + self._direction = None # type: Direction + self.dead = False + + def add_slot(self, left: int, top: int): + self.slots.append(make_slot(left, top)) + + def move(self, screen: pygame.Surface, apple: 'Apple'): + if self.direction is None: + return [] + + new_head = self.slots[0].move(*self.direction.value) + + if new_head.collidelist(self.slots[1:]) > -1: + self.dead = True + return [] + + if new_head.right > RESOLUTION[0]: + new_head = pygame.Rect(0, new_head.top, TILE_SIZE, TILE_SIZE) + elif new_head.left < 0: + new_head = pygame.Rect(RESOLUTION[0] - TILE_SIZE, new_head.top, TILE_SIZE, TILE_SIZE) + if new_head.bottom > RESOLUTION[1]: + new_head = pygame.Rect(new_head.left, 0, TILE_SIZE, TILE_SIZE) + elif new_head.top < 0: + new_head = pygame.Rect(new_head.left, RESOLUTION[1] - TILE_SIZE, TILE_SIZE, TILE_SIZE) + + self.slots.insert(0, new_head) + screen.fill(SNAKE_COLOR, new_head) + + if not new_head.colliderect(apple.rect): + old_tail = self.slots.pop() + screen.fill(BACKGROUND_COLOR, old_tail) + return [old_tail, new_head] + + return [new_head] + + @property + def head(self) -> pygame.Rect: + return self.slots[0] + + @head.setter + def head(self, value: pygame.Rect): + self.slots.pop(0) + self.slots.insert(0, value) + + @property + def direction(self) -> Direction: + return self._direction + + @direction.setter + def direction(self, value: Direction): + if not value.is_opposed_to(self.direction): + self._direction = value + else: + logger.debug('Move prohibited : tried to change to an opposed direction') + + +class Apple: + def __init__(self): + self.rect = random_slot() + self.score = 10 + + def display(self, screen: pygame.Surface) -> pygame.Rect: + screen.fill(APPLE_COLOR, self.rect) + return self.rect + + def renew(self): + self.rect = random_slot() + logger.debug(f'Apple generated at {self.rect.left / TILE_SIZE} {self.rect.top / TILE_SIZE}') diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..5fef495 --- /dev/null +++ b/utils.py @@ -0,0 +1,45 @@ +import random +from enum import Enum + +import pygame + +from config import FONT_COLOR, RESOLUTION, MAP_SIZE, TILE_SIZE + + +def get_score_text(score): + font = pygame.font.Font(None, 30) + text = font.render(f"Score : {score}", 1, FONT_COLOR) + text_rect = text.get_rect() # type: pygame.Rect + text_rect.left = 0 + text_rect.bottom = RESOLUTION[1] + return text, text_rect + + +def make_slot(left: int, top: int) -> pygame.Rect: + if left >= MAP_SIZE[0] or left < 0: + raise ValueError(f'left must be between 0 and {MAP_SIZE[0]}') + if top >= MAP_SIZE[1] or top < 0: + raise ValueError(f'top must be between 0 and {MAP_SIZE[1]}') + + return pygame.Rect(left * TILE_SIZE, top * TILE_SIZE, TILE_SIZE, TILE_SIZE) + + +def random_slot() -> pygame.Rect: + return make_slot(random.randint(0, MAP_SIZE[0] - 1), random.randint(0, MAP_SIZE[1] - 1)) + + +class Direction(Enum): + UP = 0, -TILE_SIZE + RIGHT = TILE_SIZE, 0 + DOWN = 0, TILE_SIZE + LEFT = -TILE_SIZE, 0 + + @staticmethod + def are_opposed(d1, d2): + return (d1 == Direction.UP and d2 == Direction.DOWN + or d1 == Direction.DOWN and d2 == Direction.UP + or d1 == Direction.LEFT and d2 == Direction.RIGHT + or d1 == Direction.RIGHT and d2 == Direction.LEFT) + + def is_opposed_to(self, other): + return Direction.are_opposed(self, other)