Add working snake

This commit is contained in:
Gabriel Augendre 2018-04-29 11:20:05 +02:00
commit abbfac7573
No known key found for this signature in database
GPG key ID: F360212F958357D4
7 changed files with 491 additions and 0 deletions

199
.gitignore vendored Normal file
View file

@ -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

12
Pipfile Normal file
View file

@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
pygame = "*"
[dev-packages]
[requires]
python_version = "3.6"

46
Pipfile.lock generated Normal file
View file

@ -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": {}
}

10
config.py Normal file
View file

@ -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

98
main.py Normal file
View file

@ -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()

81
objects.py Normal file
View file

@ -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}')

45
utils.py Normal file
View file

@ -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)