├── .gitignore ├── 01_introduction ├── maze.png ├── merge_two_images.py ├── merged.png └── player.png ├── 02_exceptions ├── load_tiles.py ├── load_tiles_buggy.py └── tile_combo.png ├── 03_semantic_errors ├── generate_maze.py └── generate_maze_buggy.py ├── 04_scientific_method ├── event_loop.py └── event_loop_buggy.py ├── 05_print ├── draw_maze.py ├── draw_maze_buggy.py ├── generate_maze.py ├── load_tiles.py ├── maze.png └── util.py ├── 06_introspection ├── draw_maze.py ├── generate_maze.py ├── load_tiles.py ├── moved.png ├── moves.py └── util.py ├── 07_interactive_debugger ├── .pdbrc ├── draw_maze.py ├── event_loop.py ├── generate_maze.py ├── load_tiles.py ├── maze_run.py ├── maze_run_buggy.py ├── moves.py └── util.py ├── 08_unit_tests ├── draw_maze.py ├── event_loop.py ├── generate_maze.py ├── load_tiles.py ├── maze_run.py ├── moves.py ├── test_crate.py └── util.py ├── 09_test_data ├── draw_maze.py ├── event_loop.py ├── expected.txt ├── generate_maze.py ├── load_tiles.py ├── maze_run.py ├── moves.py ├── test_crate.py ├── test_files.py ├── test_mock.py └── util.py ├── 10_test_suite ├── README.md ├── maze_run │ ├── __init__.py │ ├── __main__.py │ ├── draw_maze.py │ ├── event_loop.py │ ├── generate_maze.py │ ├── load_tiles.py │ ├── moves.py │ └── util.py └── test │ ├── expected.txt │ ├── fixtures.py │ ├── test_file_output.py │ ├── test_graphics.py │ └── test_moves.py ├── 11_testing_best_practices └── generate_maze_faster.py ├── 12_version_control ├── example.gitignore └── example_git_log.txt ├── 13_project_scaffold └── setup.py ├── 14_cleaning_up ├── code_golf.py ├── load_tile_positions.py ├── tiles.txt └── unorganized.py ├── 15_decomposing_tasks ├── event_loop.py └── mediator.py ├── 16_static_typing ├── highscores.py ├── highscores.sqlite ├── highscores_assert.py ├── highscores_cython.pyx ├── highscores_postgres.py ├── highscores_pycharm.py ├── highscores_sqlite3.py └── scores.csv ├── 17_documentation ├── Makefile ├── conf.py ├── event_loop.rst ├── iftex.sty ├── index.rst ├── mazes.rst └── tile_grid.rst ├── 9781484222409.jpg ├── LICENSE.txt ├── README.md └── contributing.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | pygame/* 3 | *.log 4 | 5 | password.txt 6 | .cache 7 | chapters/17_documentation/_build/ 8 | .idea 9 | _pygame 10 | */__pycache__/* 11 | -------------------------------------------------------------------------------- /01_introduction/maze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/01_introduction/maze.png -------------------------------------------------------------------------------- /01_introduction/merge_two_images.py: -------------------------------------------------------------------------------- 1 | 2 | # merge two images into one using Pygame 3 | 4 | from pygame import image, Rect 5 | 6 | maze = image.load('maze.png') 7 | player = image.load('player.png') 8 | 9 | maze.blit(player, Rect((32, 32, 64, 64)), Rect((0, 0, 32, 32))) 10 | image.save(maze, 'merged.png') 11 | -------------------------------------------------------------------------------- /01_introduction/merged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/01_introduction/merged.png -------------------------------------------------------------------------------- /01_introduction/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/01_introduction/player.png -------------------------------------------------------------------------------- /02_exceptions/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 02 - Exceptions in Python 3 | 4 | from pygame import image, Rect, Surface 5 | 6 | TILE_POSITIONS = [ 7 | ('#', 0, 0), # wall 8 | (' ', 0, 1), # floor 9 | ('x', 1, 1), # exit 10 | ('.', 2, 0), # dot 11 | ('*', 3, 0), # player 12 | ] 13 | SIZE = 32 14 | 15 | 16 | def get_tile_rect(x, y): 17 | """Converts tile indices to a pygame.Rect""" 18 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 19 | 20 | 21 | def load_tiles(): 22 | """ 23 | Load tiles from an image file into a dictionary. 24 | Returns a tuple of (image, tile_dict) 25 | """ 26 | tile_image = image.load('../images/tiles.xpm') 27 | tiles = {} 28 | for symbol, x, y in TILE_POSITIONS: 29 | tiles[symbol] = get_tile_rect(x, y) 30 | return tile_image, tiles 31 | 32 | 33 | if __name__ == '__main__': 34 | tile_img, tiles = load_tiles() 35 | m = Surface((96, 32)) 36 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 37 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 38 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 39 | image.save(m, 'tile_combo.png') 40 | -------------------------------------------------------------------------------- /02_exceptions/load_tiles_buggy.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 02 - Exceptions in Python 3 | 4 | # WITH BUGS! 5 | # This code contains at least 9 defects. 6 | # Try to fix them all based on the error messages. 7 | 8 | imprt pygame 9 | from pygame import imagge, Rect 10 | 11 | 12 | TILE_POSITIONS = [ 13 | ('#', 0, 0), # wall 14 | (' ', 0, 1) # floor 15 | ('.', 2, 0), # dot 16 | 17 | SIZE = 32 18 | 19 | image = 'tiles.xpm' 20 | 21 | 22 | def load_tiles() 23 | """ 24 | Load tiles from an image file into a dictionary. 25 | Returns a tuple of (image, tile_dict) 26 | """ 27 | tiles = {} 28 | tile_img = image.loaad('tiless.xpm') 29 | for x, y in TILEPOSITIONS: 30 | rect = Rect(x*SIZE, y*SIZE, SIZE, SIZE) 31 | tiles[symbol] = rect 32 | return tile_img, tiles 33 | 34 | 35 | if __name__ == '__main__': 36 | tile_img, tiles = load_tiles() 37 | m = Surface((96, 32)) 38 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 39 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 40 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 41 | image.save(m, 'tile_combo.png') 42 | 43 | 44 | # ---------------------------- 45 | 46 | # Optional exercise: 47 | # make the print statement below work 48 | # by modifying the class 49 | # so that it prints the char attribute 50 | 51 | 52 | class Tile: 53 | 54 | def __init__(self, achar, x, y): 55 | char = achar 56 | 57 | t = Tile('#', 0, 0) 58 | print(t.char) 59 | -------------------------------------------------------------------------------- /02_exceptions/tile_combo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/02_exceptions/tile_combo.png -------------------------------------------------------------------------------- /03_semantic_errors/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 03 - Semantic Errors 3 | 4 | import random 5 | 6 | XMAX, YMAX = 19, 16 7 | 8 | 9 | def create_grid_string(dots, xsize, ysize): 10 | """ 11 | Creates a grid of size (xx, yy) 12 | with the given positions of dots. 13 | """ 14 | grid = "" 15 | for y in range(ysize): 16 | for x in range(xsize): 17 | grid += "." if (x, y) in dots else "#" 18 | grid += "\n" 19 | return grid 20 | 21 | 22 | def get_all_dot_positions(xsize, ysize): 23 | """Returns a list of (x, y) tuples covering all positions in a grid""" 24 | return [(x, y) for x in range(1, xsize - 1) for y in range(1, ysize - 1)] 25 | 26 | 27 | def get_neighbors(x, y): 28 | """Returns a list with the 8 neighbor positions of (x, y)""" 29 | return [ 30 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 31 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 32 | ] 33 | 34 | 35 | def generate_dot_positions(xsize, ysize): 36 | """Creates positions of dots for a random maze""" 37 | positions = get_all_dot_positions(xsize, ysize) 38 | dots = set() 39 | while positions != []: 40 | x, y = random.choice(positions) 41 | neighbors = get_neighbors(x, y) 42 | free = [nb in dots for nb in neighbors] 43 | if free.count(True) < 5: 44 | dots.add((x, y)) 45 | positions.remove((x, y)) 46 | return dots 47 | 48 | 49 | def create_maze(xsize, ysize): 50 | """Returns a xsize*ysize maze as a string""" 51 | dots = generate_dot_positions(xsize, ysize) 52 | maze = create_grid_string(dots, xsize, ysize) 53 | return maze 54 | 55 | 56 | if __name__ == '__main__': 57 | dots = set(((1, 1), (1, 2), (1, 3), (2, 2), (3, 1), (3, 2), (3, 3))) 58 | print(create_grid_string(dots, 5, 5)) 59 | 60 | positions = get_all_dot_positions(5, 5) 61 | print(create_grid_string(positions, 5, 5)) 62 | 63 | neighbors = get_neighbors(3, 2) 64 | print(create_grid_string(neighbors, 5, 5)) 65 | 66 | maze = create_maze(12, 7) 67 | print(maze) 68 | -------------------------------------------------------------------------------- /03_semantic_errors/generate_maze_buggy.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 03 - Semantic Errors 3 | 4 | # WITH BUGS! 5 | # This code contains many defects. 6 | # Try to fix them all based on the program output. 7 | 8 | # HINT: 9 | # There are many defects in the code with overlapping defects. 10 | # Try to test the functions one by one. 11 | 12 | import random 13 | 14 | XMAX, YMAX = 12, 7 15 | 16 | 17 | def create_grid_string(dots, xx, yy): 18 | """ 19 | Creates a grid of size (xx, yy) 20 | with the given positions of dots. 21 | """ 22 | for y in range(yy): 23 | grid = "" 24 | for x in range(xx): 25 | grid = "#" if (xx, yy) in dots else "." # 3 defects in this line! 26 | grid == "\n" 27 | return grid 28 | 29 | 30 | def get_all_dot_positions(xsize, ysize): 31 | """Returns a list of (x, y) tuples covering all positions in a grid""" 32 | return [(x, y) for x in range(0, xsize) for y in range(1, ysize - 1)] 33 | 34 | 35 | def get_neighbors(x, y): 36 | """Returns a list with the 8 neighbor positions of (x, y)""" 37 | return [ 38 | (x, - 1), (y, x + 1), (x - (1), y), (x + 1), y, 39 | (x, (-1, y)), (x + 1, y, 1), (x - 1, y + 1, x + 1, y + 1) 40 | ] 41 | 42 | 43 | def generate_dot_positions(xsize, ysize): 44 | """Creates positions of dots for a random maze""" 45 | positions = get_all_dot_positions(xsize, ysize) 46 | dots = set() 47 | while positions != []: 48 | x, y = random.choice(positions) 49 | neighbors = get_neighbors(x, y) 50 | free = [nb in dots for nb in neighbors] 51 | if free.count(True) > 5: 52 | dots.add((x, y)) 53 | positions.remove((x, y)) 54 | return dots 55 | 56 | 57 | def create_maze(xsize, ysize): 58 | """Returns a xsize*ysize maze as a string""" 59 | dots = generate_dot_positions(xsize, ysize) 60 | maze = create_grid_string(dots, xsize, ysize) 61 | 62 | 63 | if __name__ == '__main__': 64 | dots = set(((1, 1), (1, 2), (1, 3), (2, 2), (3, 1), (3, 2), (3, 3))) 65 | print(create_grid_string(dots, 5, 5)) 66 | 67 | positions = get_all_dot_positions(5, 5) 68 | print(create_grid_string(positions, 5, 5)) 69 | 70 | neighbors = get_neighbors(3, 2) 71 | print(create_grid_string(neighbors, 5, 5)) 72 | 73 | maze = create_maze 74 | print(maze) 75 | -------------------------------------------------------------------------------- /04_scientific_method/event_loop.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 04 - Debugging with the Scientific Method 3 | 4 | # The program prints the codes of pressed keys. 5 | 6 | from pygame.locals import KEYDOWN 7 | import pygame 8 | 9 | 10 | def event_loop(handle_key, delay=10): 11 | """Processes events and updates callbacks.""" 12 | while True: 13 | pygame.event.pump() 14 | event = pygame.event.poll() 15 | if event.type == KEYDOWN: 16 | handle_key(event.key) 17 | pygame.time.delay(delay) 18 | 19 | 20 | if __name__ == '__main__': 21 | pygame.init() 22 | pygame.display.set_mode((640, 400)) 23 | event_loop(print) 24 | 25 | -------------------------------------------------------------------------------- /04_scientific_method/event_loop_buggy.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 04 - Debugging with the Scientific Method 3 | 4 | # WITH BUGS! 5 | # This code contains a single defect 6 | # that is hard to find by looking at the symptoms or the code. 7 | # 8 | # The program should print codes of pressed keys. 9 | 10 | from pygame.locals import KEYDOWN 11 | import pygame 12 | 13 | 14 | def event_loop(handle_key, delay=10): 15 | """Processes events and updates callbacks.""" 16 | while True: 17 | pygame.event.pump() 18 | event = pygame.event.poll() 19 | if event.type == KEYDOWN: 20 | handle_key(event.key) 21 | pygame.time.delay(delay) 22 | 23 | 24 | if __name__ == '__main__': 25 | pygame.init() 26 | event_loop(print) 27 | -------------------------------------------------------------------------------- /05_print/draw_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 05 - Debugging with print 3 | 4 | from pygame import image, Surface 5 | from load_tiles import load_tiles, get_tile_rect, SIZE 6 | from generate_maze import create_maze 7 | from util import debug_print 8 | 9 | 10 | def parse_grid(data): 11 | """Parses the string representation into a nested list""" 12 | return [list(row) for row in data.strip().split("\n")] 13 | 14 | 15 | def draw_grid(data, tile_img, tiles): 16 | """Returns an image of a tile-based grid""" 17 | debug_print("drawing level", data) 18 | xsize = len(data[0]) * SIZE 19 | ysize = len(data) * SIZE 20 | img = Surface((xsize, ysize)) 21 | for y, row in enumerate(data): 22 | for x, char in enumerate(row): 23 | rect = get_tile_rect(x, y) 24 | img.blit(tile_img, rect, tiles[char]) 25 | return img 26 | 27 | 28 | if __name__ == '__main__': 29 | tile_img, tiles = load_tiles() 30 | level = create_maze(12, 7) 31 | level = parse_grid(level) 32 | maze = draw_grid(level, tile_img, tiles) 33 | image.save(maze, 'maze.png') 34 | -------------------------------------------------------------------------------- /05_print/draw_maze_buggy.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 05 - Debugging with print 3 | 4 | # WITH BUGS! 5 | # This code contains two defects. 6 | 7 | from pygame import image, Surface 8 | from load_tiles import load_tiles, get_tile_rect, SIZE 9 | from generate_maze import create_maze 10 | 11 | 12 | def parse_grid(data): 13 | """Parses the string representation into a nested list""" 14 | return data.strip().split('\n') 15 | 16 | 17 | def draw_grid(data, tile_img, tiles): 18 | """Returns an image of a tile-based grid""" 19 | xs = len(data[0]) * SIZE 20 | ys = len(data) * SIZE 21 | img = Surface((xs, ys)) 22 | for y, row in enumerate(data): 23 | for x, char in enumerate(row): 24 | rect = get_tile_rect(xs, ys) 25 | img.blit(tile_img, tiles[char], rect) 26 | return img 27 | 28 | 29 | if __name__ == '__main__': 30 | tile_img, tiles = load_tiles() 31 | level = create_maze(12, 7) 32 | level = parse_grid(level) 33 | maze = draw_grid(level, tile_img, tiles) 34 | image.save(maze, 'maze.png') 35 | -------------------------------------------------------------------------------- /05_print/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 03 - Semantic Errors 3 | 4 | import random 5 | 6 | XMAX, YMAX = 19, 16 7 | 8 | 9 | def create_grid_string(dots, xsize, ysize): 10 | """ 11 | Creates a grid of size (xx, yy) 12 | with the given positions of dots. 13 | """ 14 | grid = "" 15 | for y in range(ysize): 16 | for x in range(xsize): 17 | grid += "." if (x, y) in dots else "#" 18 | grid += "\n" 19 | return grid 20 | 21 | 22 | def get_all_dot_positions(xsize, ysize): 23 | """Returns a list of (x, y) tuples covering all positions in a grid""" 24 | return [(x, y) for x in range(1, xsize - 1) for y in range(1, ysize - 1)] 25 | 26 | 27 | def get_neighbors(x, y): 28 | """Returns a list with the 8 neighbor positions of (x, y)""" 29 | return [ 30 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 31 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 32 | ] 33 | 34 | 35 | def generate_dot_positions(xsize, ysize): 36 | """Creates positions of dots for a random maze""" 37 | positions = get_all_dot_positions(xsize, ysize) 38 | dots = set() 39 | while positions != []: 40 | x, y = random.choice(positions) 41 | neighbors = get_neighbors(x, y) 42 | free = [nb in dots for nb in neighbors] 43 | if free.count(True) < 5: 44 | dots.add((x, y)) 45 | positions.remove((x, y)) 46 | return dots 47 | 48 | 49 | def create_maze(xsize, ysize): 50 | """Returns a xsize*ysize maze as a string""" 51 | dots = generate_dot_positions(xsize, ysize) 52 | maze = create_grid_string(dots, xsize, ysize) 53 | return maze 54 | 55 | 56 | if __name__ == '__main__': 57 | dots = set(((1, 1), (1, 2), (1, 3), (2, 2), (3, 1), (3, 2), (3, 3))) 58 | print(create_grid_string(dots, 5, 5)) 59 | 60 | positions = get_all_dot_positions(5, 5) 61 | print(create_grid_string(positions, 5, 5)) 62 | 63 | neighbors = get_neighbors(3, 2) 64 | print(create_grid_string(neighbors, 5, 5)) 65 | 66 | maze = create_maze(12, 7) 67 | print(maze) 68 | -------------------------------------------------------------------------------- /05_print/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 02 - Exceptions in Python 3 | 4 | from pygame import image, Rect, Surface 5 | 6 | TILE_POSITIONS = [ 7 | ('#', 0, 0), # wall 8 | (' ', 0, 1), # floor 9 | ('x', 1, 1), # exit 10 | ('.', 2, 0), # dot 11 | ('*', 3, 0), # player 12 | ] 13 | SIZE = 32 14 | 15 | 16 | def get_tile_rect(x, y): 17 | """Converts tile indices to a pygame.Rect""" 18 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 19 | 20 | 21 | def load_tiles(): 22 | """ 23 | Load tiles from an image file into a dictionary. 24 | Returns a tuple of (image, tile_dict) 25 | """ 26 | tile_image = image.load('../images/tiles.xpm') 27 | tiles = {} 28 | for symbol, x, y in TILE_POSITIONS: 29 | tiles[symbol] = get_tile_rect(x, y) 30 | return tile_image, tiles 31 | 32 | 33 | if __name__ == '__main__': 34 | tile_img, tiles = load_tiles() 35 | m = Surface((96, 32)) 36 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 37 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 38 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 39 | image.save(m, 'tile_combo.png') 40 | -------------------------------------------------------------------------------- /05_print/maze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/05_print/maze.png -------------------------------------------------------------------------------- /05_print/util.py: -------------------------------------------------------------------------------- 1 | 2 | from pprint import pprint 3 | import sys 4 | 5 | DEBUG = '-d' in sys.argv 6 | 7 | 8 | def debug_print(*args, **kwargs): 9 | condition = kwargs.get('condition', True) 10 | if DEBUG and condition: 11 | for a in args: 12 | pprint(a) 13 | -------------------------------------------------------------------------------- /06_introspection/draw_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 05 - Debugging with print 3 | 4 | from pygame import image, Surface 5 | from load_tiles import load_tiles, get_tile_rect, SIZE 6 | from generate_maze import create_maze 7 | from util import debug_print 8 | 9 | 10 | def parse_grid(data): 11 | """Parses the string representation into a nested list""" 12 | return [list(row) for row in data.strip().split("\n")] 13 | 14 | 15 | def draw_grid(data, tile_img, tiles): 16 | """Returns an image of a tile-based grid""" 17 | debug_print("drawing level", data) 18 | xsize = len(data[0]) * SIZE 19 | ysize = len(data) * SIZE 20 | img = Surface((xsize, ysize)) 21 | for y, row in enumerate(data): 22 | for x, char in enumerate(row): 23 | rect = get_tile_rect(x, y) 24 | img.blit(tile_img, rect, tiles[char]) 25 | return img 26 | 27 | 28 | if __name__ == '__main__': 29 | tile_img, tiles = load_tiles() 30 | level = create_maze(12, 7) 31 | level = parse_grid(level) 32 | maze = draw_grid(level, tile_img, tiles) 33 | image.save(maze, 'maze.png') 34 | -------------------------------------------------------------------------------- /06_introspection/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 03 - Semantic Errors 3 | 4 | import random 5 | 6 | XMAX, YMAX = 19, 16 7 | 8 | 9 | def create_grid_string(dots, xsize, ysize): 10 | """ 11 | Creates a grid of size (xx, yy) 12 | with the given positions of dots. 13 | """ 14 | grid = "" 15 | for y in range(ysize): 16 | for x in range(xsize): 17 | grid += "." if (x, y) in dots else "#" 18 | grid += "\n" 19 | return grid 20 | 21 | 22 | def get_all_dot_positions(xsize, ysize): 23 | """Returns a list of (x, y) tuples covering all positions in a grid""" 24 | return [(x, y) for x in range(1, xsize - 1) for y in range(1, ysize - 1)] 25 | 26 | 27 | def get_neighbors(x, y): 28 | """Returns a list with the 8 neighbor positions of (x, y)""" 29 | return [ 30 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 31 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 32 | ] 33 | 34 | 35 | def generate_dot_positions(xsize, ysize): 36 | """Creates positions of dots for a random maze""" 37 | positions = get_all_dot_positions(xsize, ysize) 38 | dots = set() 39 | while positions != []: 40 | x, y = random.choice(positions) 41 | neighbors = get_neighbors(x, y) 42 | free = [nb in dots for nb in neighbors] 43 | if free.count(True) < 5: 44 | dots.add((x, y)) 45 | positions.remove((x, y)) 46 | return dots 47 | 48 | 49 | def create_maze(xsize, ysize): 50 | """Returns a xsize*ysize maze as a string""" 51 | dots = generate_dot_positions(xsize, ysize) 52 | maze = create_grid_string(dots, xsize, ysize) 53 | return maze 54 | 55 | 56 | if __name__ == '__main__': 57 | dots = set(((1, 1), (1, 2), (1, 3), (2, 2), (3, 1), (3, 2), (3, 3))) 58 | print(create_grid_string(dots, 5, 5)) 59 | 60 | positions = get_all_dot_positions(5, 5) 61 | print(create_grid_string(positions, 5, 5)) 62 | 63 | neighbors = get_neighbors(3, 2) 64 | print(create_grid_string(neighbors, 5, 5)) 65 | 66 | maze = create_maze(12, 7) 67 | print(maze) 68 | -------------------------------------------------------------------------------- /06_introspection/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 02 - Exceptions in Python 3 | 4 | from pygame import image, Rect, Surface 5 | 6 | TILE_POSITIONS = [ 7 | ('#', 0, 0), # wall 8 | (' ', 0, 1), # floor 9 | ('x', 1, 1), # exit 10 | ('.', 2, 0), # dot 11 | ('*', 3, 0), # player 12 | ] 13 | SIZE = 32 14 | 15 | 16 | def get_tile_rect(x, y): 17 | """Converts tile indices to a pygame.Rect""" 18 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 19 | 20 | 21 | def load_tiles(): 22 | """ 23 | Load tiles from an image file into a dictionary. 24 | Returns a tuple of (image, tile_dict) 25 | """ 26 | tile_image = image.load('../images/tiles.xpm') 27 | tiles = {} 28 | for symbol, x, y in TILE_POSITIONS: 29 | tiles[symbol] = get_tile_rect(x, y) 30 | return tile_image, tiles 31 | 32 | 33 | if __name__ == '__main__': 34 | tile_img, tiles = load_tiles() 35 | m = Surface((96, 32)) 36 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 37 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 38 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 39 | image.save(m, 'tile_combo.png') 40 | -------------------------------------------------------------------------------- /06_introspection/moved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/06_introspection/moved.png -------------------------------------------------------------------------------- /06_introspection/moves.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 06 - Introspection 3 | 4 | from load_tiles import load_tiles 5 | from generate_maze import create_maze 6 | from draw_maze import draw_grid, parse_grid 7 | from pygame import image 8 | import random 9 | import sys 10 | 11 | 12 | LEFT = (-1, 0) 13 | RIGHT = (1, 0) 14 | UP = (0, -1) 15 | DOWN = (0, 1) 16 | 17 | 18 | def get_player_pos(level, player_char='*'): 19 | """Returns a (x, y) tuple of player char on the level""" 20 | for y, row in enumerate(level): 21 | for x, char in enumerate(row): 22 | if char == player_char: 23 | return x, y 24 | 25 | 26 | def move(level, direction): 27 | """Handles moves on the level""" 28 | oldx, oldy = get_player_pos(level) 29 | newx = oldx + direction[0] 30 | newy = oldy + direction[1] 31 | if level[newy][newx] == 'x': 32 | sys.exit(0) 33 | if level[newy][newx] != '#': 34 | level[oldy][oldx] = ' ' 35 | level[newy][newx] = '*' 36 | 37 | 38 | if __name__ == '__main__': 39 | tile_img, tiles = load_tiles() 40 | maze = create_maze(12, 7) 41 | maze = parse_grid(maze) 42 | maze[1][1] = '*' 43 | for i in range(100): 44 | direction = random.choice([LEFT, RIGHT, UP, DOWN]) 45 | move(maze, direction) 46 | img = draw_grid(maze, tile_img, tiles) 47 | image.save(img, 'moved.png') 48 | -------------------------------------------------------------------------------- /06_introspection/util.py: -------------------------------------------------------------------------------- 1 | 2 | from pprint import pprint 3 | import sys 4 | 5 | DEBUG = '-d' in sys.argv 6 | 7 | 8 | def debug_print(*args, **kwargs): 9 | condition = kwargs.get('condition', True) 10 | if DEBUG and condition: 11 | for a in args: 12 | pprint(a) 13 | -------------------------------------------------------------------------------- /07_interactive_debugger/.pdbrc: -------------------------------------------------------------------------------- 1 | print(dir()) 2 | import pprint 3 | ll 4 | -------------------------------------------------------------------------------- /07_interactive_debugger/draw_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 05 - Debugging with print 3 | 4 | from pygame import image, Surface 5 | from load_tiles import load_tiles, get_tile_rect, SIZE 6 | from generate_maze import create_maze 7 | from util import debug_print 8 | 9 | 10 | def parse_grid(data): 11 | """Parses the string representation into a nested list""" 12 | return [list(row) for row in data.strip().split("\n")] 13 | 14 | 15 | def draw_grid(data, tile_img, tiles): 16 | """Returns an image of a tile-based grid""" 17 | debug_print("drawing level", data) 18 | xsize = len(data[0]) * SIZE 19 | ysize = len(data) * SIZE 20 | img = Surface((xsize, ysize)) 21 | for y, row in enumerate(data): 22 | for x, char in enumerate(row): 23 | rect = get_tile_rect(x, y) 24 | img.blit(tile_img, rect, tiles[char]) 25 | return img 26 | 27 | 28 | if __name__ == '__main__': 29 | tile_img, tiles = load_tiles() 30 | level = create_maze(12, 7) 31 | level = parse_grid(level) 32 | maze = draw_grid(level, tile_img, tiles) 33 | image.save(maze, 'maze.png') 34 | -------------------------------------------------------------------------------- /07_interactive_debugger/event_loop.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 04 - Debugging with the Scientific Method 3 | 4 | # The program prints the codes of pressed keys. 5 | 6 | from pygame.locals import KEYDOWN 7 | import pygame 8 | 9 | 10 | def event_loop(handle_key, delay=10): 11 | """Processes events and updates callbacks.""" 12 | while True: 13 | pygame.event.pump() 14 | event = pygame.event.poll() 15 | if event.type == KEYDOWN: 16 | handle_key(event.key) 17 | pygame.time.delay(delay) 18 | 19 | 20 | if __name__ == '__main__': 21 | pygame.init() 22 | pygame.display.set_mode((640, 400)) 23 | event_loop(print) 24 | 25 | -------------------------------------------------------------------------------- /07_interactive_debugger/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 03 - Semantic Errors 3 | 4 | import random 5 | 6 | XMAX, YMAX = 19, 16 7 | 8 | 9 | def create_grid_string(dots, xsize, ysize): 10 | """ 11 | Creates a grid of size (xx, yy) 12 | with the given positions of dots. 13 | """ 14 | grid = "" 15 | for y in range(ysize): 16 | for x in range(xsize): 17 | grid += "." if (x, y) in dots else "#" 18 | grid += "\n" 19 | return grid 20 | 21 | 22 | def get_all_dot_positions(xsize, ysize): 23 | """Returns a list of (x, y) tuples covering all positions in a grid""" 24 | return [(x, y) for x in range(1, xsize - 1) for y in range(1, ysize - 1)] 25 | 26 | 27 | def get_neighbors(x, y): 28 | """Returns a list with the 8 neighbor positions of (x, y)""" 29 | return [ 30 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 31 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 32 | ] 33 | 34 | 35 | def generate_dot_positions(xsize, ysize): 36 | """Creates positions of dots for a random maze""" 37 | positions = get_all_dot_positions(xsize, ysize) 38 | dots = set() 39 | while positions != []: 40 | x, y = random.choice(positions) 41 | neighbors = get_neighbors(x, y) 42 | free = [nb in dots for nb in neighbors] 43 | if free.count(True) < 5: 44 | dots.add((x, y)) 45 | positions.remove((x, y)) 46 | return dots 47 | 48 | 49 | def create_maze(xsize, ysize): 50 | """Returns a xsize*ysize maze as a string""" 51 | dots = generate_dot_positions(xsize, ysize) 52 | maze = create_grid_string(dots, xsize, ysize) 53 | return maze 54 | 55 | 56 | if __name__ == '__main__': 57 | dots = set(((1, 1), (1, 2), (1, 3), (2, 2), (3, 1), (3, 2), (3, 3))) 58 | print(create_grid_string(dots, 5, 5)) 59 | 60 | positions = get_all_dot_positions(5, 5) 61 | print(create_grid_string(positions, 5, 5)) 62 | 63 | neighbors = get_neighbors(3, 2) 64 | print(create_grid_string(neighbors, 5, 5)) 65 | 66 | maze = create_maze(12, 7) 67 | print(maze) 68 | -------------------------------------------------------------------------------- /07_interactive_debugger/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 02 - Exceptions in Python 3 | 4 | from pygame import image, Rect, Surface 5 | 6 | TILE_POSITIONS = [ 7 | ('#', 0, 0), # wall 8 | (' ', 0, 1), # floor 9 | ('x', 1, 1), # exit 10 | ('.', 2, 0), # dot 11 | ('*', 3, 0), # player 12 | ] 13 | SIZE = 32 14 | 15 | 16 | def get_tile_rect(x, y): 17 | """Converts tile indices to a pygame.Rect""" 18 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 19 | 20 | 21 | def load_tiles(): 22 | """ 23 | Load tiles from an image file into a dictionary. 24 | Returns a tuple of (image, tile_dict) 25 | """ 26 | tile_image = image.load('../images/tiles.xpm') 27 | tiles = {} 28 | for symbol, x, y in TILE_POSITIONS: 29 | tiles[symbol] = get_tile_rect(x, y) 30 | return tile_image, tiles 31 | 32 | 33 | if __name__ == '__main__': 34 | tile_img, tiles = load_tiles() 35 | m = Surface((96, 32)) 36 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 37 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 38 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 39 | image.save(m, 'tile_combo.png') 40 | -------------------------------------------------------------------------------- /07_interactive_debugger/maze_run.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 07 - Interactive Debugger 3 | 4 | from load_tiles import load_tiles 5 | from generate_maze import create_maze 6 | from event_loop import event_loop 7 | from draw_maze import draw_grid, parse_grid 8 | from moves import move, LEFT, RIGHT, UP, DOWN 9 | from pygame import Rect 10 | import pygame 11 | 12 | 13 | DIRECTIONS = { 14 | 276: LEFT, 275: RIGHT, 15 | 273: UP, 274: DOWN 16 | } 17 | 18 | 19 | def draw(): 20 | """Displays the maze on the screen""" 21 | img = draw_grid(maze, tile_img, tiles) 22 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 23 | pygame.display.update() 24 | 25 | 26 | def handle_key(key): 27 | """Handles key events in the game""" 28 | direction = DIRECTIONS.get(key) 29 | if direction: 30 | move(maze, direction) 31 | draw() 32 | 33 | 34 | if __name__ == '__main__': 35 | # initialize display 36 | pygame.init() 37 | pygame.display.set_mode((800, 600)) 38 | display = pygame.display.get_surface() 39 | tile_img, tiles = load_tiles() 40 | 41 | # prepare the maze 42 | maze = parse_grid(create_maze(12, 7)) 43 | maze[1][1] = '*' 44 | maze[5][10] = 'x' 45 | 46 | # start the game 47 | draw() 48 | event_loop(handle_key) 49 | -------------------------------------------------------------------------------- /07_interactive_debugger/maze_run_buggy.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 07 - Interactive Debugger 3 | 4 | # there is defect when integrating this module with the others 5 | 6 | from load_tiles import load_tiles 7 | from generate_maze import create_maze 8 | from event_loop import event_loop 9 | from draw_maze import draw_grid, parse_grid 10 | from moves import move, LEFT, RIGHT, UP, DOWN 11 | from pygame import Rect 12 | import pygame 13 | 14 | 15 | DIRECTIONS = { 16 | 276: LEFT, 275: RIGHT, 17 | 273: UP, 274: DOWN 18 | } 19 | 20 | 21 | def draw(): 22 | """Displays the maze on the screen""" 23 | img = draw_grid(maze, tile_img, tiles) 24 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 25 | pygame.display.update() 26 | 27 | 28 | def handle_key(key): 29 | """Handles key events in the game""" 30 | move(maze, DIRECTIONS.get(key)) 31 | draw() 32 | 33 | 34 | if __name__ == '__main__': 35 | # initialize display 36 | pygame.init() 37 | pygame.display.set_mode((800, 600)) 38 | display = pygame.display.get_surface() 39 | tile_img, tiles = load_tiles() 40 | 41 | # prepare the maze 42 | maze = parse_grid(create_maze(12, 7)) 43 | maze[1][1] = '*' 44 | maze[5][10] = 'x' 45 | 46 | # start the game 47 | draw() 48 | event_loop(handle_key) 49 | -------------------------------------------------------------------------------- /07_interactive_debugger/moves.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 06 - Introspection 3 | 4 | from load_tiles import load_tiles 5 | from generate_maze import create_maze 6 | from draw_maze import draw_grid, parse_grid 7 | from pygame import image 8 | import random 9 | import sys 10 | 11 | 12 | LEFT = (-1, 0) 13 | RIGHT = (1, 0) 14 | UP = (0, -1) 15 | DOWN = (0, 1) 16 | 17 | 18 | def get_player_pos(level, player_char='*'): 19 | """Returns a (x, y) tuple of player char on the level""" 20 | for y, row in enumerate(level): 21 | for x, char in enumerate(row): 22 | if char == player_char: 23 | return x, y 24 | 25 | 26 | def move(level, direction): 27 | """Handles moves on the level""" 28 | oldx, oldy = get_player_pos(level) 29 | newx = oldx + direction[0] 30 | newy = oldy + direction[1] 31 | if level[newy][newx] == 'x': 32 | sys.exit(0) 33 | if level[newy][newx] != '#': 34 | level[oldy][oldx] = ' ' 35 | level[newy][newx] = '*' 36 | 37 | 38 | if __name__ == '__main__': 39 | tile_img, tiles = load_tiles() 40 | maze = create_maze(12, 7) 41 | maze = parse_grid(maze) 42 | maze[1][1] = '*' 43 | for i in range(100): 44 | direction = random.choice([LEFT, RIGHT, UP, DOWN]) 45 | move(maze, direction) 46 | img = draw_grid(maze, tile_img, tiles) 47 | image.save(img, 'moved.png') 48 | -------------------------------------------------------------------------------- /07_interactive_debugger/util.py: -------------------------------------------------------------------------------- 1 | 2 | from pprint import pprint 3 | import sys 4 | 5 | DEBUG = '-d' in sys.argv 6 | 7 | 8 | def debug_print(*args, **kwargs): 9 | condition = kwargs.get('condition', True) 10 | if DEBUG and condition: 11 | for a in args: 12 | pprint(a) 13 | -------------------------------------------------------------------------------- /08_unit_tests/draw_maze.py: -------------------------------------------------------------------------------- 1 | 2 | from pygame import image, Surface 3 | from load_tiles import load_tiles, get_tile_rect 4 | from load_tiles import SIZE 5 | from generate_maze import create_maze 6 | from util import debug_print 7 | 8 | 9 | def parse_grid(data): 10 | """Parses the string representation into a nested list""" 11 | return [list(row) for row in data.strip().split("\n")] 12 | 13 | 14 | def draw_grid(data, tile_img, tiles): 15 | """Returns an image of a tile-based grid""" 16 | debug_print("drawing level", data) 17 | xsize = len(data[0]) * SIZE 18 | ysize = len(data) * SIZE 19 | img = Surface((xsize, ysize)) 20 | for y, row in enumerate(data): 21 | for x, char in enumerate(row): 22 | rect = get_tile_rect(x, y) 23 | img.blit(tile_img, rect, tiles[char]) 24 | return img 25 | 26 | 27 | if __name__ == '__main__': 28 | tile_img, tiles = load_tiles() 29 | level = create_maze(12, 7) 30 | level = parse_grid(level) 31 | maze = draw_grid(level, tile_img, tiles) 32 | image.save(maze, 'maze.png') 33 | -------------------------------------------------------------------------------- /08_unit_tests/event_loop.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 04 - Debugging with the Scientific Method 3 | 4 | # The program prints the codes of pressed keys. 5 | 6 | from pygame.locals import KEYDOWN 7 | import pygame 8 | 9 | 10 | def event_loop(handle_key, delay=10): 11 | """Processes events and updates callbacks.""" 12 | while True: 13 | pygame.event.pump() 14 | event = pygame.event.poll() 15 | if event.type == KEYDOWN: 16 | handle_key(event.key) 17 | pygame.time.delay(delay) 18 | 19 | 20 | if __name__ == '__main__': 21 | pygame.init() 22 | pygame.display.set_mode((640, 400)) 23 | event_loop(print) 24 | -------------------------------------------------------------------------------- /08_unit_tests/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | XMAX, YMAX = 19, 16 5 | 6 | 7 | def create_grid_string(dots, xsize, ysize): 8 | """ 9 | Creates a grid of size (xx, yy) 10 | with the given positions of dots. 11 | """ 12 | grid = "" 13 | for y in range(ysize): 14 | for x in range(xsize): 15 | grid += "." if (x, y) in dots else "#" 16 | grid += "\n" 17 | return grid 18 | 19 | 20 | def get_all_dot_positions(xsize, ysize): 21 | """Returns a list of (x, y) tuples covering all positions in a grid""" 22 | return [(x,y) for x in range(1, xsize-1) for y in range(1, ysize-1)] 23 | 24 | 25 | def get_neighbors(x, y): 26 | """Returns a list with the 8 neighbor positions of (x, y)""" 27 | return [ 28 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 29 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 30 | ] 31 | 32 | 33 | def generate_dot_positions(xsize, ysize): 34 | """Creates positions of dots for a random maze""" 35 | positions = get_all_dot_positions(xsize, ysize) 36 | dots = set() 37 | while positions != []: 38 | x, y = random.choice(positions) 39 | neighbors = get_neighbors(x, y) 40 | free = [nb in dots for nb in neighbors] 41 | if free.count(True) < 5: 42 | dots.add((x, y)) 43 | positions.remove((x, y)) 44 | return dots 45 | 46 | 47 | def create_maze(xsize, ysize): 48 | """Returns a xsize*ysize maze as a string""" 49 | dots = generate_dot_positions(xsize, ysize) 50 | maze = create_grid_string(dots, xsize, ysize) 51 | return maze 52 | 53 | 54 | if __name__ == '__main__': 55 | dots = set(((1,1), (1,2), (1,3), (2,2), (3,1), (3,2), (3,3))) 56 | print(create_grid_string(dots, 5, 5)) 57 | 58 | positions = get_all_dot_positions(5, 5) 59 | print(create_grid_string(positions, 5, 5)) 60 | 61 | neighbors = get_neighbors(3, 2) 62 | print(create_grid_string(neighbors, 5, 5)) 63 | 64 | maze = create_maze(12, 7) 65 | print(maze) 66 | -------------------------------------------------------------------------------- /08_unit_tests/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | from pygame import image, Rect, Surface 3 | 4 | TILE_POSITIONS = [ 5 | ('#', 0, 0), # wall 6 | ('o', 1, 0), # crate 7 | (' ', 0, 1), # floor 8 | ('x', 1, 1), # exit 9 | ('.', 2, 0), # dot 10 | ('*', 3, 0), # player 11 | ] 12 | SIZE = 32 13 | 14 | 15 | def get_tile_rect(x, y): 16 | """Converts tile indices to a pygame.Rect""" 17 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 18 | 19 | 20 | def load_tiles(): 21 | """Load tiles and returns a tuple of (image, tile_dict) """ 22 | tile_image = image.load('../images/tiles.xpm') 23 | tiles = {} 24 | for symbol, x, y in TILE_POSITIONS: 25 | tiles[symbol] = get_tile_rect(x, y) 26 | return tile_image, tiles 27 | 28 | 29 | if __name__ == '__main__': 30 | tile_img, tiles = load_tiles() 31 | m = Surface((96, 32)) 32 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 33 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 34 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 35 | image.save(m, 'tile_combo.png') 36 | -------------------------------------------------------------------------------- /08_unit_tests/maze_run.py: -------------------------------------------------------------------------------- 1 | from load_tiles import load_tiles 2 | from generate_maze import create_maze 3 | from event_loop import event_loop 4 | from draw_maze import draw_grid, parse_grid 5 | from moves import move, LEFT, RIGHT, UP, DOWN 6 | from pygame import Rect 7 | import pygame 8 | 9 | 10 | # initialize display 11 | pygame.init() 12 | pygame.display.set_mode((800, 600)) 13 | display = pygame.display.get_surface() 14 | 15 | # prepare the maze 16 | maze = parse_grid(create_maze(12, 7)) 17 | maze[1][1] = '*' 18 | maze[5][10] = 'x' 19 | 20 | # draw the graphics 21 | tile_img, tiles = load_tiles() 22 | img = draw_grid(maze, tile_img, tiles) 23 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 24 | pygame.display.update() 25 | 26 | # start the game 27 | DIRECTIONS = { 28 | 276: LEFT, 29 | 275: RIGHT, 30 | 273: UP, 31 | 274: DOWN 32 | } 33 | 34 | 35 | def handle_key(key): 36 | """Handles key events in the game""" 37 | direction = DIRECTIONS.get(key) 38 | if direction: 39 | move(maze, direction) 40 | img = draw_grid(maze, tile_img, tiles) 41 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 42 | pygame.display.update() 43 | 44 | 45 | if __name__ == '__main__': 46 | event_loop(handle_key) 47 | -------------------------------------------------------------------------------- /08_unit_tests/moves.py: -------------------------------------------------------------------------------- 1 | from load_tiles import load_tiles 2 | from generate_maze import create_maze 3 | from draw_maze import draw_grid, parse_grid 4 | from pygame import image 5 | import random 6 | import sys 7 | 8 | 9 | LEFT = (-1, 0) 10 | RIGHT = (1, 0) 11 | UP = (0, -1) 12 | DOWN = (0, 1) 13 | 14 | 15 | def get_player_pos(level, player_char='*'): 16 | """Returns a (x, y) tuple of player char on the level""" 17 | for y, row in enumerate(level): 18 | for x, char in enumerate(row): 19 | if char == player_char: 20 | return x, y 21 | 22 | 23 | def move(level, direction): 24 | """Handles moves on the level""" 25 | oldx, oldy = get_player_pos(level) 26 | newx = oldx + direction[0] 27 | newy = oldy + direction[1] 28 | if level[newy][newx] == 'x': 29 | sys.exit(0) 30 | if level[newy][newx] == 'o': 31 | cratex = newx + direction[0] 32 | cratey = newy + direction[1] 33 | if level[cratey][cratex] in '. ': 34 | level[cratey][cratex] = 'o' 35 | level[newy][newx] = ' ' 36 | if level[newy][newx] in '. ': 37 | level[oldy][oldx] = ' ' 38 | level[newy][newx] = '*' 39 | 40 | 41 | if __name__ == '__main__': 42 | tile_img, tiles = load_tiles() 43 | maze = create_maze(12, 7) 44 | maze = parse_grid(maze) 45 | maze[1][1] = '*' 46 | for i in range(100): 47 | direction = random.choice([LEFT, RIGHT, UP, DOWN]) 48 | move(maze, direction) 49 | img = draw_grid(maze, tile_img, tiles) 50 | image.save(img, 'moved.png') 51 | -------------------------------------------------------------------------------- /08_unit_tests/test_crate.py: -------------------------------------------------------------------------------- 1 | 2 | from draw_maze import parse_grid 3 | from moves import move 4 | from moves import LEFT, RIGHT, UP, DOWN 5 | import pytest 6 | 7 | LEVEL = """####### 8 | #.....# 9 | #..o..# 10 | #.o*o.# 11 | #..o..# 12 | #.....# 13 | #######""" 14 | 15 | 16 | def move_crate(direction, plr_pos, crate_pos): 17 | """Helper function for testing crate moves""" 18 | maze = parse_grid(LEVEL) 19 | move(maze, direction) 20 | assert maze[plr_pos[0]][plr_pos[1]] == '*' 21 | assert maze[crate_pos[0]][crate_pos[1]] == 'o' 22 | 23 | 24 | def test_move_crate_left(): 25 | move_crate(LEFT, (3, 2), (3, 1)) 26 | 27 | 28 | def test_move_crate_right(): 29 | move_crate(RIGHT, (3, 4), (3, 5)) 30 | 31 | 32 | def test_move_crate_up(): 33 | move_crate(UP, (2, 3), (1, 3)) 34 | 35 | 36 | def test_move_crate_down(): 37 | move_crate(DOWN, (4, 3), (5, 3)) 38 | 39 | 40 | def test_assert_examples(): 41 | maze = parse_grid(LEVEL) 42 | assert len(maze) <= 7 # comparison operator 43 | assert 1 < len(maze) < 10 # range check 44 | assert maze[0][0] == '#' and maze[1][1] == '.' # logical operators 45 | assert maze[0].count('#') == 7 # using methods 46 | 47 | 48 | def test_push_crate_to_wall(): 49 | maze = parse_grid("*o#") 50 | move(maze, RIGHT) 51 | assert maze[0] == ['*', 'o', '#'] 52 | 53 | 54 | def test_push_crate_to_crate(): 55 | maze = parse_grid("*oo") 56 | move(maze, RIGHT) 57 | assert maze == [['*', 'o', 'o']] 58 | 59 | 60 | def test_move_to_none(): 61 | """direction=None generates an Exception""" 62 | maze = parse_grid(LEVEL) 63 | with pytest.raises(TypeError): 64 | move(maze, None) 65 | -------------------------------------------------------------------------------- /08_unit_tests/util.py: -------------------------------------------------------------------------------- 1 | 2 | from pprint import pprint 3 | import sys 4 | 5 | DEBUG = '-d' in sys.argv 6 | 7 | def debug_print(*args, **kwargs): 8 | condition = kwargs.get('condition', True) 9 | if DEBUG and condition: 10 | for a in args: 11 | pprint(a) 12 | -------------------------------------------------------------------------------- /09_test_data/draw_maze.py: -------------------------------------------------------------------------------- 1 | 2 | from pygame import image, Surface 3 | from load_tiles import load_tiles, get_tile_rect 4 | from load_tiles import SIZE 5 | from generate_maze import create_maze 6 | from util import debug_print 7 | 8 | 9 | def parse_grid(data): 10 | """Parses the string representation into a nested list""" 11 | return [list(row) for row in data.strip().split("\n")] 12 | 13 | 14 | def draw_grid(data, tile_img, tiles): 15 | """Returns an image of a tile-based grid""" 16 | debug_print("drawing level", data) 17 | xsize = len(data[0]) * SIZE 18 | ysize = len(data) * SIZE 19 | img = Surface((xsize, ysize)) 20 | for y, row in enumerate(data): 21 | for x, char in enumerate(row): 22 | rect = get_tile_rect(x, y) 23 | img.blit(tile_img, rect, tiles[char]) 24 | return img 25 | 26 | 27 | if __name__ == '__main__': 28 | tile_img, tiles = load_tiles() 29 | level = create_maze(12, 7) 30 | level = parse_grid(level) 31 | maze = draw_grid(level, tile_img, tiles) 32 | image.save(maze, 'maze.png') 33 | -------------------------------------------------------------------------------- /09_test_data/event_loop.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 04 - Debugging with the Scientific Method 3 | 4 | # The program prints the codes of pressed keys. 5 | 6 | from pygame.locals import KEYDOWN 7 | import pygame 8 | 9 | 10 | def event_loop(handle_key, delay=10): 11 | """Processes events and updates callbacks.""" 12 | while True: 13 | pygame.event.pump() 14 | event = pygame.event.poll() 15 | if event.type == KEYDOWN: 16 | handle_key(event.key) 17 | pygame.time.delay(delay) 18 | 19 | 20 | if __name__ == '__main__': 21 | pygame.init() 22 | pygame.display.set_mode((640, 400)) 23 | event_loop(print) 24 | -------------------------------------------------------------------------------- /09_test_data/expected.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /09_test_data/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | XMAX, YMAX = 19, 16 5 | 6 | 7 | def create_grid_string(dots, xsize, ysize): 8 | """ 9 | Creates a grid of size (xx, yy) 10 | with the given positions of dots. 11 | """ 12 | grid = "" 13 | for y in range(ysize): 14 | for x in range(xsize): 15 | grid += "." if (x, y) in dots else "#" 16 | grid += "\n" 17 | return grid 18 | 19 | 20 | def get_all_dot_positions(xsize, ysize): 21 | """Returns a list of (x, y) tuples covering all positions in a grid""" 22 | return [(x,y) for x in range(1, xsize-1) for y in range(1, ysize-1)] 23 | 24 | 25 | def get_neighbors(x, y): 26 | """Returns a list with the 8 neighbor positions of (x, y)""" 27 | return [ 28 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 29 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 30 | ] 31 | 32 | 33 | def generate_dot_positions(xsize, ysize): 34 | """Creates positions of dots for a random maze""" 35 | positions = get_all_dot_positions(xsize, ysize) 36 | dots = set() 37 | while positions != []: 38 | x, y = random.choice(positions) 39 | neighbors = get_neighbors(x, y) 40 | free = [nb in dots for nb in neighbors] 41 | if free.count(True) < 5: 42 | dots.add((x, y)) 43 | positions.remove((x, y)) 44 | return dots 45 | 46 | 47 | def create_maze(xsize, ysize): 48 | """Returns a xsize*ysize maze as a string""" 49 | dots = generate_dot_positions(xsize, ysize) 50 | maze = create_grid_string(dots, xsize, ysize) 51 | return maze 52 | 53 | 54 | if __name__ == '__main__': 55 | dots = set(((1,1), (1,2), (1,3), (2,2), (3,1), (3,2), (3,3))) 56 | print(create_grid_string(dots, 5, 5)) 57 | 58 | positions = get_all_dot_positions(5, 5) 59 | print(create_grid_string(positions, 5, 5)) 60 | 61 | neighbors = get_neighbors(3, 2) 62 | print(create_grid_string(neighbors, 5, 5)) 63 | 64 | maze = create_maze(12, 7) 65 | print(maze) 66 | -------------------------------------------------------------------------------- /09_test_data/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | from pygame import image, Rect, Surface 3 | 4 | TILE_POSITIONS = [ 5 | ('#', 0, 0), # wall 6 | ('o', 1, 0), # crate 7 | (' ', 0, 1), # floor 8 | ('x', 1, 1), # exit 9 | ('.', 2, 0), # dot 10 | ('*', 3, 0), # player 11 | ] 12 | SIZE = 32 13 | 14 | 15 | def get_tile_rect(x, y): 16 | """Converts tile indices to a pygame.Rect""" 17 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 18 | 19 | 20 | def load_tiles(): 21 | """Load tiles and returns a tuple of (image, tile_dict) """ 22 | tile_image = image.load('../images/tiles.xpm') 23 | tiles = {} 24 | for symbol, x, y in TILE_POSITIONS: 25 | tiles[symbol] = get_tile_rect(x, y) 26 | return tile_image, tiles 27 | 28 | 29 | if __name__ == '__main__': 30 | tile_img, tiles = load_tiles() 31 | m = Surface((96, 32)) 32 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 33 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 34 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 35 | image.save(m, 'tile_combo.png') 36 | -------------------------------------------------------------------------------- /09_test_data/maze_run.py: -------------------------------------------------------------------------------- 1 | from load_tiles import load_tiles 2 | from generate_maze import create_maze 3 | from event_loop import event_loop 4 | from draw_maze import draw_grid, parse_grid 5 | from moves import move, LEFT, RIGHT, UP, DOWN 6 | from pygame import Rect 7 | import pygame 8 | 9 | 10 | # initialize display 11 | pygame.init() 12 | pygame.display.set_mode((800, 600)) 13 | display = pygame.display.get_surface() 14 | 15 | # prepare the maze 16 | maze = parse_grid(create_maze(12, 7)) 17 | maze[1][1] = '*' 18 | maze[5][10] = 'x' 19 | 20 | # draw the graphics 21 | tile_img, tiles = load_tiles() 22 | img = draw_grid(maze, tile_img, tiles) 23 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 24 | pygame.display.update() 25 | 26 | # start the game 27 | DIRECTIONS = { 28 | 276: LEFT, 29 | 275: RIGHT, 30 | 273: UP, 31 | 274: DOWN 32 | } 33 | 34 | 35 | def handle_key(key): 36 | """Handles key events in the game""" 37 | direction = DIRECTIONS.get(key) 38 | if direction: 39 | move(maze, direction) 40 | img = draw_grid(maze, tile_img, tiles) 41 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 42 | pygame.display.update() 43 | 44 | 45 | if __name__ == '__main__': 46 | event_loop(handle_key) 47 | -------------------------------------------------------------------------------- /09_test_data/moves.py: -------------------------------------------------------------------------------- 1 | from load_tiles import load_tiles 2 | from generate_maze import create_maze 3 | from draw_maze import draw_grid, parse_grid 4 | from pygame import image 5 | import random 6 | import sys 7 | 8 | 9 | LEFT = (-1, 0) 10 | RIGHT = (1, 0) 11 | UP = (0, -1) 12 | DOWN = (0, 1) 13 | 14 | 15 | def get_player_pos(level, player_char='*'): 16 | """Returns a (x, y) tuple of player char on the level""" 17 | for y, row in enumerate(level): 18 | for x, char in enumerate(row): 19 | if char == player_char: 20 | return x, y 21 | 22 | 23 | def move(level, direction): 24 | """Handles moves on the level""" 25 | oldx, oldy = get_player_pos(level) 26 | newx = oldx + direction[0] 27 | newy = oldy + direction[1] 28 | if level[newy][newx] == 'x': 29 | sys.exit(0) 30 | if level[newy][newx] == 'o': 31 | cratex = newx + direction[0] 32 | cratey = newy + direction[1] 33 | if level[cratey][cratex] in '. ': 34 | level[cratey][cratex] = 'o' 35 | level[newy][newx] = ' ' 36 | if level[newy][newx] in '. ': 37 | level[oldy][oldx] = ' ' 38 | level[newy][newx] = '*' 39 | 40 | 41 | if __name__ == '__main__': 42 | tile_img, tiles = load_tiles() 43 | maze = create_maze(12, 7) 44 | maze = parse_grid(maze) 45 | maze[1][1] = '*' 46 | for i in range(100): 47 | direction = random.choice([LEFT, RIGHT, UP, DOWN]) 48 | move(maze, direction) 49 | img = draw_grid(maze, tile_img, tiles) 50 | image.save(img, 'moved.png') 51 | -------------------------------------------------------------------------------- /09_test_data/test_crate.py: -------------------------------------------------------------------------------- 1 | 2 | from draw_maze import parse_grid 3 | from moves import move 4 | from moves import LEFT, RIGHT, UP, DOWN 5 | import pytest 6 | 7 | LEVEL = """####### 8 | #.....# 9 | #..o..# 10 | #.o*o.# 11 | #..o..# 12 | #.....# 13 | #######""" 14 | 15 | @pytest.mark.parametrize('direction, pos, tile', [ 16 | (LEFT, (3, 2), '*'), 17 | (LEFT, (3, 1), 'o'), 18 | (RIGHT, (3, 4), '*'), 19 | (RIGHT, (3, 5), 'o'), 20 | (UP, (2, 3), '*'), 21 | (UP, (1, 3), 'o'), 22 | (DOWN, (4, 3), '*'), 23 | (DOWN, (5, 3), 'o'), 24 | (DOWN, (3, 3), ' '), 25 | ]) 26 | def test_move_crate(direction, pos, tile): 27 | """Move a crate and check a given tile""" 28 | maze = parse_grid(LEVEL) 29 | move(maze, direction) 30 | assert maze[pos[0]][pos[1]] == tile 31 | 32 | 33 | def test_push_crate_to_wall(): 34 | maze = parse_grid("*o#") 35 | move(maze, RIGHT) 36 | assert maze[0] == ['*', 'o', '#'] 37 | 38 | 39 | def test_push_crate_to_crate(): 40 | maze = parse_grid("*oo") 41 | move(maze, RIGHT) 42 | assert maze == [['*', 'o', 'o']] 43 | 44 | 45 | def test_push_crate_to_exit(): 46 | maze = parse_grid("*ox") 47 | with pytest.raises(NotImplementedError): 48 | move(maze, RIGHT) 49 | 50 | 51 | @pytest.fixture 52 | def bf_crate(): 53 | """A single crate that can be pushed back and forth""" 54 | maze = parse_grid(""".*o..\n.....""") 55 | return maze 56 | 57 | 58 | def test_move_left_right(bf_crate): 59 | for d in [DOWN, RIGHT, RIGHT, UP, LEFT, DOWN, LEFT, LEFT, UP, RIGHT]: 60 | move(bf_crate, d) 61 | assert bf_crate[0][2] == 'o' 62 | 63 | def test_move_right_left(bf_crate): 64 | for d in [RIGHT, DOWN, RIGHT, RIGHT, UP, LEFT]: 65 | move(bf_crate, d) 66 | assert bf_crate[0][2] == 'o' 67 | 68 | def test_move_lrrl(bf_crate): 69 | for d in [DOWN, RIGHT, RIGHT, UP, LEFT, DOWN, LEFT, LEFT, UP, 70 | RIGHT, RIGHT, DOWN, RIGHT, RIGHT, UP, LEFT]: 71 | move(bf_crate, d) 72 | assert bf_crate[0][2] == 'o' 73 | 74 | 75 | SMALL_MAZE = """ 76 | ##### 77 | #...# 78 | #*o.# 79 | #...# 80 | #####""" 81 | 82 | PATHS = [ 83 | (UP, RIGHT, RIGHT, DOWN), 84 | (UP, RIGHT, DOWN, RIGHT), 85 | (DOWN, RIGHT, UP, RIGHT), 86 | pytest.mark.xfail((RIGHT, RIGHT)) 87 | ] 88 | 89 | @pytest.mark.parametrize('path', PATHS) 90 | def test_paths(path): 91 | """Different paths to the same spot""" 92 | maze = parse_grid(SMALL_MAZE) 93 | for direction in path: 94 | move(maze, direction) 95 | assert maze[2][3] == '*' 96 | -------------------------------------------------------------------------------- /09_test_data/test_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import filecmp 4 | import difflib 5 | 6 | 7 | def teardown_function(function): 8 | if os.path.exists('output.txt'): 9 | os.remove('output.txt') 10 | 11 | 12 | def test_file_output(): 13 | open('output.txt', 'w').write("Hello World!") # the code being tested 14 | assert os.path.exists('output.txt') 15 | 16 | 17 | def test_file_output_with_tempfile(): 18 | with tempfile.TemporaryFile('w') as f: 19 | f.write("Hello World!") # the code being tested 20 | 21 | 22 | def test_file_output_with_tempdir(tmpdir): 23 | tempf = tmpdir.join('output.txt') 24 | tempf.write("Hello World!") # the code being tested 25 | content = tempf.read() 26 | assert content == "Hello World!" 27 | # assert False # uncomment to see fixture path 28 | 29 | 30 | def test_compare_files(): 31 | open('output.txt', 'w').write("***Hello World***") # the code being tested 32 | lines_result = open('output.txt').readlines() 33 | lines_expected = open('expected.txt').readlines() 34 | print('\n'.join(difflib.ndiff(lines_result, lines_expected))) 35 | assert filecmp.cmp('output.txt', 'expected.txt') 36 | -------------------------------------------------------------------------------- /09_test_data/test_mock.py: -------------------------------------------------------------------------------- 1 | from pygame import image, Rect 2 | import pygame 3 | from unittest import mock 4 | 5 | 6 | pygame.init() 7 | pygame.display.set_mode((800, 600)) 8 | 9 | 10 | def draw(surface): 11 | img = image.load('../images/tiles.xpm') 12 | surface.blit(img, Rect((0, 0, 32, 32)), Rect((0, 0, 32, 32))) 13 | pygame.display.update() 14 | 15 | 16 | @mock.patch('pygame.display.update') 17 | def test_mocking(mock_update): 18 | display = pygame.display.get_surface() 19 | draw(display) 20 | assert mock_update.called is True 21 | assert mock_update.call_count == 1 22 | 23 | 24 | def test_bad_mocks(): 25 | mo = mock.MagicMock() 26 | assert mo.twenty_blue_dolphins() 27 | assert mo.foo.bar('spam')['eggs'] 28 | assert mo.was_called() # wrong method that passes 29 | assert mo.caled # typo that passes! 30 | 31 | 32 | def test_blit(): 33 | mock_surf = mock.MagicMock(name='surface') 34 | draw(mock_surf) 35 | assert mock_surf.blit.called is True 36 | -------------------------------------------------------------------------------- /09_test_data/util.py: -------------------------------------------------------------------------------- 1 | 2 | from pprint import pprint 3 | import sys 4 | 5 | DEBUG = '-d' in sys.argv 6 | 7 | def debug_print(*args, **kwargs): 8 | condition = kwargs.get('condition', True) 9 | if DEBUG and condition: 10 | for a in args: 11 | pprint(a) 12 | -------------------------------------------------------------------------------- /10_test_suite/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Test Suite for MazeRun 3 | 4 | To run the test, the directory with this file needs to be in the PYTHONPATH. 5 | 6 | On Linux, this can be done with: 7 | 8 | export PYTHONPATH=$PYTHONPATH:your_project_dir/maze_run/10_test_suite/ 9 | 10 | And then run the entire test suite from the same directory with: 11 | 12 | py.test 13 | 14 | 15 | Or play the game with 16 | 17 | python maze_run 18 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/10_test_suite/maze_run/__init__.py -------------------------------------------------------------------------------- /10_test_suite/maze_run/__main__.py: -------------------------------------------------------------------------------- 1 | from maze_run.load_tiles import load_tiles 2 | from maze_run.generate_maze import create_maze 3 | from maze_run.event_loop import event_loop 4 | from maze_run.draw_maze import draw_grid, parse_grid 5 | from maze_run.moves import move, LEFT, RIGHT, UP, DOWN 6 | from pygame import Rect 7 | import pygame 8 | 9 | 10 | # initialize display 11 | pygame.init() 12 | pygame.display.set_mode((800, 600)) 13 | display = pygame.display.get_surface() 14 | 15 | # prepare the maze 16 | maze = parse_grid(create_maze(12, 7)) 17 | maze[1][1] = '*' 18 | maze[5][10] = 'x' 19 | 20 | # draw the graphics 21 | tile_img, tiles = load_tiles() 22 | img = draw_grid(maze, tile_img, tiles) 23 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 24 | pygame.display.update() 25 | 26 | # start the game 27 | DIRECTIONS = { 28 | 276: LEFT, 29 | 275: RIGHT, 30 | 273: UP, 31 | 274: DOWN 32 | } 33 | 34 | 35 | def handle_key(key): 36 | """Handles key events in the game""" 37 | direction = DIRECTIONS.get(key) 38 | if direction: 39 | move(maze, direction) 40 | img = draw_grid(maze, tile_img, tiles) 41 | display.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 42 | pygame.display.update() 43 | 44 | 45 | if __name__ == '__main__': 46 | event_loop(handle_key) 47 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/draw_maze.py: -------------------------------------------------------------------------------- 1 | 2 | from pygame import image, Surface 3 | from maze_run.load_tiles import load_tiles, get_tile_rect 4 | from maze_run.load_tiles import SIZE 5 | from maze_run.generate_maze import create_maze 6 | from maze_run.util import debug_print 7 | 8 | 9 | def parse_grid(data): 10 | """Parses the string representation into a nested list""" 11 | return [list(row) for row in data.strip().split("\n")] 12 | 13 | 14 | def draw_grid(data, tile_img, tiles): 15 | """Returns an image of a tile-based grid""" 16 | debug_print("drawing level", data) 17 | xsize = len(data[0]) * SIZE 18 | ysize = len(data) * SIZE 19 | img = Surface((xsize, ysize)) 20 | for y, row in enumerate(data): 21 | for x, char in enumerate(row): 22 | rect = get_tile_rect(x, y) 23 | img.blit(tile_img, rect, tiles[char]) 24 | return img 25 | 26 | 27 | if __name__ == '__main__': 28 | tile_img, tiles = load_tiles() 29 | level = create_maze(12, 7) 30 | level = parse_grid(level) 31 | maze = draw_grid(level, tile_img, tiles) 32 | image.save(maze, 'maze.png') 33 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/event_loop.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for chapter 04 - Debugging with the Scientific Method 3 | 4 | # The program prints the codes of pressed keys. 5 | 6 | from pygame.locals import KEYDOWN 7 | import pygame 8 | 9 | 10 | def event_loop(handle_key, delay=10): 11 | """Processes events and updates callbacks.""" 12 | while True: 13 | pygame.event.pump() 14 | event = pygame.event.poll() 15 | if event.type == KEYDOWN: 16 | handle_key(event.key) 17 | pygame.time.delay(delay) 18 | 19 | 20 | if __name__ == '__main__': 21 | pygame.init() 22 | pygame.display.set_mode((640, 400)) 23 | event_loop(print) 24 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/generate_maze.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | XMAX, YMAX = 19, 16 5 | 6 | 7 | def create_grid_string(dots, xsize, ysize): 8 | """ 9 | Creates a grid of size (xx, yy) 10 | with the given positions of dots. 11 | """ 12 | grid = "" 13 | for y in range(ysize): 14 | for x in range(xsize): 15 | grid += "." if (x, y) in dots else "#" 16 | grid += "\n" 17 | return grid 18 | 19 | 20 | def get_all_dot_positions(xsize, ysize): 21 | """Returns a list of (x, y) tuples covering all positions in a grid""" 22 | return [(x,y) for x in range(1, xsize-1) for y in range(1, ysize-1)] 23 | 24 | 25 | def get_neighbors(x, y): 26 | """Returns a list with the 8 neighbor positions of (x, y)""" 27 | return [ 28 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 29 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 30 | ] 31 | 32 | 33 | def generate_dot_positions(xsize, ysize): 34 | """Creates positions of dots for a random maze""" 35 | positions = get_all_dot_positions(xsize, ysize) 36 | dots = set() 37 | while positions != []: 38 | x, y = random.choice(positions) 39 | neighbors = get_neighbors(x, y) 40 | free = [nb in dots for nb in neighbors] 41 | if free.count(True) < 5: 42 | dots.add((x, y)) 43 | positions.remove((x, y)) 44 | return dots 45 | 46 | 47 | def create_maze(xsize, ysize): 48 | """Returns a xsize*ysize maze as a string""" 49 | dots = generate_dot_positions(xsize, ysize) 50 | maze = create_grid_string(dots, xsize, ysize) 51 | return maze 52 | 53 | 54 | if __name__ == '__main__': 55 | dots = set(((1,1), (1,2), (1,3), (2,2), (3,1), (3,2), (3,3))) 56 | print(create_grid_string(dots, 5, 5)) 57 | 58 | positions = get_all_dot_positions(5, 5) 59 | print(create_grid_string(positions, 5, 5)) 60 | 61 | neighbors = get_neighbors(3, 2) 62 | print(create_grid_string(neighbors, 5, 5)) 63 | 64 | maze = create_maze(12, 7) 65 | print(maze) 66 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/load_tiles.py: -------------------------------------------------------------------------------- 1 | 2 | from pygame import image, Rect, Surface 3 | 4 | TILE_POSITIONS = [ 5 | ('#', 0, 0), # wall 6 | ('o', 1, 0), # crate 7 | (' ', 0, 1), # floor 8 | ('x', 1, 1), # exit 9 | ('.', 2, 0), # dot 10 | ('*', 3, 0), # player 11 | ] 12 | SIZE = 32 13 | 14 | 15 | def get_tile_rect(x, y): 16 | """Converts tile indices to a pygame.Rect""" 17 | return Rect(x * SIZE, y * SIZE, SIZE, SIZE) 18 | 19 | 20 | def load_tiles(): 21 | """Load tiles and returns a tuple of (image, tile_dict) """ 22 | tile_image = image.load('../../images/tiles.xpm') 23 | tiles = {} 24 | for symbol, x, y in TILE_POSITIONS: 25 | tiles[symbol] = get_tile_rect(x, y) 26 | return tile_image, tiles 27 | 28 | 29 | if __name__ == '__main__': 30 | tile_img, tiles = load_tiles() 31 | m = Surface((96, 32)) 32 | m.blit(tile_img, get_tile_rect(0, 0), tiles['#']) 33 | m.blit(tile_img, get_tile_rect(1, 0), tiles[' ']) 34 | m.blit(tile_img, get_tile_rect(2, 0), tiles['*']) 35 | image.save(m, 'tile_combo.png') 36 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/moves.py: -------------------------------------------------------------------------------- 1 | from maze_run.load_tiles import load_tiles 2 | from maze_run.generate_maze import create_maze 3 | from maze_run.draw_maze import draw_grid, parse_grid 4 | from pygame import image 5 | import random 6 | import sys 7 | 8 | 9 | LEFT = (-1, 0) 10 | RIGHT = (1, 0) 11 | UP = (0, -1) 12 | DOWN = (0, 1) 13 | 14 | 15 | def get_player_pos(level, player_char='*'): 16 | """Returns a (x, y) tuple of player char on the level""" 17 | for y, row in enumerate(level): 18 | for x, char in enumerate(row): 19 | if char == player_char: 20 | return x, y 21 | 22 | 23 | def move(level, direction): 24 | """Handles moves on the level""" 25 | oldx, oldy = get_player_pos(level) 26 | newx = oldx + direction[0] 27 | newy = oldy + direction[1] 28 | if level[newy][newx] == 'x': 29 | sys.exit(0) 30 | if level[newy][newx] == 'o': 31 | cratex = newx + direction[0] 32 | cratey = newy + direction[1] 33 | if level[cratey][cratex] in '. ': 34 | level[cratey][cratex] = 'o' 35 | level[newy][newx] = ' ' 36 | if level[newy][newx] in '. ': 37 | level[oldy][oldx] = ' ' 38 | level[newy][newx] = '*' 39 | 40 | 41 | if __name__ == '__main__': 42 | tile_img, tiles = load_tiles() 43 | maze = create_maze(12, 7) 44 | maze = parse_grid(maze) 45 | maze[1][1] = '*' 46 | for i in range(100): 47 | direction = random.choice([LEFT, RIGHT, UP, DOWN]) 48 | move(maze, direction) 49 | img = draw_grid(maze, tile_img, tiles) 50 | image.save(img, 'moved.png') 51 | -------------------------------------------------------------------------------- /10_test_suite/maze_run/util.py: -------------------------------------------------------------------------------- 1 | 2 | from pprint import pprint 3 | import sys 4 | 5 | DEBUG = '-d' in sys.argv 6 | 7 | def debug_print(*args, **kwargs): 8 | condition = kwargs.get('condition', True) 9 | if DEBUG and condition: 10 | for a in args: 11 | pprint(a) 12 | -------------------------------------------------------------------------------- /10_test_suite/test/expected.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /10_test_suite/test/fixtures.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | from maze_run.draw_maze import parse_grid 4 | 5 | LEVEL = """####### 6 | #.....# 7 | #..o..# 8 | #.o*o.# 9 | #..o..# 10 | #.....# 11 | #######""" 12 | 13 | LEVEL_NO_DOTS = LEVEL.replace('.', ' ') 14 | 15 | 16 | @pytest.fixture(params=[LEVEL, LEVEL_NO_DOTS]) 17 | def level(request): 18 | """A level with four single crates""" 19 | return parse_grid(request.param) 20 | -------------------------------------------------------------------------------- /10_test_suite/test/test_file_output.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import filecmp 4 | import difflib 5 | 6 | PATH = os.path.split(__file__)[0] + os.sep 7 | 8 | 9 | def teardown_function(function): 10 | if os.path.exists('output.txt'): 11 | os.remove('output.txt') 12 | 13 | 14 | def test_file_output(): 15 | open('output.txt', 'w').write("Hello World!") # the code being tested 16 | assert os.path.exists('output.txt') 17 | 18 | 19 | def test_file_output_with_tempfile(): 20 | with tempfile.TemporaryFile('w') as f: 21 | f.write("Hello World!") # the code being tested 22 | 23 | 24 | def test_file_output_with_tempdir(tmpdir): 25 | tempf = tmpdir.join('output.txt') 26 | tempf.write("Hello World!") # the code being tested 27 | content = tempf.read() 28 | assert content == "Hello World!" 29 | # assert False # uncomment to see fixture path 30 | 31 | 32 | def test_compare_files(): 33 | open('output.txt', 'w').write("***Hello World***") # the code being tested 34 | lines_result = open('output.txt').readlines() 35 | lines_expected = open(PATH + 'expected.txt').readlines() 36 | print('\n'.join(difflib.ndiff(lines_result, lines_expected))) 37 | assert filecmp.cmp('output.txt', PATH + 'expected.txt') 38 | -------------------------------------------------------------------------------- /10_test_suite/test/test_graphics.py: -------------------------------------------------------------------------------- 1 | from maze_run.draw_maze import draw_grid 2 | from maze_run.__main__ import pygame, Rect, handle_key 3 | from maze_run.__main__ import tile_img, tiles, maze 4 | from unittest import mock 5 | 6 | 7 | def draw(surface): 8 | # prepare the maze and draw the graphics 9 | img = draw_grid(maze, tile_img, tiles) 10 | surface.blit(img, Rect((0, 0, 384, 224)), Rect((0, 0, 384, 224))) 11 | pygame.display.update() 12 | 13 | 14 | @mock.patch('pygame.display.update') 15 | def test_mocking(mock_update): 16 | handle_key(275) 17 | assert mock_update.called is True 18 | assert mock_update.call_count == 1 19 | 20 | 21 | @mock.patch('pygame.display.update') 22 | def test_bad_mocks(mock_update): 23 | assert mock_update.twenty_blue_dolphins() 24 | assert mock_update.caled # !!! passes 25 | 26 | 27 | def test_blit(): 28 | mock_surf = mock.MagicMock(name='surface') 29 | draw(mock_surf) 30 | assert mock_surf.blit.called is True 31 | -------------------------------------------------------------------------------- /10_test_suite/test/test_moves.py: -------------------------------------------------------------------------------- 1 | 2 | from maze_run.draw_maze import parse_grid 3 | from maze_run.moves import move 4 | from maze_run.moves import LEFT, RIGHT, UP, DOWN 5 | from fixtures import level 6 | import pytest 7 | 8 | CRATE_MOVES = [ 9 | (LEFT, (3, 2), (3, 1)), 10 | (RIGHT, (3, 4), (3, 5)), 11 | (UP, (2, 3), (1, 3)), 12 | (DOWN, (4, 3), (5, 3)), 13 | ] 14 | 15 | 16 | class TestCrateMoves: 17 | 18 | @pytest.mark.parametrize('direction, plr_pos, crate_pos', CRATE_MOVES) 19 | def test_move_crate(self, level, direction, plr_pos, crate_pos): 20 | """After move player and crate moved by one square""" 21 | print(direction, plr_pos, crate_pos) 22 | move(level, direction) 23 | assert level[plr_pos[0]][plr_pos[1]] == '*' 24 | assert level[crate_pos[0]][crate_pos[1]] == 'o' 25 | 26 | def test_push_crate_to_wall(self): 27 | maze = parse_grid("*o#") 28 | move(maze, RIGHT) 29 | assert maze[0] == ['*', 'o', '#'] 30 | 31 | def test_push_crate_to_crate(self): 32 | maze = parse_grid("*oo") 33 | move(maze, RIGHT) 34 | assert maze == [['*', 'o', 'o']] 35 | 36 | def test_move_crate_to_corner(self, level): 37 | """Moves top crate to upper left corner""" 38 | for d in [UP, RIGHT, UP, LEFT, LEFT, LEFT]: 39 | move(level, d) 40 | assert level[1][1] == 'o' 41 | 42 | def test_move_crate_back_forth(self, level): 43 | """Sanity check: move the top crate twice""" 44 | for d in [LEFT, UP, RIGHT, UP, RIGHT, RIGHT, DOWN, LEFT, LEFT, LEFT]: 45 | move(level, d) 46 | assert level[2] == list('#o* #') 47 | 48 | 49 | PATHS = [ 50 | ((UP, LEFT), 2, 2), 51 | ((LEFT, UP), 2, 2), 52 | ((RIGHT, UP, LEFT, LEFT), 2, 2), 53 | pytest.mark.xfail(((DOWN, DOWN), 0, 0)), 54 | ((LEFT,), 2, 3), 55 | ((LEFT, RIGHT), 3, 3), 56 | ((RIGHT, RIGHT), 4, 3), 57 | ] 58 | 59 | 60 | class TestPlayerMoves: 61 | 62 | def test_move_to_none(self, level): 63 | """direction=None generates an Exception""" 64 | with pytest.raises(TypeError): 65 | move(level, None) 66 | 67 | @pytest.mark.parametrize('path, expected_x, expected_y', PATHS) 68 | def test_move_player(self, level, path, expected_x, expected_y): 69 | """Player position changes correctly""" 70 | for direction in path: 71 | move(level, direction) 72 | assert level[expected_y][expected_x] == '*' 73 | -------------------------------------------------------------------------------- /11_testing_best_practices/generate_maze_faster.py: -------------------------------------------------------------------------------- 1 | 2 | # Improved version of the code from chapter 03 3 | # created in chapter 11 to accelerate execution 4 | 5 | 6 | import random 7 | 8 | XMAX, YMAX = 19, 16 9 | 10 | 11 | def create_grid_string(dots, xsize, ysize): 12 | """ 13 | Creates a grid of size (xx, yy) 14 | with the given positions of dots. 15 | """ 16 | grid = "" 17 | for y in range(ysize): 18 | for x in range(xsize): 19 | grid += "." if (x, y) in dots else "#" 20 | grid += "\n" 21 | return grid 22 | 23 | 24 | def get_all_dot_positions(xsize, ysize): 25 | """Returns a list of (x, y) tuples covering all positions in a grid""" 26 | return [(x,y) for x in range(1, xsize-1) for y in range(1, ysize-1)] 27 | 28 | 29 | def get_neighbors(x, y): 30 | """Returns a list with the 8 neighbor positions of (x, y)""" 31 | return [ 32 | (x, y-1), (x, y+1), (x-1, y), (x+1, y), 33 | (x-1, y-1), (x+1, y-1), (x-1, y+1), (x+1, y+1) 34 | ] 35 | 36 | 37 | def generate_dot_positions(xsize, ysize): 38 | """Creates positions of dots for a random maze""" 39 | positions = get_all_dot_positions(xsize, ysize) 40 | random.shuffle(positions) 41 | dots = set() 42 | for x, y in positions: 43 | neighbors = get_neighbors(x, y) 44 | free = [nb in dots for nb in neighbors] 45 | if free.count(True) < 5: 46 | dots.add((x, y)) 47 | return dots 48 | 49 | 50 | def create_maze(xsize, ysize): 51 | """Returns a xsize*ysize maze as a string""" 52 | dots = generate_dot_positions(xsize, ysize) 53 | maze = create_grid_string(dots, xsize, ysize) 54 | return maze 55 | 56 | 57 | if __name__ == '__main__': 58 | dots = set(((1,1), (1,2), (1,3), (2,2), (3,1), (3,2), (3,3))) 59 | print(create_grid_string(dots, 5, 5)) 60 | 61 | positions = get_all_dot_positions(5, 5) 62 | print(create_grid_string(positions, 5, 5)) 63 | 64 | neighbors = get_neighbors(3, 2) 65 | print(create_grid_string(neighbors, 5, 5)) 66 | 67 | maze = create_maze(12, 7) 68 | print(maze) 69 | -------------------------------------------------------------------------------- /12_version_control/example.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | pygame/* 3 | *.log 4 | 5 | password.txt 6 | -------------------------------------------------------------------------------- /12_version_control/example_git_log.txt: -------------------------------------------------------------------------------- 1 | commit 8633a6684be437d6291732909c5a74c1a0e0acfc 2 | Author: krother 3 | Date: Sat Jul 9 21:07:08 2016 +0200 4 | 5 | added password to .gitignore 6 | 7 | commit 57bdf0b9e4ad3ec0ba0dd0d367036e505564debf 8 | Author: krother 9 | Date: Fri Jul 1 11:45:40 2016 +0200 10 | 11 | improved module docstring for maze generator 12 | 13 | commit b865b483a7e042e02724464cc6bd944b23e2324e 14 | Author: krother 15 | Date: Fri May 27 21:18:51 2016 +0200 16 | 17 | cleaned up scripts for part 1 18 | 19 | commit dbc3822a2d3a82273c0b602adf203085e749fde8 20 | Author: krother 21 | Date: Fri May 27 15:06:36 2016 +0200 22 | 23 | added chapter links to README 24 | 25 | commit b12f390e50c9ecb174a995f84ff3f55dd93bcdc6 26 | Author: krother 27 | Date: Fri May 27 14:48:16 2016 +0200 28 | 29 | linking chapter branch from README 30 | 31 | commit fb99da022985dc243cbbbcd62efe227f66238787 32 | Author: krother 33 | Date: Fri May 27 13:49:03 2016 +0200 34 | 35 | created branch for chapter 12 36 | 37 | commit 1b0fcb5f8954e16fdb7f7a6b4e81aaa3eed070f5 38 | Author: krother 39 | Date: Fri May 27 13:48:30 2016 +0200 40 | 41 | created branch for chapter 11 42 | 43 | commit 4a633bdbf1d2f758e8947206a467f0a72a9037b2 44 | Author: krother 45 | Date: Fri May 27 13:47:50 2016 +0200 46 | 47 | created branch for chapter 10 48 | 49 | commit 3ec9eaec42d40f7df00e552abf978481ea68ed83 50 | Author: krother 51 | Date: Fri May 27 13:44:04 2016 +0200 52 | 53 | refactored out create_display() 54 | 55 | commit 34a706331cf7040638efe7c71b6c4334511542da 56 | Author: krother 57 | Date: Fri May 27 10:44:52 2016 +0200 58 | 59 | cleaned up code for chapter 10 60 | 61 | commit a1584effba8aeee2752b997a55be1348c3481fb9 62 | Author: krother 63 | Date: Tue May 24 21:45:39 2016 +0200 64 | 65 | cleaned up code for chapter09 66 | 67 | commit fd87f22486e6e41dff59d9b2b9ac9b60eb6d2972 68 | Author: krother 69 | Date: Tue May 24 18:05:06 2016 +0200 70 | 71 | added code examples for chapter 08 72 | 73 | commit c57e1b4443cf160b8fdc155997d2fa8b8ba21ab8 74 | Author: krother 75 | Date: Mon May 23 15:39:53 2016 +0200 76 | 77 | cleaned up code for part 1 78 | 79 | commit 528be6552f956d2bdefe01a7efa359584316565c 80 | Author: krother 81 | Date: Mon May 23 11:03:45 2016 +0200 82 | 83 | cleaned up code for chapter 05 84 | 85 | commit d8a82e02bc57a9e33e2f2ae0437dc1530dbf4336 86 | Author: krother 87 | Date: Mon May 23 00:13:16 2016 +0200 88 | 89 | cleaned code for chapter 04 90 | 91 | commit 6c3f01bb1efbe1dfdf6479a60c96897505c1d7a4 92 | Author: krother 93 | Date: Sat May 21 07:38:25 2016 +0200 94 | 95 | cleaned up code for chapter 03 96 | 97 | commit 4e57ebeaaec22dcd9231e86328972b77c7cfdb61 98 | Author: krother 99 | Date: Sat May 21 07:01:15 2016 +0200 100 | 101 | completed cleanup 102 | 103 | commit ed8574b8f1b123f96917cf21afb84cc93d27300e 104 | Author: krother 105 | Date: Sat May 21 06:59:44 2016 +0200 106 | 107 | moved part2 108 | 109 | commit 1c00e5e99821781dbc2fdf9b2d3ba19e7c13f238 110 | Author: krother 111 | Date: Sat May 21 06:55:53 2016 +0200 112 | 113 | added code for part2 114 | 115 | commit 285f37eb7d6bc69468a23de4a636f01d08e4a1c1 116 | Author: krother 117 | Date: Sat May 21 06:55:00 2016 +0200 118 | 119 | cleaned chapter 02 120 | 121 | commit 0de499696ff57d5dab3fd8ec55121abde391daa4 122 | Author: krother 123 | Date: Sat May 21 06:32:15 2016 +0200 124 | 125 | finished reorganizing 126 | 127 | commit b9699d1e57292d33aa908a790013c93e15f24962 128 | Author: krother 129 | Date: Sat May 21 06:29:25 2016 +0200 130 | 131 | reorganizd part1 132 | 133 | commit 1cc42227e49bf772f96cd87c207b409ea7c37ba1 134 | Author: krother 135 | Date: Sat May 21 06:18:31 2016 +0200 136 | 137 | update before moving 138 | 139 | commit 6a6e9227b313f6f131681b10e9a98f1d207b4d8c 140 | Author: krother 141 | Date: Fri May 20 14:14:22 2016 +0200 142 | 143 | added material for chapter 01 144 | 145 | commit dcbf3dca47746419621910dd5c89ce153fa83fb2 146 | Author: krother 147 | Date: Wed May 11 01:12:41 2016 +0200 148 | 149 | refactored more classes into part2 program 150 | 151 | commit e430f0e477e7ad6bec7324d3dd22fe4bb23b3b38 152 | Author: krother 153 | Date: Mon May 2 10:13:45 2016 +0200 154 | 155 | sprites working 156 | 157 | commit cc6566d4c40533b08d83d873f76f15c6a4e0c92a 158 | Author: krother 159 | Date: Mon May 2 09:35:00 2016 +0200 160 | 161 | added a few features for part II 162 | 163 | commit 7f85384d2a05ac6e1d57b3cdd7f2cc62b4886970 164 | Author: krother 165 | Date: Wed Apr 27 01:35:41 2016 +0200 166 | 167 | removed introduction folder 168 | 169 | commit 00775ae39c0ff0af64221986e6bf375a77a30454 170 | Author: krother 171 | Date: Wed Apr 27 01:33:43 2016 +0200 172 | 173 | refactored scripts on debugging into one file 174 | 175 | commit 413bcfabac79ca5bb8df17ccbadf9bffc23354ed 176 | Author: krother 177 | Date: Wed Apr 27 00:57:47 2016 +0200 178 | 179 | cleanup work 180 | 181 | commit efbe74e4b9de02d43313050983ee00633a70ebdf 182 | Author: krother 183 | Date: Wed Apr 27 00:42:42 2016 +0200 184 | 185 | added code full of bugs 186 | 187 | commit 193a2f2c43280e6ac16e3e5ff6052f3a99f0ed34 188 | Author: krother 189 | Date: Tue Apr 26 13:42:31 2016 +0200 190 | 191 | game in working state after writing chapters 02-06 192 | 193 | commit d0148dbe16ccd64156a50dc9a3756a427a9e0ed9 194 | Author: krother 195 | Date: Tue Apr 26 11:31:46 2016 +0200 196 | 197 | edited code while writing chapters 02-06 198 | 199 | commit b91c8ae8c308b769b86b45109e0ee7d28d5fb1e2 200 | Author: krother 201 | Date: Sun Mar 6 20:16:08 2016 +0100 202 | 203 | added maze generator 204 | 205 | commit f5eb1a58391fa6cc0041c7ab087dfe4d9e4d4fa7 206 | Author: krother 207 | Date: Thu Mar 3 09:08:44 2016 +0100 208 | 209 | added maze generator 210 | 211 | commit b8e131959627e80dce94de5ad8c76916abe48634 212 | Author: krother 213 | Date: Mon Feb 29 10:13:10 2016 +0100 214 | 215 | added bugs 216 | 217 | commit b2155b8036c3997fd6a6a98bf3a4e2522335a75b 218 | Author: krother 219 | Date: Sat Feb 27 23:38:59 2016 +0100 220 | 221 | cleaned up import 222 | 223 | commit 3625f8d20b60e0f78e48fdfe9a5461f421f76fbf 224 | Author: krother 225 | Date: Fri Feb 26 12:21:40 2016 +0100 226 | 227 | fixed bug in moves.py 228 | 229 | commit 9102f2c10702cda745ea82b4a9b9af5eeae6e0a7 230 | Author: krother 231 | Date: Fri Feb 26 10:45:27 2016 +0100 232 | 233 | fixed background of tiles 234 | 235 | commit 1991ccbc0be4222d57ff6076182435b56f6ef207 236 | Author: krother 237 | Date: Wed Feb 24 17:39:36 2016 +0100 238 | 239 | code for first part released 240 | 241 | commit 164de6f680682f330196b66262b74955fa2d2a9a 242 | Author: krother 243 | Date: Wed Feb 24 08:46:56 2016 +0100 244 | 245 | set up maze run project 246 | -------------------------------------------------------------------------------- /13_project_scaffold/setup.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from setuptools import setup 4 | except ImportError: 5 | from distutils.core import setup 6 | 7 | config = { 8 | 'description': 'My Project', 9 | 'author': 'My Name', 10 | 'url': 'URL to get it at.', 11 | 'download_url': 'Where to download it.', 12 | 'author_email': 'My email.', 13 | 'version': '0.1', 14 | 'install_requires': ['nose'], 15 | 'packages': ['maze_run'], 16 | 'scripts': [], 17 | 'name': 'projectname' 18 | } 19 | 20 | setup(**config) 21 | -------------------------------------------------------------------------------- /14_cleaning_up/code_golf.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Chapter 14: loading tile positions from a file 4 | 5 | # CODE GOLF: using as few key strokes as possible (bad practice) 6 | 7 | xyconv = lambda x: [x[0], int(x[1]), int(x[2])] 8 | tile_positions = list(map(xyconv, [l.split('\t') for l in open('tiles.txt') if l[0]!='R'])) 9 | 10 | print(tile_positions) 11 | -------------------------------------------------------------------------------- /14_cleaning_up/load_tile_positions.py: -------------------------------------------------------------------------------- 1 | 2 | # Chapter 14 - Organizing code 3 | 4 | # Cleaned code for loading tiles 5 | 6 | import csv 7 | import os 8 | from pygame.rect import Rect 9 | 10 | CONFIG_PATH = os.path.split(__file__)[0] 11 | TILE_POSITION_FILE = os.path.join(CONFIG_PATH, 'tiles.txt') 12 | TILE_IMAGE_FILE = os.path.join(CONFIG_PATH, '../images/tiles.xpm') 13 | SIZE = 32 14 | 15 | 16 | def load_tile_positions(filename): 17 | """ 18 | Returns a dictionary of positions {name: (x, y), ..} parsed from the file 19 | """ 20 | tile_positions = {} 21 | with open(filename) as f: 22 | for row in csv.reader(f, delimiter='\t'): 23 | name = row[0] 24 | if not name.startswith('REMARK'): 25 | x = int(row[1]) 26 | y = int(row[2]) 27 | rect = Rect(x * SIZE, y * SIZE, SIZE, SIZE) 28 | tile_positions[name] = rect 29 | return tile_positions 30 | 31 | 32 | if __name__ == '__main__': 33 | tile_positions = load_tile_positions(TILE_POSITION_FILE) 34 | print(tile_positions) 35 | -------------------------------------------------------------------------------- /14_cleaning_up/tiles.txt: -------------------------------------------------------------------------------- 1 | REMARK x and y positions of tiles in the .xpm file 2 | # 0 0 3 | 0 1 4 | x 1 1 5 | . 2 0 6 | * 3 0 7 | g 5 0 -------------------------------------------------------------------------------- /14_cleaning_up/unorganized.py: -------------------------------------------------------------------------------- 1 | 2 | # Chapter 14: Organizing code 3 | 4 | # unstructured code containing redundancies (bad practice) 5 | 6 | 7 | TILE_POSITIONS = [] 8 | 9 | for line in open('tiles.txt'): 10 | print([line]) 11 | # print(dat) 12 | # if 'REMARK' in dat == True: # didn't work 13 | TILE_POSITIONS.append([line[0]]) 14 | if line.find('REMARK') != 0: 15 | x = line[2] 16 | y = line[4] 17 | TILE_POSITIONS[-1].append(int(x)) 18 | TILE_POSITIONS[-1].append(int(y)) 19 | # print(TILE_POSITIONS[-1]) 20 | else: 21 | TILE_POSITIONS.pop() 22 | line = line.strip() 23 | print(line[7:]) 24 | continue 25 | 26 | print(TILE_POSITIONS) 27 | -------------------------------------------------------------------------------- /15_decomposing_tasks/event_loop.py: -------------------------------------------------------------------------------- 1 | 2 | # Chapter 09 - Decomposing functionality 3 | 4 | import pygame 5 | from pygame.locals import USEREVENT, KEYDOWN 6 | 7 | 8 | EXIT = USEREVENT 9 | UPDATE_DISPLAY = USEREVENT + 1 10 | MOVE_GHOST = USEREVENT + 2 11 | 12 | 13 | def move_player(event): 14 | """moves when keys are pressed""" 15 | print('player moves') 16 | 17 | def move_ghost(event): 18 | """Ghost moves randomly""" 19 | print('ghost moves') 20 | 21 | def update_graphics(event): 22 | """Re-draws the game window""" 23 | print('graphics updated') 24 | pass 25 | 26 | 27 | 28 | callbacks = { 29 | KEYDOWN: move_player, 30 | MOVE_GHOST: move_ghost, 31 | UPDATE_DISPLAY: update_graphics, 32 | } 33 | 34 | def post_event(event): 35 | """Example for a user-generated event""" 36 | exit = pygame.event.Event(EXIT) 37 | pygame.event.post(exit) 38 | 39 | def event_loop(callbacks, delay=10): 40 | """Processes events and updates callbacks.""" 41 | running = True 42 | while running: 43 | pygame.event.pump() 44 | event = pygame.event.poll() 45 | action = callbacks.get(event.type) 46 | if action: 47 | action(event) 48 | pygame.time.delay(delay) 49 | if event.type == EXIT: 50 | running = False 51 | 52 | 53 | if __name__ == '__main__': 54 | pygame.init() 55 | pygame.display.set_mode((10, 10)) 56 | pygame.time.set_timer(UPDATE_DISPLAY, 1000) 57 | pygame.time.set_timer(MOVE_GHOST, 300) 58 | pygame.time.set_timer(EXIT, 5000) 59 | event_loop(callbacks) 60 | -------------------------------------------------------------------------------- /15_decomposing_tasks/mediator.py: -------------------------------------------------------------------------------- 1 | 2 | # Chapter 09 - Decomposing functionality 3 | 4 | import pygame 5 | from pygame import USEREVENT 6 | from itertools import count 7 | 8 | EXIT = USEREVENT + 1 9 | COUNTDOWN = USEREVENT + 2 10 | 11 | 12 | def event_loop(callbacks, delay=10): 13 | """Processes events and updates callbacks.""" 14 | running = True 15 | while running: 16 | pygame.event.pump() 17 | event = pygame.event.poll() 18 | action = callbacks.get(event.type) 19 | if action: 20 | action(event) 21 | pygame.time.delay(delay) 22 | if event.type == EXIT: 23 | running = False 24 | 25 | 26 | def exit_game(): 27 | exit = pygame.event.Event(EXIT) 28 | pygame.event.post(exit) 29 | 30 | 31 | if __name__ == '__main__': 32 | 33 | def countdown(event): 34 | """exits when the counter reaches 0""" 35 | number = next(ticker) 36 | print(number) 37 | if number == 0: 38 | exit_game() 39 | 40 | callbacks = {COUNTDOWN: countdown} 41 | 42 | ticker = count(10, -1) 43 | pygame.init() 44 | pygame.display.set_mode((10, 10)) 45 | pygame.time.set_timer(COUNTDOWN, 100) 46 | event_loop(callbacks) 47 | -------------------------------------------------------------------------------- /16_static_typing/highscores.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import NamedTuple, List 3 | import csv 4 | 5 | Highscore = NamedTuple('Highscore', [('score', int), ('name', str)]) 6 | 7 | 8 | class HighscoreList: 9 | """A sorted list of top scores""" 10 | def __init__(self, places: int=10) -> None: 11 | self.scores = [] # type: List[Highscore] 12 | self.places = places 13 | 14 | def add(self, highscore: Highscore) -> None: 15 | self.scores.append(highscore) 16 | self.scores.sort() 17 | self.scores = self.scores[:self.places] 18 | 19 | def __repr__(self) -> str: 20 | return "\n".join(["{:10s} {}".format(s.name, s.score) for s in self.scores]) 21 | 22 | 23 | def load_highscores(filename: str) -> HighscoreList: 24 | hs = HighscoreList() 25 | for row in csv.reader(open(filename)): 26 | name = row[0] 27 | score = int(row[1]) 28 | hs.add(Highscore(score, name)) 29 | return hs 30 | 31 | 32 | if __name__ == '__main__': 33 | # highscores = load_highscores('scores.csv') 34 | hs = HighscoreList() 35 | hs.add(Highscore(5500, 'Ada')) 36 | hs.add(Highscore(4400, 'Bob')) 37 | hs.add(Highscore('Charly', 777)) 38 | print(hs) 39 | -------------------------------------------------------------------------------- /16_static_typing/highscores.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/16_static_typing/highscores.sqlite -------------------------------------------------------------------------------- /16_static_typing/highscores_assert.py: -------------------------------------------------------------------------------- 1 | 2 | from collections import namedtuple 3 | 4 | Highscore = namedtuple('Highscore', ['score', 'name']) 5 | 6 | highscores = [] 7 | 8 | 9 | def add(name, score): 10 | assert type(name) is str 11 | assert type(score) is int 12 | hs = Highscore(score, name) 13 | highscores.append(hs) 14 | assert len(highscores) < 5 15 | 16 | 17 | add('Ada', 5500) 18 | add('Bob', 4400) 19 | add(3300, 'Charly') 20 | -------------------------------------------------------------------------------- /16_static_typing/highscores_cython.pyx: -------------------------------------------------------------------------------- 1 | 2 | 3 | cdef struct Highscore: 4 | char *name 5 | int score 6 | 7 | cdef Highscore scores[5] 8 | 9 | scores[0] = Highscore('Ada', 5500) 10 | scores[1] = Highscore('Bob', 4400) 11 | scores[2] = Highscore(3300, 'Charly') 12 | 13 | 14 | for i in range(2): 15 | print(scores[i].name, scores[i].score) 16 | -------------------------------------------------------------------------------- /16_static_typing/highscores_postgres.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | 3 | DB_SETUP = ''' 4 | CREATE TABLE IF NOT EXISTS scores ( 5 | player VARCHAR(25), 6 | score INTEGER); 7 | ''' 8 | 9 | db = psycopg2.connect(host="127.0.0.1", 10 | user="krother", 11 | dbname="highscores") 12 | cur = db.cursor() 13 | 14 | # create the database 15 | cur.execute(DB_SETUP) 16 | 17 | # fill the database with entries 18 | insert = "INSERT INTO scores VALUES (%s,%s);" 19 | cur.execute(insert, ('Ada', 5500)) 20 | cur.execute(insert, ('Bob', 4400)) 21 | cur.execute(insert, (3300, 'Charlie')) 22 | 23 | # retrieve the top five entries in descending order 24 | query = 'SELECT player, score FROM scores ORDER BY score DESC LIMIT 5;' 25 | cur.execute(query) 26 | for result in cur.fetchall(): 27 | print(result) 28 | 29 | db.close() 30 | -------------------------------------------------------------------------------- /16_static_typing/highscores_pycharm.py: -------------------------------------------------------------------------------- 1 | 2 | def add(a:int, b:int) -> int: 3 | return a + b 4 | 5 | add(3, 4) 6 | add(3.1, 2.4) 7 | add('1', '2') 8 | 9 | -------------------------------------------------------------------------------- /16_static_typing/highscores_sqlite3.py: -------------------------------------------------------------------------------- 1 | 2 | import sqlite3 3 | 4 | DB_SETUP = ''' 5 | CREATE TABLE IF NOT EXISTS scores ( 6 | player VARCHAR(25), 7 | score INTEGER); 8 | ''' 9 | 10 | # create the database 11 | db = sqlite3.connect('highscores.sqlite') 12 | db.executescript(DB_SETUP) 13 | 14 | # fill the database with entries 15 | insert = 'INSERT INTO scores VALUES (?,?);' 16 | db.execute(insert, ('Ada', 5500)) 17 | db.execute(insert, ('Bob', 4400)) 18 | db.execute(insert, (3300, 'Charlie')) 19 | 20 | 21 | # retrieve the top five entries in descending order 22 | query = 'SELECT player, score FROM scores ORDER BY score DESC LIMIT 5;' 23 | for result in db.execute(query): 24 | player, score = result 25 | print(result) 26 | -------------------------------------------------------------------------------- /16_static_typing/scores.csv: -------------------------------------------------------------------------------- 1 | Timmy,550 2 | Lancelot,440 3 | Guido,330 4 | Arthur,220 5 | Kristian,110 6 | -------------------------------------------------------------------------------- /17_documentation/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MazeRun.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MazeRun.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/MazeRun" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MazeRun" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /17_documentation/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # MazeRun documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 10 11:36:16 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.ifconfig', 39 | 'sphinx.ext.viewcode', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # 48 | # source_suffix = ['.rst', '.md'] 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | # 53 | # source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = 'MazeRun' 60 | copyright = '2016, Kristian Rother' 61 | author = 'Kristian Rother' 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = '1.0' 69 | # The full version, including alpha/beta/rc tags. 70 | release = '1.0' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # There are two options for replacing |today|: either, you set today to some 80 | # non-false value, then it is used: 81 | # 82 | # today = '' 83 | # 84 | # Else, today_fmt is used as the format for a strftime call. 85 | # 86 | # today_fmt = '%B %d, %Y' 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | # This patterns also effect to html_static_path and html_extra_path 91 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 92 | 93 | # The reST default role (used for this markup: `text`) to use for all 94 | # documents. 95 | # 96 | # default_role = None 97 | 98 | # If true, '()' will be appended to :func: etc. cross-reference text. 99 | # 100 | # add_function_parentheses = True 101 | 102 | # If true, the current module name will be prepended to all description 103 | # unit titles (such as .. function::). 104 | # 105 | # add_module_names = True 106 | 107 | # If true, sectionauthor and moduleauthor directives will be shown in the 108 | # output. They are ignored by default. 109 | # 110 | # show_authors = False 111 | 112 | # The name of the Pygments (syntax highlighting) style to use. 113 | pygments_style = 'sphinx' 114 | 115 | # A list of ignored prefixes for module index sorting. 116 | # modindex_common_prefix = [] 117 | 118 | # If true, keep warnings as "system message" paragraphs in the built documents. 119 | # keep_warnings = False 120 | 121 | # If true, `todo` and `todoList` produce output, else they produce nothing. 122 | todo_include_todos = True 123 | 124 | 125 | # -- Options for HTML output ---------------------------------------------- 126 | 127 | # The theme to use for HTML and HTML Help pages. See the documentation for 128 | # a list of builtin themes. 129 | # 130 | html_theme = 'default' 131 | 132 | # Theme options are theme-specific and customize the look and feel of a theme 133 | # further. For a list of options available for each theme, see the 134 | # documentation. 135 | # 136 | # html_theme_options = {} 137 | 138 | # Add any paths that contain custom themes here, relative to this directory. 139 | # html_theme_path = [] 140 | 141 | # The name for this set of Sphinx documents. 142 | # " v documentation" by default. 143 | # 144 | # html_title = 'MazeRun v1.0' 145 | 146 | # A shorter title for the navigation bar. Default is the same as html_title. 147 | # 148 | # html_short_title = None 149 | 150 | # The name of an image file (relative to this directory) to place at the top 151 | # of the sidebar. 152 | # 153 | # html_logo = None 154 | 155 | # The name of an image file (relative to this directory) to use as a favicon of 156 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 157 | # pixels large. 158 | # 159 | # html_favicon = None 160 | 161 | # Add any paths that contain custom static files (such as style sheets) here, 162 | # relative to this directory. They are copied after the builtin static files, 163 | # so a file named "default.css" will overwrite the builtin "default.css". 164 | html_static_path = ['_static'] 165 | 166 | # Add any extra paths that contain custom files (such as robots.txt or 167 | # .htaccess) here, relative to this directory. These files are copied 168 | # directly to the root of the documentation. 169 | # 170 | # html_extra_path = [] 171 | 172 | # If not None, a 'Last updated on:' timestamp is inserted at every page 173 | # bottom, using the given strftime format. 174 | # The empty string is equivalent to '%b %d, %Y'. 175 | # 176 | # html_last_updated_fmt = None 177 | 178 | # If true, SmartyPants will be used to convert quotes and dashes to 179 | # typographically correct entities. 180 | # 181 | # html_use_smartypants = True 182 | 183 | # Custom sidebar templates, maps document names to template names. 184 | # 185 | # html_sidebars = {} 186 | 187 | # Additional templates that should be rendered to pages, maps page names to 188 | # template names. 189 | # 190 | # html_additional_pages = {} 191 | 192 | # If false, no module index is generated. 193 | # 194 | # html_domain_indices = True 195 | 196 | # If false, no index is generated. 197 | # 198 | # html_use_index = True 199 | 200 | # If true, the index is split into individual pages for each letter. 201 | # 202 | # html_split_index = False 203 | 204 | # If true, links to the reST sources are added to the pages. 205 | # 206 | # html_show_sourcelink = True 207 | 208 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 209 | # 210 | # html_show_sphinx = True 211 | 212 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 213 | # 214 | # html_show_copyright = True 215 | 216 | # If true, an OpenSearch description file will be output, and all pages will 217 | # contain a tag referring to it. The value of this option must be the 218 | # base URL from which the finished HTML is served. 219 | # 220 | # html_use_opensearch = '' 221 | 222 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 223 | # html_file_suffix = None 224 | 225 | # Language to be used for generating the HTML full-text search index. 226 | # Sphinx supports the following languages: 227 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 228 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 229 | # 230 | # html_search_language = 'en' 231 | 232 | # A dictionary with options for the search language support, empty by default. 233 | # 'ja' uses this config value. 234 | # 'zh' user can custom change `jieba` dictionary path. 235 | # 236 | # html_search_options = {'type': 'default'} 237 | 238 | # The name of a javascript file (relative to the configuration directory) that 239 | # implements a search results scorer. If empty, the default will be used. 240 | # 241 | # html_search_scorer = 'scorer.js' 242 | 243 | # Output file base name for HTML help builder. 244 | htmlhelp_basename = 'MazeRundoc' 245 | 246 | # -- Options for LaTeX output --------------------------------------------- 247 | 248 | latex_elements = { 249 | # The paper size ('letterpaper' or 'a4paper'). 250 | # 251 | # 'papersize': 'letterpaper', 252 | 253 | # The font size ('10pt', '11pt' or '12pt'). 254 | # 255 | # 'pointsize': '10pt', 256 | 257 | # Additional stuff for the LaTeX preamble. 258 | # 259 | # 'preamble': '', 260 | 261 | # Latex figure (float) alignment 262 | # 263 | # 'figure_align': 'htbp', 264 | } 265 | 266 | # Grouping the document tree into LaTeX files. List of tuples 267 | # (source start file, target name, title, 268 | # author, documentclass [howto, manual, or own class]). 269 | latex_documents = [ 270 | (master_doc, 'MazeRun.tex', 'MazeRun Documentation', 271 | 'Kristian Rother', 'manual'), 272 | ] 273 | 274 | # The name of an image file (relative to this directory) to place at the top of 275 | # the title page. 276 | # 277 | # latex_logo = None 278 | 279 | # For "manual" documents, if this is true, then toplevel headings are parts, 280 | # not chapters. 281 | # 282 | # latex_use_parts = False 283 | 284 | # If true, show page references after internal links. 285 | # 286 | # latex_show_pagerefs = False 287 | 288 | # If true, show URL addresses after external links. 289 | # 290 | # latex_show_urls = False 291 | 292 | # Documents to append as an appendix to all manuals. 293 | # 294 | # latex_appendices = [] 295 | 296 | # If false, no module index is generated. 297 | # 298 | # latex_domain_indices = True 299 | 300 | 301 | # -- Options for manual page output --------------------------------------- 302 | 303 | # One entry per manual page. List of tuples 304 | # (source start file, name, description, authors, manual section). 305 | man_pages = [ 306 | (master_doc, 'mazerun', 'MazeRun Documentation', 307 | [author], 1) 308 | ] 309 | 310 | # If true, show URL addresses after external links. 311 | # 312 | # man_show_urls = False 313 | 314 | 315 | # -- Options for Texinfo output ------------------------------------------- 316 | 317 | # Grouping the document tree into Texinfo files. List of tuples 318 | # (source start file, target name, title, author, 319 | # dir menu entry, description, category) 320 | texinfo_documents = [ 321 | (master_doc, 'MazeRun', 'MazeRun Documentation', 322 | author, 'MazeRun', 'One line description of project.', 323 | 'Miscellaneous'), 324 | ] 325 | 326 | # Documents to append as an appendix to all manuals. 327 | # 328 | # texinfo_appendices = [] 329 | 330 | # If false, no module index is generated. 331 | # 332 | # texinfo_domain_indices = True 333 | 334 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 335 | # 336 | # texinfo_show_urls = 'footnote' 337 | 338 | # If true, do not generate a @detailmenu in the "Top" node's menu. 339 | # 340 | # texinfo_no_detailmenu = False 341 | 342 | 343 | # -- Options for Epub output ---------------------------------------------- 344 | 345 | # Bibliographic Dublin Core info. 346 | epub_title = project 347 | epub_author = author 348 | epub_publisher = author 349 | epub_copyright = copyright 350 | 351 | # The basename for the epub file. It defaults to the project name. 352 | # epub_basename = project 353 | 354 | # The HTML theme for the epub output. Since the default themes are not 355 | # optimized for small screen space, using the same theme for HTML and epub 356 | # output is usually not wise. This defaults to 'epub', a theme designed to save 357 | # visual space. 358 | # 359 | # epub_theme = 'epub' 360 | 361 | # The language of the text. It defaults to the language option 362 | # or 'en' if the language is not set. 363 | # 364 | # epub_language = '' 365 | 366 | # The scheme of the identifier. Typical schemes are ISBN or URL. 367 | # epub_scheme = '' 368 | 369 | # The unique identifier of the text. This can be a ISBN number 370 | # or the project homepage. 371 | # 372 | # epub_identifier = '' 373 | 374 | # A unique identification for the text. 375 | # 376 | # epub_uid = '' 377 | 378 | # A tuple containing the cover image and cover page html template filenames. 379 | # 380 | # epub_cover = () 381 | 382 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 383 | # 384 | # epub_guide = () 385 | 386 | # HTML files that should be inserted before the pages created by sphinx. 387 | # The format is a list of tuples containing the path and title. 388 | # 389 | # epub_pre_files = [] 390 | 391 | # HTML files that should be inserted after the pages created by sphinx. 392 | # The format is a list of tuples containing the path and title. 393 | # 394 | # epub_post_files = [] 395 | 396 | # A list of files that should not be packed into the epub file. 397 | epub_exclude_files = ['search.html'] 398 | 399 | # The depth of the table of contents in toc.ncx. 400 | # 401 | # epub_tocdepth = 3 402 | 403 | # Allow duplicate toc entries. 404 | # 405 | # epub_tocdup = True 406 | 407 | # Choose between 'default' and 'includehidden'. 408 | # 409 | # epub_tocscope = 'default' 410 | 411 | # Fix unsupported image types using the Pillow. 412 | # 413 | # epub_fix_images = False 414 | 415 | # Scale large images. 416 | # 417 | # epub_max_image_width = 0 418 | 419 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 420 | # 421 | # epub_show_urls = 'inline' 422 | 423 | # If false, no index is generated. 424 | # 425 | # epub_use_index = True 426 | 427 | size = "long" 428 | 429 | def setup(app): 430 | app.add_config_value('size', '', 'env') 431 | 432 | -------------------------------------------------------------------------------- /17_documentation/event_loop.rst: -------------------------------------------------------------------------------- 1 | 2 | event loop 3 | ---------- 4 | 5 | -------------------------------------------------------------------------------- /17_documentation/iftex.sty: -------------------------------------------------------------------------------- 1 | %% 2 | %% This is file `iftex.sty', 3 | 4 | %% 5 | %% __________________________________ 6 | %% Copyright © 2010–2013 Persian TeX Group 7 | %% 8 | %% License information appended. 9 | %% 10 | %% 11 | \csname iftexloaded\endcsname 12 | \let\iftexloaded\endinput 13 | \expandafter\ifx\csname ProvidesPackage\endcsname\relax\else 14 | \ProvidesPackage{iftex} 15 | [2013/04/04 v0.2 Provides if(tex) conditional for PDFTeX, XeTeX, and LuaTeX] 16 | \fi 17 | \def\RequirePDFTeX{% 18 | \ifPDFTeX\else 19 | \begingroup 20 | \errorcontextlines=-1\relax 21 | \newlinechar=10\relax 22 | \errmessage{^^J 23 | ********************************************^^J 24 | * PDFTeX is required to compile this document.^^J 25 | * Sorry!^^J 26 | ********************************************}% 27 | \endgroup 28 | \fi} 29 | \def\RequireXeTeX{% 30 | \ifXeTeX\else 31 | \begingroup 32 | \errorcontextlines=-1\relax 33 | \newlinechar=10\relax 34 | \errmessage{^^J 35 | ********************************************^^J 36 | * XeTeX is required to compile this document.^^J 37 | * Sorry!^^J 38 | ********************************************}% 39 | \endgroup 40 | \fi} 41 | \def\RequireLuaTeX{% 42 | \ifLuaTeX\else 43 | \begingroup 44 | \errorcontextlines=-1\relax 45 | \newlinechar=10\relax 46 | \errmessage{^^J 47 | ********************************************^^J 48 | * LuaTeX is required to compile this document.^^J 49 | * Sorry!^^J 50 | ********************************************}% 51 | \endgroup 52 | \fi} 53 | \expandafter\ifx\csname ifPDFTeX\endcsname\relax\else 54 | \expandafter\endinput 55 | \fi 56 | \expandafter\ifx\csname ifXeTeX\endcsname\relax\else 57 | \expandafter\endinput 58 | \fi 59 | \expandafter\ifx\csname ifLuaTeX\endcsname\relax\else 60 | \expandafter\endinput 61 | \fi 62 | \newif\ifPDFTeX 63 | \begingroup\expandafter\expandafter\expandafter\endgroup 64 | \expandafter\ifx\csname pdfmatch\endcsname\relax 65 | \PDFTeXfalse 66 | \else 67 | \PDFTeXtrue 68 | \fi 69 | \newif\ifXeTeX 70 | \begingroup\expandafter\expandafter\expandafter\endgroup 71 | \expandafter\ifx\csname XeTeXinterchartoks\endcsname\relax 72 | \XeTeXfalse 73 | \else 74 | \XeTeXtrue 75 | \fi 76 | \newif\ifLuaTeX 77 | \begingroup\expandafter\expandafter\expandafter\endgroup 78 | \expandafter\ifx\csname directlua\endcsname\relax 79 | \LuaTeXfalse 80 | \else 81 | \LuaTeXtrue 82 | \fi 83 | %% 84 | %% Copyright © 2010–2013 by Persian TeX Group 85 | %% 86 | %% Distributable under the LaTeX Project Public License, 87 | %% version 1.3c or higher (your choice). The latest version of 88 | %% this license is at: http://www.latex-project.org/lppl.txt 89 | %% 90 | %% This work is "maintained" (as per LPPL maintenance status) 91 | %% by Persian TeX Group. 92 | %% 93 | %% 94 | %% 95 | %% 96 | %% 97 | %% End of file `iftex.sty'. 98 | -------------------------------------------------------------------------------- /17_documentation/index.rst: -------------------------------------------------------------------------------- 1 | .. MazeRun documentation master file, created by 2 | sphinx-quickstart on Fri Jun 10 11:36:16 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to MazeRun's documentation! 7 | =================================== 8 | 9 | 10 | TODOs in the MazeRun project 11 | ++++++++++++++++++++++++++++ 12 | 13 | .. todolist:: 14 | 15 | 16 | .. ifconfig:: size == "long" 17 | 18 | TileGrid 19 | ++++++++ 20 | The MazeRun class is an example for a reusable object-oriented design in Python. 21 | It uses several magic methods and properties to make it easy to access the data. 22 | 23 | .. ifconfig:: size == "short" 24 | 25 | bla bla bla 26 | 27 | .. include('tile_grid.rst') 28 | 29 | 30 | Contents: 31 | 32 | .. toctree:: 33 | mazes.rst 34 | tile_grid.rst 35 | :maxdepth: 2 36 | 37 | 38 | Indices and tables 39 | ================== 40 | 41 | * :ref:`genindex` 42 | * :ref:`modindex` 43 | * :ref:`search` 44 | 45 | -------------------------------------------------------------------------------- /17_documentation/mazes.rst: -------------------------------------------------------------------------------- 1 | Maze Generation 2 | =============== 3 | 4 | Creating a maze from scratch 5 | ---------------------------- 6 | 7 | Use the ``generate_maze`` module: 8 | 9 | .. doctest:: 10 | 11 | >>> from maze_run.generate_maze import create_grid_string 12 | >>> dots = [(1,1), (1, 2), (2,2), (2,3), (3,3)] 13 | >>> maze = create_grid_string(dots, 5, 5) 14 | >>> print(maze.strip()) 15 | ##### 16 | #.### 17 | #..## 18 | ##..# 19 | ##### 20 | 21 | 22 | This is $\sum_i^n{MAAAATH}!$ 23 | 24 | .. todo:: 25 | 26 | include output for doctest 27 | 28 | .. ifconfig:: eggs > 900 29 | 30 | Hello hidden easter eggs! 31 | 32 | 33 | .. autofunction:: maze_run.generate_maze.create_maze 34 | 35 | Whole modules can be discovered this way: 36 | 37 | draw_map module 38 | +++++++++++++++ 39 | 40 | .. automodule:: maze_run.draw_maze 41 | :members: 42 | 43 | 44 | -------------------------------------------------------------------------------- /17_documentation/tile_grid.rst: -------------------------------------------------------------------------------- 1 | The Maze Generator 2 | ------------------ 3 | 4 | The module ``maze_run.generate_maze`` is responsible for generating mazes. It creates a **two-dimensional list** with a *fixed size* that can be used to create a graphical representation in Pygame. 5 | 6 | The maze consists of corridors represented by dots and walls represented by hashes. The algorithm leaves a circular path close to the border. 7 | 8 | Using the ``generate_maze`` module: 9 | 10 | .. doctest:: 11 | 12 | >>> from maze_run.generate_maze import create_maze 13 | >>> import random 14 | >>> random.seed(0) 15 | >>> maze = create_maze(14, 7) 16 | >>> print(maze.strip()) 17 | ############## 18 | #............# 19 | #.#.#..#.###.# 20 | #.#...##.#...# 21 | #...#....#..## 22 | #.#....#.....# 23 | ############## 24 | >>> len(maze) 25 | 105 26 | 27 | 28 | Example of a failing doctest 29 | ++++++++++++++++++++++++++++ 30 | 31 | .. doctest:: 32 | 33 | >>> 1 + 2 34 | 2 35 | 36 | Example of TODO entries 37 | +++++++++++++++++++++++ 38 | 39 | .. todo:: 40 | 41 | Adding caching to the TileGrid class could accelerate graphics a lot. 42 | 43 | .. todo:: 44 | 45 | Describe how to connect a moving element to a TileGrid. 46 | -------------------------------------------------------------------------------- /9781484222409.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/9781484222409.jpg -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/pro-python-best-practices/a480ba24a8d8c13a27af11b2024edebf2f9a0613/LICENSE.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Pro Python Best Practices*](http://www.apress.com/9781484222409) by Kristian Rother (Apress, 2017). 4 | 5 | ![Cover image](9781484222409.jpg) 6 | 7 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 8 | 9 | ## Releases 10 | 11 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 12 | 13 | ## Contributions 14 | 15 | See the file Contributing.md for more information on how you can contribute to this repository. 16 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! --------------------------------------------------------------------------------