From 53216391dcde13bee5f0bac2a615f4aa22d624e1 Mon Sep 17 00:00:00 2001 From: Bradley Cornford Date: Tue, 18 Jul 2017 14:05:00 +0100 Subject: [PATCH] Initiall development. --- .gitignore | 20 ++ .travis.yml | 13 + LICENSE.txt | 21 ++ MANIFEST.in | 5 + README.md | 43 +++ data/database.json | 1 + sensehatsnake/__init__.py | 0 sensehatsnake/example.config.py | 10 + sensehatsnake/lib/__init__.py | 0 sensehatsnake/lib/apple.py | 75 +++++ sensehatsnake/lib/board.py | 68 +++++ sensehatsnake/lib/game.py | 500 ++++++++++++++++++++++++++++++++ sensehatsnake/lib/snake.py | 165 +++++++++++ sensehatsnake/main.py | 25 ++ setup.cfg | 2 + setup.py | 60 ++++ test/__init__.py | 0 test/lib/__init__.py | 0 test/lib/test_apple.py | 56 ++++ test/lib/test_board.py | 43 +++ test/lib/test_game.py | 93 ++++++ test/lib/test_snake.py | 90 ++++++ test/test_main.py | 8 + tox.ini | 21 ++ 24 files changed, 1319 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.txt create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 data/database.json create mode 100644 sensehatsnake/__init__.py create mode 100644 sensehatsnake/example.config.py create mode 100644 sensehatsnake/lib/__init__.py create mode 100644 sensehatsnake/lib/apple.py create mode 100644 sensehatsnake/lib/board.py create mode 100644 sensehatsnake/lib/game.py create mode 100644 sensehatsnake/lib/snake.py create mode 100644 sensehatsnake/main.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test/__init__.py create mode 100644 test/lib/__init__.py create mode 100644 test/lib/test_apple.py create mode 100644 test/lib/test_board.py create mode 100644 test/lib/test_game.py create mode 100644 test/lib/test_snake.py create mode 100644 test/test_main.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f9c690 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# general things to ignore +build/ +dist/ +*.egg-info/ +*.egg +*.py[cod] +__pycache__/ +*.so +*~ + +# due to using tox and pytest +.tox +.cache + +# editor ignores +.idea + +# package ignores +sensehatsnake/config.py +data/database.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ac2f58d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python + +env: + - TOXENV=py27 + - TOXENV=py33 + - TOXENV=py34 + +install: pip install tox + +script: tox + +notifications: + email: false \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0cfe9fd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Bradley Cornford + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..75af3b0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +# Include the license file +include LICENSE.txt + +# Include the data files +recursive-include data * \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7a3c41 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Raspberry Pi Python sense hat snake + +A project to create a Snake game on a Raspberry Pi using a Sense Hat. + +## Requirements + +This package requires the following system packages to be installed: + +- python-pip +- python-dev + +## Installation + +Begin by installing this packages requirements: + + pip install -e . + +Finally copy the example configuration file `example.config.py`, and save it as `config.py` + + cp sensehatsnake/example.config.py sensehatsnake/config.py + +## Configuration + +You can now configure Sense-Hat-Snake in a few simple steps. Open `sensehatsnake/config.py` and update the options as needed. + +- `columns` - The number of columns the board has. +- `rows` - The number of rows the board has. +- `fps` - The games frames per second. +- `countdown` - The game countdown in seconds. +- `interval` - The game tick interval in milliseconds. +- `score_increment` - The number to increment score by in game. +- `level_increment` - The score when to increment the level by in game. +- `interval_increment` - The number to reduce the game tick interval by in milliseconds. + +## Usage + +It's really as simple as running the main file + + sudo python sensehatsnake/main.py + +### License + +Sense-Hat-Snake is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) \ No newline at end of file diff --git a/data/database.json b/data/database.json new file mode 100644 index 0000000..f15d35f --- /dev/null +++ b/data/database.json @@ -0,0 +1 @@ +{"_default": {"1": {"score": 0}, "2": {"score": 0}, "3": {"score": 1}, "4": {"score": 0}, "5": {"score": 0}, "6": {"score": 0}, "7": {"score": 0}, "8": {"score": 0}}} \ No newline at end of file diff --git a/sensehatsnake/__init__.py b/sensehatsnake/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensehatsnake/example.config.py b/sensehatsnake/example.config.py new file mode 100644 index 0000000..367a3bb --- /dev/null +++ b/sensehatsnake/example.config.py @@ -0,0 +1,10 @@ +game = { + 'columns': 8, + 'rows': 8, + 'fps': 30, + 'countdown': 5, + 'interval': 800, + 'score_increment': 1, + 'level_increment': 8, + 'interval_increment': 50 +} diff --git a/sensehatsnake/lib/__init__.py b/sensehatsnake/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensehatsnake/lib/apple.py b/sensehatsnake/lib/apple.py new file mode 100644 index 0000000..da79eee --- /dev/null +++ b/sensehatsnake/lib/apple.py @@ -0,0 +1,75 @@ +from __future__ import print_function + + +class Apple: + COORDINATES = None + + X = None + Y = None + + def __init__(self, coordinates, x=0, y=0): + print("[Apple][info] Initialising Apple") + + self.COORDINATES = coordinates + self.X = x + self.Y = y + + def set_coordinates(self, coordinates): + print("[Apple][info] Setting Apple coordinates") + + self.COORDINATES = coordinates + + def set_x(self, x): + print("[Apple][info] Setting Apple X position") + + self.X = x + + def set_y(self, y): + print("[Apple][info] Setting Apple Y position") + + self.Y = y + + def set_position(self, coordinates): + print("[Apple][info] Setting Apple X, Y position") + + x, y = coordinates + + self.X = x + self.Y = y + + def coordinates(self): + print("[Apple][info] Getting Apple coordinates") + + return self.COORDINATES + + def x(self): + print("[Apple][info] Getting Apple X position") + + return self.X + + def y(self): + print("[Apple][info] Getting Apple Y position") + + return self.Y + + def position(self): + print("[Apple][info] Getting Apple X, Y position") + + return (self.X, self.Y) + + def clear(self): + print("[Apple][info] Clearing Apple") + + self.COORDINATES = None + self.X = None + self.Y = None + + def cleanup(self): + print("[Apple][info] Apple clean up") + + self.clear() + + def __exit__(self): + print("[Apple][info] Apple exit") + + self.cleanup() diff --git a/sensehatsnake/lib/board.py b/sensehatsnake/lib/board.py new file mode 100644 index 0000000..d417cde --- /dev/null +++ b/sensehatsnake/lib/board.py @@ -0,0 +1,68 @@ +from __future__ import print_function + + +class Board: + COLUMNS = None + + ROWS = None + + COORDINATES = None + + def __init__(self, columns, rows): + print("[Board][info] Initialising Board") + + self.COLUMNS = columns + self.ROWS = rows + self.__generate() + + def __generate(self): + self.COORDINATES = [ + [0 for x in xrange(self.COLUMNS)] + for y in xrange(self.ROWS) + ] + + def columns(self): + print("[Board][info] Getting Board columns") + + return self.COLUMNS + + def rows(self): + print("[Board][info] Getting Board rows") + + return self.ROWS + + def coordinates(self): + print("[Board][info] Getting Board coordinates") + + return self.COORDINATES + + def check_collision(self, snake, offset): + print("[Board][info] Checking for Snake collision") + + offset_x, offset_y = offset + + if 0 > offset_x or offset_x > self.ROWS - 1 or 0 > offset_y or offset_y > self.COLUMNS - 1: + return True + else: + for segment in snake.segments(): + if segment[0] == offset_x and segment[1] == offset_y: + return True + + return False + + def clear(self): + print("[Board][info] Clearing board") + + self.COORDINATES = None + self.COLUMNS = None + self.ROWS = None + + def cleanup(self): + print("[Board][info] Board clean up") + + self.clear() + + def __exit__(self): + print("[Board][info] Board exit") + + self.cleanup() diff --git a/sensehatsnake/lib/game.py b/sensehatsnake/lib/game.py new file mode 100644 index 0000000..5b532e8 --- /dev/null +++ b/sensehatsnake/lib/game.py @@ -0,0 +1,500 @@ +from __future__ import print_function +from board import Board +from apple import Apple +from mock import MagicMock, patch +from random import randint +from snake import Snake +from tinydb import TinyDB, Query +import math +import pygame +import sys + + +try: + from sense_hat import SenseHat +except ImportError: + print("[Game][error] An error occurred importing 'sense_hat.SenseHat'") + mock = MagicMock() + mock.clear.return_value = True + mock.set_pixel.return_value = True + mock.show_message.return_value = True + + with patch.dict('sys.modules', {'sense_hat': mock, 'sense_hat.SenseHat': mock.RTIMU}): + from sense_hat import SenseHat + + +class Game: + BLOCK_SIZE = 36 + + COLUMNS = 8 + ROWS = 8 + + FPS = None + + COLORS = [ + # 0 - Black + (0, 0, 0), + # 1 - Green + (0, 255, 0), + # 2 - Red + (255, 0, 0), + # 3 - Purple + (128, 0, 128), + # 4 - Blue + (0, 0, 255), + # 5 - Orange + (255, 165, 0), + # 6 - Cyan + (0, 255, 255), + # 7 - Yellow + (255, 255, 0), + # 8 - Dark Grey + (35, 35, 35), + # 9 - White + (255, 255, 255) + ] + + SHAPES = [ + # Snake + [ + [1], + ], + + # Apple + [ + [2] + ], + ] + + WIDTH = None + HEIGHT = None + + COUNTDOWN = None + + SCORE_INCREMENT = None + + SCORE = 0 + APPLES = 0 + + INTERVAL = None + INTERVAL_INCREMENT = None + + LEVEL = 1 + LEVEL_INCREMENT = None + + PAUSED = False + + GAMEOVER = False + + BACKGROUND_GRID = None + + SNAKE_MOVED = True + + pygame = None + pygame_font = None + pygame_screen = None + sensehat = None + db = None + + board = None + snake = None + apple = None + + def __init__(self, columns, rows, fps, countdown, interval, score_increment, level_increment, interval_increment, pygame_instance=None): + self.COLUMNS = columns + self.ROWS = rows + self.FPS = fps + self.COUNTDOWN = countdown + self.INTERVAL = interval + self.SCORE_INCREMENT = score_increment + self.LEVEL_INCREMENT = level_increment + self.INTERVAL_INCREMENT = interval_increment + + if pygame_instance is None: + self.pygame = pygame + else: + self.pygame = pygame_instance + + self.sensehat = SenseHat() + self.db = TinyDB('data/database.json') + + try: + self.WIDTH = self.BLOCK_SIZE * self.COLUMNS + 150 + self.HEIGHT = self.BLOCK_SIZE * self.ROWS + + self.BACKGROUND_GRID = [ + [8 if x % 2 == y % 2 else 0 for x in xrange(self.COLUMNS)] + for y in xrange(self.ROWS) + ] + + self.pygame.init() + self.pygame.key.set_repeat(0, 0) + self.pygame_font = self.pygame.font.Font(self.pygame.font.get_default_font(), 12) + self.pygame_screen = self.pygame.display.set_mode((self.WIDTH, self.HEIGHT), 0, 24) + self.pygame.event.set_blocked(self.pygame.MOUSEMOTION) + + self.board = Board(self.COLUMNS, self.ROWS) + self.__generate_snake() + self.__generate_apple() + except AttributeError: + print("[Game][error] An error occurred initialising game") + + def start(self, run_once=False): + print("[Game][info] Starting game") + + try: + pygame_wait = True + + while pygame_wait: + for event in self.pygame.event.get(): + if event.type == self.pygame.KEYDOWN: + if event.key == self.pygame.K_RETURN: + pygame_wait = False + elif event.key == self.pygame.K_ESCAPE: + self.quit() + + self.pygame_screen.fill(self.COLORS[0]) + self.__display_message("Press to start") + self.pygame.display.update() + + self.sensehat.clear() + except AttributeError: + print("[Game][error] An error occurred starting game") + + self.__countdown() + self.__loop() + self.finish() + + if run_once is not True: + self.start() + + def __countdown(self): + print("[Game][info] Starting game countdown") + + try: + seconds = 0 + + while True: + self.pygame_screen.fill(self.COLORS[0]) + remaining = (self.COUNTDOWN - seconds) + + if seconds > self.COUNTDOWN: + break + + if seconds == self.COUNTDOWN: + self.__display_message("Go!") + else: + self.__display_message("%d!" % remaining) + + seconds += 1 + self.sensehat.clear() + self.pygame.display.update() + self.pygame.time.wait(1000) + + self.pygame_screen.fill(self.COLORS[0]) + self.sensehat.clear() + except AttributeError: + print("[Game][error] An error occurred starting game countdown") + + def __loop(self): + print("[Game][info] Starting game loop") + + try: + self.pygame.time.set_timer(pygame.USEREVENT + 1, self.INTERVAL) + + key_actions = { + 'ESCAPE': lambda: self.quit(), + 'LEFT': lambda: self.__direction_left(), + 'RIGHT': lambda: self.__direction_right(), + 'DOWN': lambda: self.__direction_down(), + 'UP': lambda: self.__direction_up(), + 'p': lambda: self.toggle_pause(), + } + + pygame_clock = self.pygame.time.Clock() + + while not self.GAMEOVER: + self.sensehat.clear() + self.pygame_screen.fill(self.COLORS[0]) + + if self.PAUSED: + self.__display_message("Paused") + else: + self.__draw_line( + ((self.BLOCK_SIZE * self.COLUMNS) + 1, 0), + ((self.BLOCK_SIZE * self.COLUMNS) + 1, (self.HEIGHT - 1)), + self.COLORS[9] + ) + + self.__display_message( + "Score: %d\n\nLevel: %d\n\nApples: %d" % (self.SCORE, self.LEVEL, self.APPLES), + ((self.BLOCK_SIZE * self.COLUMNS) + self.BLOCK_SIZE, 2), + self.COLORS[9], + self.COLORS[0], + False + ) + + self.__draw_matrix(self.BACKGROUND_GRID, (0, 0), None, False) + self.__draw_matrix(self.board.coordinates(), (0, 0), None, False) + self.__draw_matrix(self.snake.coordinates(self.COLUMNS, self.ROWS), (0, 0)) + self.__draw_matrix(self.apple.coordinates(), (self.apple.x(), self.apple.y())) + + self.pygame.display.update() + + for event in self.pygame.event.get(): + if event.type == self.pygame.USEREVENT + 1: + self.__move() + elif event.type == self.pygame.QUIT: + self.quit() + elif event.type == self.pygame.KEYDOWN: + for key in key_actions: + if event.key == eval("self.pygame.K_" + key): + key_actions[key]() + + pygame_clock.tick(self.FPS) + except AttributeError: + print("[Game][error] An error occurred during game loop") + + def __generate_snake(self): + print("[Game][info] Generating snake") + + self.snake = Snake(self.SHAPES[0], int(math.floor(self.COLUMNS / 2)), int(math.floor(self.ROWS / 2))) + + def __generate_apple(self): + print("[Game][info] Generating apple") + + apple_collision = True + offset_x, offset_y = (0, 0) + + while apple_collision: + offset_x = randint(0, self.ROWS - 1) + offset_y = randint(0, self.COLUMNS - 1) + + apple_collision = self.board.check_collision(self.snake, (offset_x, offset_y)) + + self.apple = Apple(self.SHAPES[1], offset_x, offset_y) + + def __display_message(self, message, coordinates=None, color=COLORS[9], background_color=COLORS[0], sensehat=True): + print("[Game][info] Displaying message") + + if sensehat: + self.sensehat.show_message(message, text_colour=color, scroll_speed=0.05) + + for i, line in enumerate(message.splitlines()): + message_image = self.pygame_font.render(line, False, color, background_color) + + if coordinates is not None: + position_x, position_y = coordinates + else: + message_image_center_x, message_image_center_y = message_image.get_size() + message_image_center_x //= 2 + message_image_center_y //= 2 + position_x = self.WIDTH // 2 - message_image_center_x + position_y = self.HEIGHT // 2 - message_image_center_y + + self.pygame_screen.blit( + message_image, + (position_x, position_y + i * 22) + ) + + def __draw_line(self, start_position, end_position, color=COLORS[9], sensehat=True): + print("[Game][info] Drawing line") + + if sensehat: + start_x, start_y = start_position + end_x, end_y = start_position + + for i in xrange(start_y, end_y): + for j in xrange(start_x, end_x): + if start_x == end_x or start_y == end_y: + self.sensehat.set_pixel(j, i, color) + + self.pygame.draw.line(self.pygame_screen, color, start_position, end_position) + + def __draw_matrix(self, matrix, offset, color=None, sensehat=True): + print("[Game][info] Drawing matrix") + + offset_x, offset_y = offset + + for y, row in enumerate(matrix): + for x, val in enumerate(row): + if val: + if color is None: + shape_color = self.COLORS[val] + else: + shape_color = color + + if sensehat: + self.sensehat.set_pixel((offset_x + x), (offset_y + y), shape_color) + + self.pygame.draw.rect( + self.pygame_screen, + shape_color, + self.pygame.Rect((offset_x + x) * self.BLOCK_SIZE, (offset_y + y) * self.BLOCK_SIZE, self.BLOCK_SIZE, self.BLOCK_SIZE), + 0 + ) + + def __count_clear_apples(self, apples): + print("[Game][info] Counting cleared apples") + + if apples > 0: + self.APPLES += apples + self.SCORE += self.SCORE_INCREMENT * self.LEVEL + + if self.APPLES >= self.LEVEL * self.LEVEL_INCREMENT: + self.LEVEL += 1 + delay = self.INTERVAL - self.INTERVAL_INCREMENT * (self.LEVEL - 1) + delay = 100 if delay < 100 else delay + self.pygame.time.set_timer(self.pygame.USEREVENT + 1, delay) + + def __move(self): + print("[Game][info] Moving snake %s" % (self.snake.direction())) + + if not self.GAMEOVER and not self.PAUSED: + new_x, new_y = self.snake.head() + + if self.snake.direction() == self.snake.DIRECTION_UP: + new_y = self.snake.y() - 1 + elif self.snake.direction() == self.snake.DIRECTION_DOWN: + new_y = self.snake.y() + 1 + elif self.snake.direction() == self.snake.DIRECTION_LEFT: + new_x = self.snake.x() - 1 + elif self.snake.direction() == self.snake.DIRECTION_RIGHT: + new_x = self.snake.x() + 1 + + if self.board.check_collision(self.snake, (new_x, new_y)): + self.GAMEOVER = True + + return False + + tail_x, tail_y = self.snake.tail() + self.snake.set_position((new_x, new_y)) + cleared_apples = 0 + + if new_x == self.apple.x() and new_y == self.apple.y(): + self.snake.add_segment((tail_x, tail_y)) + cleared_apples += 1 + self.__generate_apple() + + self.__count_clear_apples(cleared_apples) + self.SNAKE_MOVED = True + + return True + + def __direction_up(self): + print("[Game][info] Event direction up") + + if self.snake.direction() != self.snake.DIRECTION_DOWN and self.SNAKE_MOVED is True: + self.snake.set_direction(self.snake.DIRECTION_UP) + self.SNAKE_MOVED = False + + def __direction_down(self): + print("[Game][info] Event direction down") + + if self.snake.direction() != self.snake.DIRECTION_UP and self.SNAKE_MOVED is True: + self.snake.set_direction(self.snake.DIRECTION_DOWN) + self.SNAKE_MOVED = False + + def __direction_left(self): + print("[Game][info] Event direction left") + + if self.snake.direction() != self.snake.DIRECTION_RIGHT and self.SNAKE_MOVED is True: + self.snake.set_direction(self.snake.DIRECTION_LEFT) + self.SNAKE_MOVED = False + + def __direction_right(self): + print("[Game][info] Event direction right") + + if self.snake.direction() != self.snake.DIRECTION_LEFT and self.SNAKE_MOVED is True: + self.snake.set_direction(self.snake.DIRECTION_RIGHT) + self.SNAKE_MOVED = False + + def toggle_pause(self): + print("[Game][info] Toggling paused state") + + self.PAUSED = not self.PAUSED + + def get_score(self): + print("[Game][info] Calculating score") + + return self.SCORE + + def print_score(self, high_score=False): + print("[Game][info] Printing score") + + score = self.get_score() + + try: + self.sensehat.clear() + self.pygame_screen.fill(self.COLORS[0]) + + if high_score: + self.__display_message("Game Over!\n\nHigh score: %d" % score) + self.pygame.display.update() + else: + self.__display_message("Game Over!\n\nYour score: %d!" % self.get_score()) + self.pygame.display.update() + + self.pygame.time.wait(3000) + except AttributeError: + print("[Game][error] An error occurred printing score") + + def finish(self): + print("[Game][info] Finishing game") + + score = self.get_score() + + self.pygame.display.update() + + if self.db.contains(Query().score >= score): + self.print_score() + else: + self.print_score(True) + + self.db.insert({'score': score}) + self.reset() + + def quit(self): + print("[Game][info] Quitting game") + + self.pygame_screen.fill(self.COLORS[0]) + self.sensehat.clear() + self.__display_message("Exiting...") + self.pygame.display.update() + sys.exit() + + def reset(self): + print("[Game][info] Resetting game") + + self.PAUSED = False + self.GAMEOVER = False + self.SCORE = 0 + self.APPLES = 0 + self.LEVEL = 1 + self.SNAKE_MOVED = True + + self.board = Board(self.COLUMNS, self.ROWS) + self.snake = None + self.apple = None + self.__generate_snake() + self.__generate_apple() + + self.sensehat.clear() + self.pygame.time.set_timer(pygame.USEREVENT + 1, 0) + self.pygame.display.update() + + def cleanup(self): + print("[Game][info] Game clean up") + + try: + self.sensehat.clear() + self.pygame_screen.fill(self.COLORS[0]) + self.pygame.display.update() + self.pygame.quit() + except AttributeError: + print("[Game][error] An error occurred cleaning up") + + def __exit__(self): + print("[Game][info] Game exit") + + self.cleanup() diff --git a/sensehatsnake/lib/snake.py b/sensehatsnake/lib/snake.py new file mode 100644 index 0000000..168b207 --- /dev/null +++ b/sensehatsnake/lib/snake.py @@ -0,0 +1,165 @@ + + +class Snake: + COORDINATES = None + + SEGMENTS = None + + X = None + Y = None + + DIRECTION = None + + DIRECTION_UP = 'up' + DIRECTION_DOWN = 'down' + DIRECTION_LEFT = 'left' + DIRECTION_RIGHT = 'right' + + def __init__(self, coordinates, x=0, y=0, direction=DIRECTION_LEFT): + print("[Snake][info] Initialising Snake") + + self.COORDINATES = coordinates + self.SEGMENTS = [] + self.add_segment((x, y)) + self.X = x + self.Y = y + self.DIRECTION = direction + + def set_coordinates(self, coordinates): + print("[Snake][info] Setting Snake coordinates") + + self.COORDINATES = coordinates + + def set_direction(self, direction): + print("[Snake][info] Setting Snake direction") + + self.DIRECTION = direction + + def set_x(self, x): + print("[Snake][info] Setting Snake X position") + + if self.length() > 1: + self.remove_segment(-1) + self.add_segment((x, self.SEGMENTS[0][1]), 0) + else: + self.SEGMENTS[0] = (x, self.SEGMENTS[0][1]) + + self.X = x + + def set_y(self, y): + print("[Snake][info] Setting Snake Y position") + + if self.length() > 1: + self.remove_segment(-1) + self.add_segment((self.SEGMENTS[0][0], y), 0) + else: + self.SEGMENTS[0] = (self.SEGMENTS[0][0], y) + + self.Y = y + + def set_position(self, coordinates): + print("[Snake][info] Setting Snake X, Y position") + + x, y = coordinates + + if self.length() > 1: + self.remove_segment(-1) + self.add_segment(coordinates, 0) + else: + self.SEGMENTS[0] = coordinates + + self.X = x + self.Y = y + + def add_segment(self, coordinates, position=-1): + print("[Snake][info] Creating Snake segment at index %d" % (position)) + + self.SEGMENTS.insert(position if position != -1 else len(self.SEGMENTS), coordinates) + + def remove_segment(self, position=-1): + print("[Snake][info] Removing Snake segment at index %d" % (position)) + + self.SEGMENTS.pop(position) + + def coordinates(self, columns=0, rows=0): + print("[Snake][info] Getting Snake coordinates") + + if columns == 0 and rows == 0: + return self.COORDINATES + + coordinates = [ + [False for x in xrange(columns)] + for y in xrange(rows) + ] + + for segment in self.SEGMENTS: + coordinates[segment[1]][segment[0]] = self.COORDINATES[0][0] + + return coordinates + + def direction(self): + print("[Snake][info] Getting Snake direction") + + return self.DIRECTION + + def x(self): + print("[Snake][info] Getting Snake X position") + + return self.X + + def y(self): + print("[Snake][info] Getting Snake Y position") + + return self.Y + + def position(self): + print("[Snake][info] Getting Snake X,Y position") + + return (self.X, self.Y) + + def segments(self): + print("[Snake][info] Getting all Snake segments") + + return self.SEGMENTS + + def head(self): + print("[Snake][info] Getting Snake head") + + return self.SEGMENTS[0] + + def body(self): + print("[Snake][info] Getting Snake body") + + if len(self.SEGMENTS) < 2: + return None + + return self.SEGMENTS[1:] + + def tail(self): + print("[Snake][info] Getting Snake tail") + + return self.SEGMENTS[-1] + + def length(self): + print("[Snake][info] Getting Snake length") + + return len(self.SEGMENTS) + + def clear(self): + print("[Snake][info] Clearing Snake") + + self.COORDINATES = None + self.SEGMENTS = None + self.X = None + self.Y = None + self.DIRECTION = None + + def cleanup(self): + print("[Snake][info] Snake clean up") + + self.clear() + + def __exit__(self): + print("[Snake][info] Snake exit") + + self.cleanup() diff --git a/sensehatsnake/main.py b/sensehatsnake/main.py new file mode 100644 index 0000000..26ae512 --- /dev/null +++ b/sensehatsnake/main.py @@ -0,0 +1,25 @@ +from __future__ import print_function +from lib.game import Game +import atexit +import config + + +def main(): + game = Game( + config.game['columns'], + config.game['rows'], + config.game['fps'], + config.game['countdown'], + config.game['interval'], + config.game['score_increment'], + config.game['level_increment'], + config.game['interval_increment'], + ) + + game.start() + + atexit.register(game.__exit__) + + +if __name__ == '__main__': + main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3480374 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2076d1b --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +from setuptools import setup, find_packages +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='sense-hat-snake', + + version='1.0.0', + + description='Raspberry Pi Python sense hat snake', + long_description=long_description, + + url='https://github.com/bradcornford/Sense-Hat-Snake', + + author='Bradley Cornford', + author_email='me@bradleycornford.co.uk', + + license='MIT', + + classifiers=[ + 'Development Status :: 5 - Production/Stable', + + 'Intended Audience :: Developers', + + 'Topic :: Software Development :: Build Tools', + + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + + keywords='sense hat snake raspberry pi', + + packages=find_packages(exclude=['contrib', 'docs', 'test']), + + install_requires=['pygame', 'sense-hat', 'tinydb', 'mock'], + + extras_require={ + 'dev': ['check-manifest'], + 'test': ['coverage'], + }, + + package_data={}, + + data_files=[], + + entry_points={ + 'console_scripts': [] + }, +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/lib/__init__.py b/test/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/lib/test_apple.py b/test/lib/test_apple.py new file mode 100644 index 0000000..17c5a5c --- /dev/null +++ b/test/lib/test_apple.py @@ -0,0 +1,56 @@ +from __future__ import print_function +from sensehatsnake.lib.apple import Apple +import unittest + + +class AppleTestCase(unittest.TestCase): + COORDINATES = [ + [1] + ] + + apple = None + + def setUp(self): + self.apple = Apple(self.COORDINATES) + + def test__init__(self): + self.assertIsInstance(self.apple, Apple) + + def test_set_coordinates(self): + self.assertIs(self.apple.set_coordinates([[0]]), None) + self.assertEquals(self.apple.coordinates(), [[0]]) + + def test_set_x(self): + self.assertIs(self.apple.set_x(1), None) + self.assertEquals(self.apple.x(), 1) + + def test_set_y(self): + self.assertIs(self.apple.set_y(1), None) + self.assertEquals(self.apple.y(), 1) + + def test_set_position(self): + self.assertIs(self.apple.set_position((1, 1)), None) + self.assertEquals(self.apple.x(), 1) + self.assertEquals(self.apple.y(), 1) + + def test_coordinates(self): + self.assertEquals(self.apple.coordinates(), self.COORDINATES) + + def test_x(self): + self.assertEquals(self.apple.x(), 0) + + def test_y(self): + self.assertEquals(self.apple.y(), 0) + + def test_position(self): + self.assertEquals(self.apple.position(), (0, 0)) + + def test_cleanup(self): + self.assertIs(self.apple.cleanup(), None) + + def test__exit__(self): + self.assertIs(self.apple.__exit__(), None) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/lib/test_board.py b/test/lib/test_board.py new file mode 100644 index 0000000..55d1587 --- /dev/null +++ b/test/lib/test_board.py @@ -0,0 +1,43 @@ +from __future__ import print_function +from sensehatsnake.lib.board import Board +from sensehatsnake.lib.snake import Snake +import unittest + + +class BoardTestCase(unittest.TestCase): + COLUMNS = 2 + ROWS = 2 + + board = None + + def setUp(self): + self.board = Board(self.COLUMNS, self.ROWS) + + def test__init__(self): + self.assertIsInstance(self.board, Board) + + def test_columns(self): + self.assertEquals(self.board.columns(), self.COLUMNS) + + def test_rows(self): + self.assertEquals(self.board.rows(), self.ROWS) + + def test_coordinates(self): + self.assertEquals(self.board.coordinates(), [[0, 0], [0, 0]]) + + def test_check_collision(self): + snake = Snake([[1]]) + self.assertIs(self.board.check_collision(snake, (snake.x(), snake.y())), True) + + def test_clear(self): + self.assertIs(self.board.clear(), None) + + def test_cleanup(self): + self.assertIs(self.board.cleanup(), None) + + def test__exit__(self): + self.assertIs(self.board.__exit__(), None) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/lib/test_game.py b/test/lib/test_game.py new file mode 100644 index 0000000..7bfe527 --- /dev/null +++ b/test/lib/test_game.py @@ -0,0 +1,93 @@ +from __future__ import print_function +from sensehatsnake.lib.game import Game +from mock import MagicMock +import unittest + + +class GameTestCase(unittest.TestCase): + COLUMNS = 2 + ROWS = 2 + FPS = 1 + COUNTDOWN = 0 + INTERVAL = 0 + SCORE_INCREMENT = 1 + LEVEL_INCREMENT = 1 + INTERVAL_INCREMENT = 0 + + game = None + + def setUp(self): + mock = MagicMock() + + keypress_event_mock = MagicMock() + keypress_event_mock.type = 1 + keypress_event_mock.key = 1 + + userevent_event_mock = MagicMock() + userevent_event_mock.type = 3 + + mock.get_size.return_value = (0, 0) + mock.get.return_value = [keypress_event_mock, userevent_event_mock] + mock.get_ticks.side_effect = [0, 6000, 0, 5000] + + mock.Font.return_value = mock + mock.render.return_value = mock + + mock.KEYDOWN = 1 + mock.K_RETURN = 1 + mock.USEREVENT = 2 + + mock.font = mock + mock.display = mock + mock.event = mock + mock.time = mock + + self.game = Game( + self.COLUMNS, + self.ROWS, + self.FPS, + self.COUNTDOWN, + self.INTERVAL, + self.SCORE_INCREMENT, + self.LEVEL_INCREMENT, + self.INTERVAL_INCREMENT, + mock + ) + + def test__init__(self): + self.assertIsInstance(self.game, Game) + + def test_start(self): + self.assertIs(self.game.start(True), None) + + def test_toggle_pause(self): + self.assertIs(self.game.toggle_pause(), None) + self.assertEqual(self.game.PAUSED, True) + self.assertIs(self.game.toggle_pause(), None) + self.assertEqual(self.game.PAUSED, False) + + def test_get_score(self): + self.assertIs(self.game.get_score(), 0) + + def test_print_score(self): + self.assertIs(self.game.print_score(), None) + + def test_finish(self): + self.assertIs(self.game.finish(), None) + + def test_quit(self): + with self.assertRaises(SystemExit): + self.assertIs(self.game.quit(), None) + + def test_reset(self): + self.assertIs(self.game.reset(), None) + + def test_cleanup(self): + self.assertIs(self.game.cleanup(), None) + + def test__exit__(self): + self.assertIs(self.game.__exit__(), None) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/lib/test_snake.py b/test/lib/test_snake.py new file mode 100644 index 0000000..e7141d8 --- /dev/null +++ b/test/lib/test_snake.py @@ -0,0 +1,90 @@ +from __future__ import print_function +from sensehatsnake.lib.snake import Snake +import unittest + + +class SnakeTestCase(unittest.TestCase): + COORDINATES = [ + [1] + ] + + COLUMNS = 2 + ROWS = 2 + + snake = None + + def setUp(self): + self.snake = Snake(self.COORDINATES) + + def test__init__(self): + self.assertIsInstance(self.snake, Snake) + + def test_set_coordinates(self): + self.assertIs(self.snake.set_coordinates([[0]]), None) + self.assertEquals(self.snake.coordinates(), [[0]]) + + def test_set_direction(self): + self.assertIs(self.snake.set_direction(self.snake.DIRECTION_UP), None) + self.assertEquals(self.snake.direction(), self.snake.DIRECTION_UP) + + def test_set_x(self): + self.assertIs(self.snake.set_x(1), None) + self.assertEquals(self.snake.x(), 1) + + def test_set_y(self): + self.assertIs(self.snake.set_y(1), None) + self.assertEquals(self.snake.y(), 1) + + def test_set_position(self): + self.assertIs(self.snake.set_position((1, 1)), None) + self.assertEquals(self.snake.x(), 1) + self.assertEquals(self.snake.y(), 1) + + def test_add_segment(self): + self.assertIs(self.snake.add_segment((1, 1)), None) + self.assertEquals(self.snake.length(), 2) + + def test_remove_segment(self): + self.assertIs(self.snake.remove_segment(), None) + self.assertEquals(self.snake.length(), 0) + + def test_coordinates(self): + self.assertEquals(self.snake.coordinates(), self.COORDINATES) + self.assertEquals(self.snake.coordinates(self.COLUMNS, self.ROWS), [[self.COORDINATES[0][0], False], [False, False]]) + + def test_direction(self): + self.assertEquals(self.snake.direction(), self.snake.DIRECTION_LEFT) + + def test_x(self): + self.assertEquals(self.snake.x(), 0) + + def test_y(self): + self.assertEquals(self.snake.y(), 0) + + def test_position(self): + self.assertEquals(self.snake.position(), (0, 0)) + + def test_segments(self): + self.assertEquals(self.snake.segments(), [(0, 0)]) + + def test_head(self): + self.assertEquals(self.snake.head(), (0, 0)) + + def test_body(self): + self.assertEquals(self.snake.body(), None) + + def test_tail(self): + self.assertEquals(self.snake.tail(), (0, 0)) + + def test_length(self): + self.assertEquals(self.snake.length(), 1) + + def test_cleanup(self): + self.assertIs(self.snake.cleanup(), None) + + def test__exit__(self): + self.assertIs(self.snake.__exit__(), None) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..7243ef8 --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,8 @@ +from __future__ import print_function +import sensehatsnake.main +import unittest + + +class MainTestCase(unittest.TestCase): + def test__init__(self): + self.assertTrue("main" in dir(sensehatsnake.main)) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5e76ab8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,21 @@ +[tox] +envlist = py{27,33,34} + +[testenv] +basepython = + py27: python2.7 + py33: python3.3 + py34: python3.4 +deps = + check-manifest + readme_renderer + flake8 + pytest +commands = + check-manifest --ignore tox.ini,tests* + python setup.py check -m -r -s + flake8 . + py.test tests +[flake8] +exclude = .tox,*.egg,build,data +select = E,W,F \ No newline at end of file