├── .gitignore ├── LICENSE ├── README.md ├── res ├── android.txt ├── apple_types.gif ├── controls.gif ├── game_background.gif ├── highscores.txt ├── menu_bg.gif ├── menu_top.gif ├── objectives.gif ├── template.gif ├── tile.gif └── tree_top.gif └── scripts ├── aabb.py ├── apple.py ├── button.py ├── common.py ├── drawer.py ├── graphics.py ├── highscores.py ├── main.py ├── player.py ├── projectile.py ├── state_enum.py ├── state_gameover.py ├── state_menu.py ├── state_playing.py ├── tiles.py └── vector.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__ 3 | *.zip 4 | AndroidAppleDrop/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Matthew Hopson 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Apple Drop 2 | 3 | Demonstation: https://www.youtube.com/watch?v=e9HhxOSJWLs 4 | 5 | How it was made: https://www.youtube.com/watch?v=AX-Zfcz1AEs 6 | 7 | This is a game about an Android eating apples. It was created for a contest run by the computing dept. at my university. 8 | 9 | The rules were as follows: 10 | 11 | * I had to use `graphics.py` for the graphics. 12 | * I was not allowed to write my own classes (I presume because we were yet to be taught this, so they wanted to keep it fair) 13 | * I was not allowed to use non-standard modules such as `pygame` 14 | * This means only Python 3 standard modules could be used 15 | * The game had to either be about a bat and ball, or an Android eating apples (I went with the latter) 16 | 17 | To run the game, run from main.py 18 | 19 | ![GamePlaye](https://i.imgur.com/rCsVO5N.png) 20 | -------------------------------------------------------------------------------- /res/android.txt: -------------------------------------------------------------------------------- 1 | 0 20 2 | 0 50 3 | 10 50 4 | 10 60 5 | 15 60 6 | 15 75 7 | 25 75 8 | 25 60 9 | 35 60 10 | 35 75 11 | 45 75 12 | 45 60 13 | 50 60 14 | 50 50 15 | 60 50 16 | 60 20 -------------------------------------------------------------------------------- /res/apple_types.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/apple_types.gif -------------------------------------------------------------------------------- /res/controls.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/controls.gif -------------------------------------------------------------------------------- /res/game_background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/game_background.gif -------------------------------------------------------------------------------- /res/highscores.txt: -------------------------------------------------------------------------------- 1 | Hopson`12688 2 | Hopson`8525 3 | Hopson`8492 4 | Hopson`7038 5 | Hopson`6762 6 | Hopson`6478 7 | -------------------------------------------------------------------------------- /res/menu_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/menu_bg.gif -------------------------------------------------------------------------------- /res/menu_top.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/menu_top.gif -------------------------------------------------------------------------------- /res/objectives.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/objectives.gif -------------------------------------------------------------------------------- /res/template.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/template.gif -------------------------------------------------------------------------------- /res/tile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/tile.gif -------------------------------------------------------------------------------- /res/tree_top.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hopson97/Android-Apple-Drop/643999a5dee8caacc8225b358067f5fff23228b9/res/tree_top.gif -------------------------------------------------------------------------------- /scripts/aabb.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def create(x, y, w, h): 4 | ''' 5 | x is the X coordinate 6 | y is the Y coordinate 7 | w is the width 8 | h is the height 9 | ''' 10 | return { 11 | "x": x, 12 | "y": y, 13 | "w": w, 14 | "h": h 15 | } 16 | 17 | def isPointInAABB(point, aabb): 18 | px = point.getX() 19 | py = point.getY() 20 | 21 | minX = aabb["x"] 22 | minY = aabb["y"] 23 | maxX = aabb["x"] + aabb["w"] 24 | maxY = aabb["y"] + aabb["h"] 25 | 26 | return px > minX and px < maxX and py > minY and py < maxY -------------------------------------------------------------------------------- /scripts/apple.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | 3 | import random 4 | import math 5 | 6 | import tiles 7 | from common import WINDOW_WIDTH, WINDOW_HEIGHT 8 | 9 | ''' 10 | APPLE TYPES: 11 | DEFAULT -> Just a normal apple 12 | REPAIR -> Repairs all the tiles 13 | BOOST -> Increases lifes by 1 14 | ''' 15 | 16 | #Default apple attributes 17 | _APPLE_SPEED = 1.5 18 | _RADIUS = 15 19 | DIAMETER = _RADIUS * 2 20 | 21 | #Enum for the different apple types, + also their radius 22 | DEFAULT = _RADIUS 23 | REPAIR = _RADIUS - 1 24 | BOOST = _RADIUS - 2 25 | APPLPOCALYPSE = _RADIUS - 3 26 | 27 | #List of different apple types 28 | _RADIUS_TYPES = [DEFAULT, REPAIR, BOOST, APPLPOCALYPSE] 29 | _APPLE_COLOURS = ["red", "green", "yellow", "pink"] 30 | 31 | #The enum for the apple type, also doubling as the index for the lists above 32 | _DEFAULT_APPLE = 0 33 | _REPAIR_APPLE = 1 34 | _BOOST_APPLE = 2 35 | _APLPOCALYPSE_APPLE = 3 36 | 37 | def getRandomAppleType(): 38 | '''Gets a random apple type enum''' 39 | appleType = random.randint(0, 170) 40 | if appleType > 25: 41 | return _DEFAULT_APPLE 42 | elif appleType > 10: 43 | return _REPAIR_APPLE 44 | elif appleType > 4: 45 | return _BOOST_APPLE 46 | else: 47 | return _APLPOCALYPSE_APPLE 48 | 49 | def getRandomAppleInfo(): 50 | '''get random radius and colour for apples''' 51 | appleType = getRandomAppleType() 52 | radius = _RADIUS_TYPES [appleType] 53 | colour = _APPLE_COLOURS[appleType] 54 | return radius, colour 55 | 56 | def makeAppleSprite(x, y, colour, radius, window): 57 | '''Creates a single apple''' 58 | apple = gfx.Circle(gfx.Point(x, y), radius) 59 | apple.draw(window) 60 | apple.setFill(colour) 61 | apple.setOutline("black") 62 | return apple 63 | 64 | def makeDefaultApple(x, y, window): 65 | '''Creates a basic red apple''' 66 | return makeAppleSprite(x, y, "red", _RADIUS, window) 67 | 68 | def getRandomAppleXPosition(): 69 | '''Gets an X position that is center to a tile''' 70 | x = random.randint(DIAMETER, WINDOW_WIDTH - _RADIUS + 1) 71 | while (x + 25) % tiles.TILE_SIZE != 0: 72 | x -= 1 73 | return x - 2 74 | 75 | def createRandomApple(window): 76 | '''Creates an random apple at top of window''' 77 | x = getRandomAppleXPosition() 78 | y = random.randint(-DIAMETER * 15, 0) 79 | radius, colour = getRandomAppleInfo() 80 | return makeAppleSprite(x, y, colour, radius, window) 81 | 82 | 83 | def isCollidingTile(apple, isTileActive, tileSprites): 84 | '''Test if apple is colliding with a tile, remove tile if it is''' 85 | y = apple.getCenter().getY() - _RADIUS 86 | x = apple.getCenter().getX() 87 | tileIndex = round((x) / tiles.TILE_SIZE) 88 | 89 | if y >= tiles.BASE_HEIGHT and isTileActive[tileIndex]: 90 | isTileActive[tileIndex] = False 91 | tileSprites [tileIndex].undraw() 92 | return True 93 | 94 | def isOffScreen(apple): 95 | '''Tests if the apple is out of the window bounds''' 96 | return apple.getCenter().getY() > WINDOW_HEIGHT + _RADIUS 97 | 98 | def moveApple(apple): 99 | apple.move(0, _APPLE_SPEED) 100 | 101 | def removeApple(apples, apple): 102 | apple.undraw() 103 | apples.remove(apple) -------------------------------------------------------------------------------- /scripts/button.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | 3 | import aabb 4 | import common 5 | 6 | #Constant values for typical centered menu 7 | WIDTH = int(common.WINDOW_WIDTH / 4) 8 | HEIGHT = int(common.WINDOW_HEIGHT / 10) 9 | LEFT = int(common.WINDOW_WIDTH / 2 - WIDTH / 2) 10 | 11 | def create(y, text, window): 12 | x1 = LEFT 13 | y1 = y 14 | x2 = LEFT + WIDTH 15 | y2 = y + HEIGHT 16 | btnSprite = gfx.Rectangle(gfx.Point(x1, y1), gfx.Point(x2, y2)) 17 | btnText = gfx.Text (gfx.Point(x1 + WIDTH / 2, y1 + HEIGHT / 2), text) 18 | 19 | btnSprite.setFill("gray") 20 | btnSprite.draw(window) 21 | btnText .draw(window) 22 | 23 | return [btnSprite, btnText], aabb.create(x1, y1, WIDTH, HEIGHT) 24 | 25 | def isButtonPressed(point, bounds, window): 26 | if point is not None: 27 | return aabb.isPointInAABB(point, bounds) 28 | return False 29 | 30 | def undraw(sprite, text): 31 | sprite.undraw() 32 | text.undraw() 33 | 34 | -------------------------------------------------------------------------------- /scripts/common.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is for functions which are generally going to be common amongst multiple files 3 | ''' 4 | import graphics as gfx 5 | 6 | import state_enum as states 7 | import drawer 8 | 9 | import math 10 | import time 11 | 12 | #Speed at which the game updates (Frames/updates per second) 13 | UPDATE_SPEED = 30 14 | 15 | WINDOW_WIDTH = 1100 16 | WINDOW_HEIGHT = 720 17 | 18 | WINDOW_CENTER_X = WINDOW_WIDTH // 2 19 | WINDOW_CENTER_Y = WINDOW_HEIGHT // 2 20 | 21 | GAME_NAME = "Android Apple Drop" 22 | 23 | def getKeyPress(window): 24 | '''Gets a key press from the window''' 25 | if window.closed: 26 | return "" 27 | return window.checkKey() 28 | 29 | def switchState(window, control, newState): 30 | '''Changes the current game state''' 31 | if newState != states.EXIT: 32 | drawer.undrawAll(window) 33 | #window.items.clear() 34 | control["state"] = newState 35 | 36 | def shouldExit(window, control): 37 | '''Checks if the window is shut''' 38 | if window.closed: 39 | switchState(window, control, states.EXIT) 40 | return True 41 | return False 42 | 43 | def calculateTime(start): 44 | '''Calculates the time since program start''' 45 | now = time.time() 46 | return now - start 47 | 48 | 49 | def createTitle(text, window = None, colour = "orange", x = WINDOW_WIDTH / 2, y = WINDOW_HEIGHT / 12, size = 36): 50 | '''Creates a big text at the top of the window, pass window in for auto drawing''' 51 | titleText = gfx.Text(gfx.Point(x, y), text) 52 | titleText.setSize(size) 53 | titleText.setFill(colour) 54 | titleText.setStyle("bold") 55 | if window is not None: 56 | titleText.draw(window) 57 | return titleText 58 | 59 | def createCenteredImage(name): 60 | '''Loads up an image, and places it at centre of window''' 61 | fullName = "../res/" + name + ".gif" 62 | return gfx.Image(gfx.Point(WINDOW_CENTER_X, WINDOW_CENTER_Y), fullName) -------------------------------------------------------------------------------- /scripts/drawer.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | 3 | def loadSpriteVerticies(fileName): 4 | '''Loads up vertex data from a file, and returns it as a list of points''' 5 | with open("../res/" + fileName + ".txt") as inFile: 6 | data = inFile.read() 7 | data = data.split() 8 | data = list(map(int, data)) #convert strig list to list of integers 9 | 10 | points = [] 11 | for i in range(0, len(data), 2): 12 | x = data[i] 13 | y = data[i + 1] 14 | points.append(gfx.Point(x, y)) 15 | return points 16 | 17 | def undrawAll(window): 18 | '''Undraws everything stored in the window item list''' 19 | for item in window.items: 20 | item.undraw() 21 | 22 | def undrawList(spriteList): 23 | '''Undraws all graphics objects that are stored in a list''' 24 | for sprite in spriteList: 25 | sprite.undraw() 26 | 27 | def drawList(sprites, window): 28 | '''Draws a list of sprites''' 29 | for sprite in sprites: 30 | sprite.draw(window) 31 | 32 | def redrawSprite(sprite, window): 33 | sprite.undraw() 34 | sprite.draw(window) 35 | 36 | def redrawList(sprites, window): 37 | '''Puts a list of sprites on-front''' 38 | for sprite in sprites: 39 | redrawSprite(sprite, window) -------------------------------------------------------------------------------- /scripts/graphics.py: -------------------------------------------------------------------------------- 1 | # graphics.py 2 | """Simple object oriented graphics library 3 | 4 | The library is designed to make it very easy for novice programmers to 5 | experiment with computer graphics in an object oriented fashion. It is 6 | written by John Zelle for use with the book "Python Programming: An 7 | Introduction to Computer Science" (Franklin, Beedle & Associates). 8 | 9 | LICENSE: This is open-source software released under the terms of the 10 | GPL (http://www.gnu.org/licenses/gpl.html). 11 | 12 | PLATFORMS: The package is a wrapper around Tkinter and should run on 13 | any platform where Tkinter is available. 14 | 15 | INSTALLATION: Put this file somewhere where Python can see it. 16 | 17 | OVERVIEW: There are two kinds of objects in the library. The GraphWin 18 | class implements a window where drawing can be done and various 19 | GraphicsObjects are provided that can be drawn into a GraphWin. As a 20 | simple example, here is a complete program to draw a circle of radius 21 | 10 centered in a 100x100 window: 22 | 23 | -------------------------------------------------------------------- 24 | from graphics import * 25 | 26 | def main(): 27 | win = GraphWin("My Circle", 100, 100) 28 | c = Circle(Point(50,50), 10) 29 | c.draw(win) 30 | win.getMouse() # Pause to view result 31 | win.close() # Close window when done 32 | 33 | main() 34 | -------------------------------------------------------------------- 35 | GraphWin objects support coordinate transformation through the 36 | setCoords method and mouse and keyboard interaction methods. 37 | 38 | The library provides the following graphical objects: 39 | Point 40 | Line 41 | Circle 42 | Oval 43 | Rectangle 44 | Polygon 45 | Text 46 | Entry (for text-based input) 47 | Image 48 | 49 | Various attributes of graphical objects can be set such as 50 | outline-color, fill-color and line-width. Graphical objects also 51 | support moving and hiding for animation effects. 52 | 53 | The library also provides a very simple class for pixel-based image 54 | manipulation, Pixmap. A pixmap can be loaded from a file and displayed 55 | using an Image object. Both getPixel and setPixel methods are provided 56 | for manipulating the image. 57 | 58 | DOCUMENTATION: For complete documentation, see Chapter 4 of "Python 59 | Programming: An Introduction to Computer Science" by John Zelle, 60 | published by Franklin, Beedle & Associates. Also see 61 | http://mcsp.wartburg.edu/zelle/python for a quick reference""" 62 | 63 | __version__ = "5.0" 64 | 65 | # Version 5 8/26/2016 66 | # * update at bottom to fix MacOS issue causing askopenfile() to hang 67 | # * update takes an optional parameter specifying update rate 68 | # * Entry objects get focus when drawn 69 | # * __repr_ for all objects 70 | # * fixed offset problem in window, made canvas borderless 71 | 72 | # Version 4.3 4/25/2014 73 | # * Fixed Image getPixel to work with Python 3.4, TK 8.6 (tuple type handling) 74 | # * Added interactive keyboard input (getKey and checkKey) to GraphWin 75 | # * Modified setCoords to cause redraw of current objects, thus 76 | # changing the view. This supports scrolling around via setCoords. 77 | # 78 | # Version 4.2 5/26/2011 79 | # * Modified Image to allow multiple undraws like other GraphicsObjects 80 | # Version 4.1 12/29/2009 81 | # * Merged Pixmap and Image class. Old Pixmap removed, use Image. 82 | # Version 4.0.1 10/08/2009 83 | # * Modified the autoflush on GraphWin to default to True 84 | # * Autoflush check on close, setBackground 85 | # * Fixed getMouse to flush pending clicks at entry 86 | # Version 4.0 08/2009 87 | # * Reverted to non-threaded version. The advantages (robustness, 88 | # efficiency, ability to use with other Tk code, etc.) outweigh 89 | # the disadvantage that interactive use with IDLE is slightly more 90 | # cumbersome. 91 | # * Modified to run in either Python 2.x or 3.x (same file). 92 | # * Added Image.getPixmap() 93 | # * Added update() -- stand alone function to cause any pending 94 | # graphics changes to display. 95 | # 96 | # Version 3.4 10/16/07 97 | # Fixed GraphicsError to avoid "exploded" error messages. 98 | # Version 3.3 8/8/06 99 | # Added checkMouse method to GraphWin 100 | # Version 3.2.3 101 | # Fixed error in Polygon init spotted by Andrew Harrington 102 | # Fixed improper threading in Image constructor 103 | # Version 3.2.2 5/30/05 104 | # Cleaned up handling of exceptions in Tk thread. The graphics package 105 | # now raises an exception if attempt is made to communicate with 106 | # a dead Tk thread. 107 | # Version 3.2.1 5/22/05 108 | # Added shutdown function for tk thread to eliminate race-condition 109 | # error "chatter" when main thread terminates 110 | # Renamed various private globals with _ 111 | # Version 3.2 5/4/05 112 | # Added Pixmap object for simple image manipulation. 113 | # Version 3.1 4/13/05 114 | # Improved the Tk thread communication so that most Tk calls 115 | # do not have to wait for synchonization with the Tk thread. 116 | # (see _tkCall and _tkExec) 117 | # Version 3.0 12/30/04 118 | # Implemented Tk event loop in separate thread. Should now work 119 | # interactively with IDLE. Undocumented autoflush feature is 120 | # no longer necessary. Its default is now False (off). It may 121 | # be removed in a future version. 122 | # Better handling of errors regarding operations on windows that 123 | # have been closed. 124 | # Addition of an isClosed method to GraphWindow class. 125 | 126 | # Version 2.2 8/26/04 127 | # Fixed cloning bug reported by Joseph Oldham. 128 | # Now implements deep copy of config info. 129 | # Version 2.1 1/15/04 130 | # Added autoflush option to GraphWin. When True (default) updates on 131 | # the window are done after each action. This makes some graphics 132 | # intensive programs sluggish. Turning off autoflush causes updates 133 | # to happen during idle periods or when flush is called. 134 | # Version 2.0 135 | # Updated Documentation 136 | # Made Polygon accept a list of Points in constructor 137 | # Made all drawing functions call TK update for easier animations 138 | # and to make the overall package work better with 139 | # Python 2.3 and IDLE 1.0 under Windows (still some issues). 140 | # Removed vestigial turtle graphics. 141 | # Added ability to configure font for Entry objects (analogous to Text) 142 | # Added setTextColor for Text as an alias of setFill 143 | # Changed to class-style exceptions 144 | # Fixed cloning of Text objects 145 | 146 | # Version 1.6 147 | # Fixed Entry so StringVar uses _root as master, solves weird 148 | # interaction with shell in Idle 149 | # Fixed bug in setCoords. X and Y coordinates can increase in 150 | # "non-intuitive" direction. 151 | # Tweaked wm_protocol so window is not resizable and kill box closes. 152 | 153 | # Version 1.5 154 | # Fixed bug in Entry. Can now define entry before creating a 155 | # GraphWin. All GraphWins are now toplevel windows and share 156 | # a fixed root (called _root). 157 | 158 | # Version 1.4 159 | # Fixed Garbage collection of Tkinter images bug. 160 | # Added ability to set text atttributes. 161 | # Added Entry boxes. 162 | 163 | import time, os, sys 164 | 165 | try: # import as appropriate for 2.x vs. 3.x 166 | import tkinter as tk 167 | except: 168 | import Tkinter as tk 169 | 170 | 171 | ########################################################################## 172 | # Module Exceptions 173 | 174 | class GraphicsError(Exception): 175 | """Generic error class for graphics module exceptions.""" 176 | pass 177 | 178 | OBJ_ALREADY_DRAWN = "Object currently drawn" 179 | UNSUPPORTED_METHOD = "Object doesn't support operation" 180 | BAD_OPTION = "Illegal option value" 181 | 182 | ########################################################################## 183 | # global variables and funtions 184 | 185 | _root = tk.Tk() 186 | _root.withdraw() 187 | 188 | _update_lasttime = time.time() 189 | 190 | def update(rate=None): 191 | global _update_lasttime 192 | if rate: 193 | now = time.time() 194 | pauseLength = 1/rate-(now-_update_lasttime) 195 | if pauseLength > 0: 196 | time.sleep(pauseLength) 197 | _update_lasttime = now + pauseLength 198 | else: 199 | _update_lasttime = now 200 | 201 | _root.update() 202 | 203 | ############################################################################ 204 | # Graphics classes start here 205 | 206 | class GraphWin(tk.Canvas): 207 | 208 | """A GraphWin is a toplevel window for displaying graphics.""" 209 | 210 | def __init__(self, title="Graphics Window", 211 | width=200, height=200, autoflush=True): 212 | assert type(title) == type(""), "Title must be a string" 213 | master = tk.Toplevel(_root) 214 | master.protocol("WM_DELETE_WINDOW", self.close) 215 | tk.Canvas.__init__(self, master, width=width, height=height, 216 | highlightthickness=0, bd=0) 217 | self.master.title(title) 218 | self.pack() 219 | master.resizable(0,0) 220 | self.foreground = "black" 221 | self.items = [] 222 | self.mouseX = None 223 | self.mouseY = None 224 | self.bind("", self._onClick) 225 | self.bind_all("", self._onKey) 226 | self.height = int(height) 227 | self.width = int(width) 228 | self.autoflush = autoflush 229 | self._mouseCallback = None 230 | self.trans = None 231 | self.closed = False 232 | master.lift() 233 | self.lastKey = "" 234 | if autoflush: _root.update() 235 | 236 | def __repr__(self): 237 | if self.isClosed(): 238 | return "" 239 | else: 240 | return "GraphWin('{}', {}, {})".format(self.master.title(), 241 | self.getWidth(), 242 | self.getHeight()) 243 | 244 | def __str__(self): 245 | return repr(self) 246 | 247 | def __checkOpen(self): 248 | if self.closed: 249 | raise GraphicsError("window is closed") 250 | 251 | def _onKey(self, evnt): 252 | self.lastKey = evnt.keysym 253 | 254 | 255 | def setBackground(self, color): 256 | """Set background color of the window""" 257 | self.__checkOpen() 258 | self.config(bg=color) 259 | self.__autoflush() 260 | 261 | def setCoords(self, x1, y1, x2, y2): 262 | """Set coordinates of window to run from (x1,y1) in the 263 | lower-left corner to (x2,y2) in the upper-right corner.""" 264 | self.trans = Transform(self.width, self.height, x1, y1, x2, y2) 265 | self.redraw() 266 | 267 | def close(self): 268 | """Close the window""" 269 | 270 | if self.closed: return 271 | self.closed = True 272 | self.master.destroy() 273 | self.__autoflush() 274 | 275 | 276 | def isClosed(self): 277 | return self.closed 278 | 279 | 280 | def isOpen(self): 281 | return not self.closed 282 | 283 | 284 | def __autoflush(self): 285 | if self.autoflush: 286 | _root.update() 287 | 288 | 289 | def plot(self, x, y, color="black"): 290 | """Set pixel (x,y) to the given color""" 291 | self.__checkOpen() 292 | xs,ys = self.toScreen(x,y) 293 | self.create_line(xs,ys,xs+1,ys, fill=color) 294 | self.__autoflush() 295 | 296 | def plotPixel(self, x, y, color="black"): 297 | """Set pixel raw (independent of window coordinates) pixel 298 | (x,y) to color""" 299 | self.__checkOpen() 300 | self.create_line(x,y,x+1,y, fill=color) 301 | self.__autoflush() 302 | 303 | def flush(self): 304 | """Update drawing to the window""" 305 | self.__checkOpen() 306 | self.update_idletasks() 307 | 308 | def getMouse(self): 309 | """Wait for mouse click and return Point object representing 310 | the click""" 311 | self.update() # flush any prior clicks 312 | self.mouseX = None 313 | self.mouseY = None 314 | while self.mouseX == None or self.mouseY == None: 315 | self.update() 316 | if self.isClosed(): raise GraphicsError("getMouse in closed window") 317 | time.sleep(.1) # give up thread 318 | x,y = self.toWorld(self.mouseX, self.mouseY) 319 | self.mouseX = None 320 | self.mouseY = None 321 | return Point(x,y) 322 | 323 | def checkMouse(self): 324 | """Return last mouse click or None if mouse has 325 | not been clicked since last call""" 326 | if self.isClosed(): 327 | raise GraphicsError("checkMouse in closed window") 328 | self.update() 329 | if self.mouseX != None and self.mouseY != None: 330 | x,y = self.toWorld(self.mouseX, self.mouseY) 331 | self.mouseX = None 332 | self.mouseY = None 333 | return Point(x,y) 334 | else: 335 | return None 336 | 337 | def getKey(self): 338 | """Wait for user to press a key and return it as a string.""" 339 | self.lastKey = "" 340 | while self.lastKey == "": 341 | self.update() 342 | if self.isClosed(): raise GraphicsError("getKey in closed window") 343 | time.sleep(.1) # give up thread 344 | 345 | key = self.lastKey 346 | self.lastKey = "" 347 | return key 348 | 349 | def checkKey(self): 350 | """Return last key pressed or None if no key pressed since last call""" 351 | if self.isClosed(): 352 | raise GraphicsError("checkKey in closed window") 353 | self.update() 354 | key = self.lastKey 355 | self.lastKey = "" 356 | return key 357 | 358 | def getHeight(self): 359 | """Return the height of the window""" 360 | return self.height 361 | 362 | def getWidth(self): 363 | """Return the width of the window""" 364 | return self.width 365 | 366 | def toScreen(self, x, y): 367 | trans = self.trans 368 | if trans: 369 | return self.trans.screen(x,y) 370 | else: 371 | return x,y 372 | 373 | def toWorld(self, x, y): 374 | trans = self.trans 375 | if trans: 376 | return self.trans.world(x,y) 377 | else: 378 | return x,y 379 | 380 | def setMouseHandler(self, func): 381 | self._mouseCallback = func 382 | 383 | def _onClick(self, e): 384 | self.mouseX = e.x 385 | self.mouseY = e.y 386 | if self._mouseCallback: 387 | self._mouseCallback(Point(e.x, e.y)) 388 | 389 | def addItem(self, item): 390 | self.items.append(item) 391 | 392 | def delItem(self, item): 393 | self.items.remove(item) 394 | 395 | def redraw(self): 396 | for item in self.items[:]: 397 | item.undraw() 398 | item.draw(self) 399 | self.update() 400 | 401 | 402 | class Transform: 403 | 404 | """Internal class for 2-D coordinate transformations""" 405 | 406 | def __init__(self, w, h, xlow, ylow, xhigh, yhigh): 407 | # w, h are width and height of window 408 | # (xlow,ylow) coordinates of lower-left [raw (0,h-1)] 409 | # (xhigh,yhigh) coordinates of upper-right [raw (w-1,0)] 410 | xspan = (xhigh-xlow) 411 | yspan = (yhigh-ylow) 412 | self.xbase = xlow 413 | self.ybase = yhigh 414 | self.xscale = xspan/float(w-1) 415 | self.yscale = yspan/float(h-1) 416 | 417 | def screen(self,x,y): 418 | # Returns x,y in screen (actually window) coordinates 419 | xs = (x-self.xbase) / self.xscale 420 | ys = (self.ybase-y) / self.yscale 421 | return int(xs+0.5),int(ys+0.5) 422 | 423 | def world(self,xs,ys): 424 | # Returns xs,ys in world coordinates 425 | x = xs*self.xscale + self.xbase 426 | y = self.ybase - ys*self.yscale 427 | return x,y 428 | 429 | 430 | # Default values for various item configuration options. Only a subset of 431 | # keys may be present in the configuration dictionary for a given item 432 | DEFAULT_CONFIG = {"fill":"", 433 | "outline":"black", 434 | "width":"1", 435 | "arrow":"none", 436 | "text":"", 437 | "justify":"center", 438 | "font": ("helvetica", 12, "normal")} 439 | 440 | class GraphicsObject: 441 | 442 | """Generic base class for all of the drawable objects""" 443 | # A subclass of GraphicsObject should override _draw and 444 | # and _move methods. 445 | 446 | def __init__(self, options): 447 | # options is a list of strings indicating which options are 448 | # legal for this object. 449 | 450 | # When an object is drawn, canvas is set to the GraphWin(canvas) 451 | # object where it is drawn and id is the TK identifier of the 452 | # drawn shape. 453 | self.canvas = None 454 | self.id = None 455 | 456 | # config is the dictionary of configuration options for the widget. 457 | config = {} 458 | for option in options: 459 | config[option] = DEFAULT_CONFIG[option] 460 | self.config = config 461 | 462 | def setFill(self, color): 463 | """Set interior color to color""" 464 | self._reconfig("fill", color) 465 | 466 | def setOutline(self, color): 467 | """Set outline color to color""" 468 | self._reconfig("outline", color) 469 | 470 | def setWidth(self, width): 471 | """Set line weight to width""" 472 | self._reconfig("width", width) 473 | 474 | def draw(self, graphwin): 475 | 476 | """Draw the object in graphwin, which should be a GraphWin 477 | object. A GraphicsObject may only be drawn into one 478 | window. Raises an error if attempt made to draw an object that 479 | is already visible.""" 480 | 481 | if self.canvas and not self.canvas.isClosed(): raise GraphicsError(OBJ_ALREADY_DRAWN) 482 | if graphwin.isClosed(): raise GraphicsError("Can't draw to closed window") 483 | self.canvas = graphwin 484 | self.id = self._draw(graphwin, self.config) 485 | graphwin.addItem(self) 486 | if graphwin.autoflush: 487 | _root.update() 488 | return self 489 | 490 | 491 | def undraw(self): 492 | 493 | """Undraw the object (i.e. hide it). Returns silently if the 494 | object is not currently drawn.""" 495 | 496 | if not self.canvas: return 497 | if not self.canvas.isClosed(): 498 | self.canvas.delete(self.id) 499 | self.canvas.delItem(self) 500 | if self.canvas.autoflush: 501 | _root.update() 502 | self.canvas = None 503 | self.id = None 504 | 505 | 506 | def move(self, dx, dy): 507 | 508 | """move object dx units in x direction and dy units in y 509 | direction""" 510 | 511 | self._move(dx,dy) 512 | canvas = self.canvas 513 | if canvas and not canvas.isClosed(): 514 | trans = canvas.trans 515 | if trans: 516 | x = dx/ trans.xscale 517 | y = -dy / trans.yscale 518 | else: 519 | x = dx 520 | y = dy 521 | self.canvas.move(self.id, x, y) 522 | if canvas.autoflush: 523 | _root.update() 524 | 525 | def _reconfig(self, option, setting): 526 | # Internal method for changing configuration of the object 527 | # Raises an error if the option does not exist in the config 528 | # dictionary for this object 529 | if option not in self.config: 530 | raise GraphicsError(UNSUPPORTED_METHOD) 531 | options = self.config 532 | options[option] = setting 533 | if self.canvas and not self.canvas.isClosed(): 534 | self.canvas.itemconfig(self.id, options) 535 | if self.canvas.autoflush: 536 | _root.update() 537 | 538 | 539 | def _draw(self, canvas, options): 540 | """draws appropriate figure on canvas with options provided 541 | Returns Tk id of item drawn""" 542 | pass # must override in subclass 543 | 544 | 545 | def _move(self, dx, dy): 546 | """updates internal state of object to move it dx,dy units""" 547 | pass # must override in subclass 548 | 549 | 550 | class Point(GraphicsObject): 551 | def __init__(self, x, y): 552 | GraphicsObject.__init__(self, ["outline", "fill"]) 553 | self.setFill = self.setOutline 554 | self.x = float(x) 555 | self.y = float(y) 556 | 557 | def __repr__(self): 558 | return "Point({}, {})".format(self.x, self.y) 559 | 560 | def _draw(self, canvas, options): 561 | x,y = canvas.toScreen(self.x,self.y) 562 | return canvas.create_rectangle(x,y,x+1,y+1,options) 563 | 564 | def _move(self, dx, dy): 565 | self.x = self.x + dx 566 | self.y = self.y + dy 567 | 568 | def clone(self): 569 | other = Point(self.x,self.y) 570 | other.config = self.config.copy() 571 | return other 572 | 573 | def getX(self): return self.x 574 | def getY(self): return self.y 575 | 576 | class _BBox(GraphicsObject): 577 | # Internal base class for objects represented by bounding box 578 | # (opposite corners) Line segment is a degenerate case. 579 | 580 | def __init__(self, p1, p2, options=["outline","width","fill"]): 581 | GraphicsObject.__init__(self, options) 582 | self.p1 = p1.clone() 583 | self.p2 = p2.clone() 584 | 585 | def _move(self, dx, dy): 586 | self.p1.x = self.p1.x + dx 587 | self.p1.y = self.p1.y + dy 588 | self.p2.x = self.p2.x + dx 589 | self.p2.y = self.p2.y + dy 590 | 591 | def getP1(self): return self.p1.clone() 592 | 593 | def getP2(self): return self.p2.clone() 594 | 595 | def getCenter(self): 596 | p1 = self.p1 597 | p2 = self.p2 598 | return Point((p1.x+p2.x)/2.0, (p1.y+p2.y)/2.0) 599 | 600 | 601 | class Rectangle(_BBox): 602 | 603 | def __init__(self, p1, p2): 604 | _BBox.__init__(self, p1, p2) 605 | 606 | def __repr__(self): 607 | return "Rectangle({}, {})".format(str(self.p1), str(self.p2)) 608 | 609 | def _draw(self, canvas, options): 610 | p1 = self.p1 611 | p2 = self.p2 612 | x1,y1 = canvas.toScreen(p1.x,p1.y) 613 | x2,y2 = canvas.toScreen(p2.x,p2.y) 614 | return canvas.create_rectangle(x1,y1,x2,y2,options) 615 | 616 | def clone(self): 617 | other = Rectangle(self.p1, self.p2) 618 | other.config = self.config.copy() 619 | return other 620 | 621 | 622 | class Oval(_BBox): 623 | 624 | def __init__(self, p1, p2): 625 | _BBox.__init__(self, p1, p2) 626 | 627 | def __repr__(self): 628 | return "Oval({}, {})".format(str(self.p1), str(self.p2)) 629 | 630 | 631 | def clone(self): 632 | other = Oval(self.p1, self.p2) 633 | other.config = self.config.copy() 634 | return other 635 | 636 | def _draw(self, canvas, options): 637 | p1 = self.p1 638 | p2 = self.p2 639 | x1,y1 = canvas.toScreen(p1.x,p1.y) 640 | x2,y2 = canvas.toScreen(p2.x,p2.y) 641 | return canvas.create_oval(x1,y1,x2,y2,options) 642 | 643 | class Circle(Oval): 644 | 645 | def __init__(self, center, radius): 646 | p1 = Point(center.x-radius, center.y-radius) 647 | p2 = Point(center.x+radius, center.y+radius) 648 | Oval.__init__(self, p1, p2) 649 | self.radius = radius 650 | 651 | def __repr__(self): 652 | return "Circle({}, {})".format(str(self.getCenter()), str(self.radius)) 653 | 654 | def clone(self): 655 | other = Circle(self.getCenter(), self.radius) 656 | other.config = self.config.copy() 657 | return other 658 | 659 | def getRadius(self): 660 | return self.radius 661 | 662 | 663 | class Line(_BBox): 664 | 665 | def __init__(self, p1, p2): 666 | _BBox.__init__(self, p1, p2, ["arrow","fill","width"]) 667 | self.setFill(DEFAULT_CONFIG['outline']) 668 | self.setOutline = self.setFill 669 | 670 | def __repr__(self): 671 | return "Line({}, {})".format(str(self.p1), str(self.p2)) 672 | 673 | def clone(self): 674 | other = Line(self.p1, self.p2) 675 | other.config = self.config.copy() 676 | return other 677 | 678 | def _draw(self, canvas, options): 679 | p1 = self.p1 680 | p2 = self.p2 681 | x1,y1 = canvas.toScreen(p1.x,p1.y) 682 | x2,y2 = canvas.toScreen(p2.x,p2.y) 683 | return canvas.create_line(x1,y1,x2,y2,options) 684 | 685 | def setArrow(self, option): 686 | if not option in ["first","last","both","none"]: 687 | raise GraphicsError(BAD_OPTION) 688 | self._reconfig("arrow", option) 689 | 690 | 691 | class Polygon(GraphicsObject): 692 | 693 | def __init__(self, *points): 694 | # if points passed as a list, extract it 695 | if len(points) == 1 and type(points[0]) == type([]): 696 | points = points[0] 697 | self.points = list(map(Point.clone, points)) 698 | GraphicsObject.__init__(self, ["outline", "width", "fill"]) 699 | 700 | def __repr__(self): 701 | return "Polygon"+str(tuple(p for p in self.points)) 702 | 703 | def clone(self): 704 | other = Polygon(*self.points) 705 | other.config = self.config.copy() 706 | return other 707 | 708 | def getPoints(self): 709 | return list(map(Point.clone, self.points)) 710 | 711 | def _move(self, dx, dy): 712 | for p in self.points: 713 | p.move(dx,dy) 714 | 715 | def _draw(self, canvas, options): 716 | args = [canvas] 717 | for p in self.points: 718 | x,y = canvas.toScreen(p.x,p.y) 719 | args.append(x) 720 | args.append(y) 721 | args.append(options) 722 | return GraphWin.create_polygon(*args) 723 | 724 | class Text(GraphicsObject): 725 | 726 | def __init__(self, p, text): 727 | GraphicsObject.__init__(self, ["justify","fill","text","font"]) 728 | self.setText(text) 729 | self.anchor = p.clone() 730 | self.setFill(DEFAULT_CONFIG['outline']) 731 | self.setOutline = self.setFill 732 | 733 | def __repr__(self): 734 | return "Text({}, '{}')".format(self.anchor, self.getText()) 735 | 736 | def _draw(self, canvas, options): 737 | p = self.anchor 738 | x,y = canvas.toScreen(p.x,p.y) 739 | return canvas.create_text(x,y,options) 740 | 741 | def _move(self, dx, dy): 742 | self.anchor.move(dx,dy) 743 | 744 | def clone(self): 745 | other = Text(self.anchor, self.config['text']) 746 | other.config = self.config.copy() 747 | return other 748 | 749 | def setText(self,text): 750 | self._reconfig("text", text) 751 | 752 | def getText(self): 753 | return self.config["text"] 754 | 755 | def getAnchor(self): 756 | return self.anchor.clone() 757 | 758 | def setFace(self, face): 759 | if face in ['helvetica','arial','courier','times roman']: 760 | f,s,b = self.config['font'] 761 | self._reconfig("font",(face,s,b)) 762 | else: 763 | raise GraphicsError(BAD_OPTION) 764 | 765 | def setSize(self, size): 766 | if 5 <= size <= 36: 767 | f,s,b = self.config['font'] 768 | self._reconfig("font", (f,size,b)) 769 | else: 770 | raise GraphicsError(BAD_OPTION) 771 | 772 | def setStyle(self, style): 773 | if style in ['bold','normal','italic', 'bold italic']: 774 | f,s,b = self.config['font'] 775 | self._reconfig("font", (f,s,style)) 776 | else: 777 | raise GraphicsError(BAD_OPTION) 778 | 779 | def setTextColor(self, color): 780 | self.setFill(color) 781 | 782 | 783 | class Entry(GraphicsObject): 784 | 785 | def __init__(self, p, width): 786 | GraphicsObject.__init__(self, []) 787 | self.anchor = p.clone() 788 | #print self.anchor 789 | self.width = width 790 | self.text = tk.StringVar(_root) 791 | self.text.set("") 792 | self.fill = "gray" 793 | self.color = "black" 794 | self.font = DEFAULT_CONFIG['font'] 795 | self.entry = None 796 | 797 | def __repr__(self): 798 | return "Entry({}, {})".format(self.anchor, self.width) 799 | 800 | def _draw(self, canvas, options): 801 | p = self.anchor 802 | x,y = canvas.toScreen(p.x,p.y) 803 | frm = tk.Frame(canvas.master) 804 | self.entry = tk.Entry(frm, 805 | width=self.width, 806 | textvariable=self.text, 807 | bg = self.fill, 808 | fg = self.color, 809 | font=self.font) 810 | self.entry.pack() 811 | #self.setFill(self.fill) 812 | self.entry.focus_set() 813 | return canvas.create_window(x,y,window=frm) 814 | 815 | def getText(self): 816 | return self.text.get() 817 | 818 | def _move(self, dx, dy): 819 | self.anchor.move(dx,dy) 820 | 821 | def getAnchor(self): 822 | return self.anchor.clone() 823 | 824 | def clone(self): 825 | other = Entry(self.anchor, self.width) 826 | other.config = self.config.copy() 827 | other.text = tk.StringVar() 828 | other.text.set(self.text.get()) 829 | other.fill = self.fill 830 | return other 831 | 832 | def setText(self, t): 833 | self.text.set(t) 834 | 835 | 836 | def setFill(self, color): 837 | self.fill = color 838 | if self.entry: 839 | self.entry.config(bg=color) 840 | 841 | 842 | def _setFontComponent(self, which, value): 843 | font = list(self.font) 844 | font[which] = value 845 | self.font = tuple(font) 846 | if self.entry: 847 | self.entry.config(font=self.font) 848 | 849 | 850 | def setFace(self, face): 851 | if face in ['helvetica','arial','courier','times roman']: 852 | self._setFontComponent(0, face) 853 | else: 854 | raise GraphicsError(BAD_OPTION) 855 | 856 | def setSize(self, size): 857 | if 5 <= size <= 36: 858 | self._setFontComponent(1,size) 859 | else: 860 | raise GraphicsError(BAD_OPTION) 861 | 862 | def setStyle(self, style): 863 | if style in ['bold','normal','italic', 'bold italic']: 864 | self._setFontComponent(2,style) 865 | else: 866 | raise GraphicsError(BAD_OPTION) 867 | 868 | def setTextColor(self, color): 869 | self.color=color 870 | if self.entry: 871 | self.entry.config(fg=color) 872 | 873 | 874 | class Image(GraphicsObject): 875 | 876 | idCount = 0 877 | imageCache = {} # tk photoimages go here to avoid GC while drawn 878 | 879 | def __init__(self, p, *pixmap): 880 | GraphicsObject.__init__(self, []) 881 | self.anchor = p.clone() 882 | self.imageId = Image.idCount 883 | Image.idCount = Image.idCount + 1 884 | if len(pixmap) == 1: # file name provided 885 | self.img = tk.PhotoImage(file=pixmap[0], master=_root) 886 | else: # width and height provided 887 | width, height = pixmap 888 | self.img = tk.PhotoImage(master=_root, width=width, height=height) 889 | 890 | def __repr__(self): 891 | return "Image({}, {}, {})".format(self.anchor, self.getWidth(), self.getHeight()) 892 | 893 | def _draw(self, canvas, options): 894 | p = self.anchor 895 | x,y = canvas.toScreen(p.x,p.y) 896 | self.imageCache[self.imageId] = self.img # save a reference 897 | return canvas.create_image(x,y,image=self.img) 898 | 899 | def _move(self, dx, dy): 900 | self.anchor.move(dx,dy) 901 | 902 | def undraw(self): 903 | try: 904 | del self.imageCache[self.imageId] # allow gc of tk photoimage 905 | except KeyError: 906 | pass 907 | GraphicsObject.undraw(self) 908 | 909 | def getAnchor(self): 910 | return self.anchor.clone() 911 | 912 | def clone(self): 913 | other = Image(Point(0,0), 0, 0) 914 | other.img = self.img.copy() 915 | other.anchor = self.anchor.clone() 916 | other.config = self.config.copy() 917 | return other 918 | 919 | def getWidth(self): 920 | """Returns the width of the image in pixels""" 921 | return self.img.width() 922 | 923 | def getHeight(self): 924 | """Returns the height of the image in pixels""" 925 | return self.img.height() 926 | 927 | def getPixel(self, x, y): 928 | """Returns a list [r,g,b] with the RGB color values for pixel (x,y) 929 | r,g,b are in range(256) 930 | 931 | """ 932 | 933 | value = self.img.get(x,y) 934 | if type(value) == type(0): 935 | return [value, value, value] 936 | elif type(value) == type((0,0,0)): 937 | return list(value) 938 | else: 939 | return list(map(int, value.split())) 940 | 941 | def setPixel(self, x, y, color): 942 | """Sets pixel (x,y) to the given color 943 | 944 | """ 945 | self.img.put("{" + color +"}", (x, y)) 946 | 947 | 948 | def save(self, filename): 949 | """Saves the pixmap image to filename. 950 | The format for the save image is determined from the filname extension. 951 | 952 | """ 953 | 954 | path, name = os.path.split(filename) 955 | ext = name.split(".")[-1] 956 | self.img.write( filename, format=ext) 957 | 958 | 959 | def color_rgb(r,g,b): 960 | """r,g,b are intensities of red, green, and blue in range(256) 961 | Returns color specifier string for the resulting color""" 962 | return "#%02x%02x%02x" % (r,g,b) 963 | 964 | def test(): 965 | win = GraphWin() 966 | win.setCoords(0,0,10,10) 967 | t = Text(Point(5,5), "Centered Text") 968 | t.draw(win) 969 | p = Polygon(Point(1,1), Point(5,3), Point(2,7)) 970 | p.draw(win) 971 | e = Entry(Point(5,6), 10) 972 | e.draw(win) 973 | win.getMouse() 974 | p.setFill("red") 975 | p.setOutline("blue") 976 | p.setWidth(2) 977 | s = "" 978 | for pt in p.getPoints(): 979 | s = s + "(%0.1f,%0.1f) " % (pt.getX(), pt.getY()) 980 | t.setText(e.getText()) 981 | e.setFill("green") 982 | e.setText("Spam!") 983 | e.move(2,0) 984 | win.getMouse() 985 | p.move(2,3) 986 | s = "" 987 | for pt in p.getPoints(): 988 | s = s + "(%0.1f,%0.1f) " % (pt.getX(), pt.getY()) 989 | t.setText(s) 990 | win.getMouse() 991 | p.undraw() 992 | e.undraw() 993 | t.setStyle("bold") 994 | win.getMouse() 995 | t.setStyle("normal") 996 | win.getMouse() 997 | t.setStyle("italic") 998 | win.getMouse() 999 | t.setStyle("bold italic") 1000 | win.getMouse() 1001 | t.setSize(14) 1002 | win.getMouse() 1003 | t.setFace("arial") 1004 | t.setSize(20) 1005 | win.getMouse() 1006 | win.close() 1007 | 1008 | #MacOS fix 2 1009 | #tk.Toplevel(_root).destroy() 1010 | 1011 | # MacOS fix 1 1012 | update() 1013 | 1014 | if __name__ == "__main__": 1015 | test() 1016 | -------------------------------------------------------------------------------- /scripts/highscores.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | 3 | import math 4 | from pathlib import Path 5 | 6 | import common 7 | import drawer 8 | 9 | #Relative path to the text file containing the highscores listings 10 | PATH = "../res/highscores.txt" 11 | 12 | #Constants for the creation of the highscore GUI 13 | GAP = common.WINDOW_WIDTH / 4 14 | RANK_X_LOCATION = GAP 15 | NAME_X_LOCATION = GAP * 2 16 | SCORE_X_LOCATION = GAP * 3 17 | 18 | def highscoresExist(): 19 | path = Path(PATH) 20 | return path.is_file() 21 | 22 | 23 | def createFile(): 24 | open(PATH, "w").close() 25 | 26 | def loadScores(): 27 | '''Loads the raw data from the highscore file''' 28 | if not highscoresExist(): 29 | createFile() 30 | 31 | with open(PATH) as inFile: 32 | data = inFile.read() 33 | return data 34 | 35 | def writeScores(scores): 36 | '''Writes list of tuple(name, score) to the highscore file''' 37 | with open(PATH, "w") as outFile: 38 | for nameScore in scores: 39 | #Writes the tuple to the file, with a space between the elements 40 | outFile.write("`".join(str(x) for x in nameScore) + "\n") 41 | 42 | def extractScores(data): 43 | '''Extracts the highscores, and returns a list of tuple(name, score)''' 44 | highscores = [] 45 | lines = data.split("\n") 46 | newData = [] 47 | for line in lines: 48 | newData += line.split("`") 49 | for i in range(0, len(newData) - 1, 2): 50 | pair = (newData[i], int(newData[i + 1])) 51 | highscores.append(pair) 52 | return highscores 53 | 54 | def getScoresList(): 55 | return extractScores(loadScores()) 56 | 57 | def submitScore(name, score): 58 | '''Adds a score to the highscores''' 59 | highscores = getScoresList() 60 | highscores.append((name, score)) 61 | highscores = sorted(highscores, key = lambda x: x[1]) 62 | highscores = highscores[::-1] 63 | writeScores(highscores) 64 | 65 | def addHighscoreTitles(sprites, y): 66 | '''Adds the titles to the highscores''' 67 | sprites.append(gfx.Text(gfx.Point(RANK_X_LOCATION, y), "Rank")) 68 | sprites.append(gfx.Text(gfx.Point(NAME_X_LOCATION, y), "Name")) 69 | sprites.append(gfx.Text(gfx.Point(SCORE_X_LOCATION, y), "Score")) 70 | 71 | for i in range(1, 4): 72 | sprites[-i].setStyle("bold") 73 | sprites[-i].setTextColor("gray10") 74 | 75 | def createBackgroundRect(sprites, y, colour): 76 | rect = gfx.Rectangle(gfx.Point(0, y - 10), gfx.Point(common.WINDOW_WIDTH, y + 10)) 77 | rect.setFill(colour) 78 | rect.setOutline(colour) 79 | sprites.append(rect) 80 | 81 | def addField(sprites, name, rank, score, y): 82 | sprites.append(gfx.Text(gfx.Point(RANK_X_LOCATION, y), rank)) 83 | sprites.append(gfx.Text(gfx.Point(NAME_X_LOCATION, y), name)) 84 | sprites.append(gfx.Text(gfx.Point(SCORE_X_LOCATION, y), score)) 85 | 86 | def createHighscoresDisplay(window): 87 | '''Creation of the GUI for the highscores screen''' 88 | highscores = getScoresList() 89 | sprites = [] 90 | 91 | #Create title bar 92 | sprites.append(common.createTitle("Highscores")) 93 | 94 | colours = ["gray90", "cornsilk4"] * (len(highscores) // 2 + 1) 95 | for i in range(len(highscores) + 1): 96 | rank = str(i) 97 | name = str(highscores[i - 1][0]) 98 | score = str(highscores[i - 1][1]) 99 | y = i * 20 + 100 + 10 100 | createBackgroundRect(sprites, y, colours[i]) 101 | if i == 0: 102 | addHighscoreTitles(sprites, y) 103 | continue 104 | addField(sprites, name, rank, score, y) 105 | if i + 1 == 26: #Maximum of 25 highscore fields can be displayed 106 | break 107 | 108 | drawer.drawList(sprites, window) 109 | return sprites -------------------------------------------------------------------------------- /scripts/main.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | import state_enum as states 3 | 4 | from state_playing import runPlayState 5 | from state_menu import runMenuState 6 | from state_gameover import runGameOverState 7 | 8 | import common 9 | 10 | def makeWindow(): 11 | '''Creates a window that does not automatically update''' 12 | return gfx.GraphWin(common.GAME_NAME + " - By Matthew Hopson", 13 | common.WINDOW_WIDTH, common.WINDOW_HEIGHT, 14 | autoflush = False) #Turning off autoflush allows more control over the framerate 15 | 16 | def createControlDictionary(): 17 | return { 18 | "running": True, #Whether the game is running or not 19 | "state": states.STATE_MENU #The current game state 20 | } 21 | 22 | def runGame(window, control): 23 | '''Main loop of the game''' 24 | #Chooses state based on the main control 25 | while control["running"]: 26 | currentState = control["state"] 27 | if currentState == states.STATE_MENU: 28 | runMenuState(window, control) 29 | elif currentState == states.STATE_PLAYING: 30 | score, elapsed = runPlayState(window, control) 31 | elif currentState == states.STATE_GAME_OVER: 32 | runGameOverState(window, control, score, elapsed) 33 | #Exit the game 34 | if window.closed or currentState == states.EXIT: 35 | control["running"] = False 36 | gfx.update(common.UPDATE_SPEED) 37 | 38 | if __name__ == "__main__": 39 | '''Entry point of program''' 40 | window = makeWindow() 41 | #Create control variables in a dictionary so it can pass-by-reference 42 | control = createControlDictionary() 43 | runGame(window, control) 44 | 45 | 46 | -------------------------------------------------------------------------------- /scripts/player.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | import common 3 | import vector 4 | import tiles 5 | import apple as appleF 6 | import math 7 | import drawer 8 | 9 | def createAndroid(window): 10 | '''Creates the Android sprite (based on Dr. M. Poole's code)''' 11 | coords = drawer.loadSpriteVerticies("android") 12 | body = gfx.Polygon(coords) 13 | head = gfx.Circle(gfx.Point(30, 20), 20) 14 | eye1 = gfx.Circle(gfx.Point(22, 7), 4) 15 | eye2 = gfx.Circle(gfx.Point(37, 7), 4) 16 | 17 | droidParts = [body, head, eye1, eye2] 18 | eyes = [eye1, eye2] 19 | for part in droidParts: 20 | if part in eyes: 21 | colour = "white" 22 | else: 23 | colour = "green" 24 | part.setFill(colour) 25 | part.setOutline(colour) 26 | part.move(500, tiles.BASE_HEIGHT - 45) 27 | part.draw(window) 28 | return droidParts 29 | 30 | def handleInput(key, velocity): 31 | '''Says it on the tin''' 32 | acceleration = 1.25 33 | if key == "a": 34 | if (velocity > 0): 35 | velocity = -velocity / 2 36 | velocity -= acceleration 37 | elif key == "d": 38 | if (velocity < 0): 39 | velocity = -velocity / 2 40 | velocity += acceleration 41 | return velocity 42 | 43 | def clampVelocity(velX): 44 | clamp = 10 45 | if (velX > clamp): 46 | return clamp 47 | elif (velX < -clamp): 48 | return -clamp 49 | else: 50 | return velX 51 | 52 | def movePlayer(sprite, amount): 53 | for part in sprite: 54 | part.move(amount, 0) 55 | 56 | def tryCollideMissingTiles(playerVel, minIndex, maxIndex, isTilesActive): 57 | '''Collides player with any tiles that might be missing''' 58 | if playerVel < 0: #moving left 59 | if not isTilesActive[minIndex]: 60 | playerVel = 0.5 61 | elif playerVel > 0: 62 | if not isTilesActive[maxIndex]: 63 | playerVel = -0.5 64 | return playerVel 65 | 66 | def tryCollideWindowEdges(playerVel, minX, maxX): 67 | '''Collides player with window edges''' 68 | if minX < 0: 69 | playerVel = 0.5 70 | elif maxX > common.WINDOW_WIDTH: 71 | playerVel = -0.5 72 | return playerVel 73 | 74 | def tryCollideEdges(playerVel, minX, maxX, isTilesActive): 75 | '''Collides player with the X-edges of the window, as well as inactive tiles''' 76 | tileIndexMin = math.floor((minX + 15) / tiles.TILE_SIZE) 77 | tileIndexMax = math.ceil ((maxX - 15) / tiles.TILE_SIZE) - 1 78 | 79 | playerVel = tryCollideMissingTiles(playerVel, tileIndexMin, tileIndexMax, isTilesActive) 80 | playerVel = tryCollideWindowEdges (playerVel, minX, maxX) 81 | 82 | return playerVel * 0.91 #apply velocity dampening 83 | 84 | def isTochingApple(apple, minX): 85 | '''Returns True if the player is touching an apple''' 86 | appleX = apple.getCenter().x 87 | appleY = apple.getCenter().y 88 | return vector.distance(minX + 30, tiles.BASE_HEIGHT - 20, 89 | appleX, appleY) < appleF.DIAMETER 90 | 91 | def shouldFireProjectile(window): 92 | '''Returns true on mouse click (which is > 10px away from last click)''' 93 | mousePoint = window.checkMouse() 94 | if mousePoint != None: 95 | x1 = shouldFireProjectile.oldPos.x 96 | x2 = mousePoint.x 97 | y1 = shouldFireProjectile.oldPos.y 98 | y2 = mousePoint.y 99 | if vector.distance(x1, y1, x2, y2) > 10: 100 | shouldFireProjectile.oldPos = mousePoint 101 | return True, mousePoint 102 | return False, shouldFireProjectile.oldPos 103 | 104 | shouldFireProjectile.oldPos = gfx.Point(-100, -100) -------------------------------------------------------------------------------- /scripts/projectile.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | 3 | import common 4 | import vector 5 | 6 | import apple as appleFuncs 7 | 8 | SPEED = 12 9 | 10 | def testForAppleProjectileCollision(projectile, apples): 11 | for apple in apples[:]: 12 | appleCenter = apple.getCenter() 13 | projCenter = projectile.getCenter() 14 | if vector.distanceBetween(appleCenter, projCenter) < appleFuncs.DIAMETER: 15 | appleFuncs.removeApple(apples, apple) 16 | 17 | def moveProjectile(direction, projectile): 18 | dx = direction.getX() 19 | dy = direction.getY() 20 | #direction.y += 0.05 #Apply gravity 21 | projectile.move(dx, dy) 22 | 23 | def isOutOfBounds(centre): 24 | x = centre.getX() 25 | y = centre.getY() 26 | d = appleFuncs.DIAMETER 27 | return x - d > common.WINDOW_WIDTH or x + d < 0 or \ 28 | y - d > common.WINDOW_HEIGHT or y + d < 0 29 | 30 | def update(projectiles, projectileDirections, apples): 31 | '''Updates the player's projectiles''' 32 | removeMe = [] 33 | for i in range(len(projectiles)): 34 | moveProjectile(projectileDirections[i], projectiles[i]) 35 | testForAppleProjectileCollision(projectiles[i], apples) 36 | if isOutOfBounds(projectiles[i].getCenter()): 37 | removeMe.append(i) 38 | 39 | 40 | ''' 41 | for x in removeMe: 42 | projectiles[i].undraw() 43 | projectileDirections.pop(x) 44 | projectiles.pop(x) 45 | ''' 46 | 47 | def create(playerPoint, target, window): 48 | '''Creates a projectile''' 49 | dx, dy = vector.getPointDifference(playerPoint, target) 50 | proj = appleFuncs.makeDefaultApple(playerPoint.getX(), playerPoint.getY(), window) 51 | 52 | dirVector = vector.normalise(gfx.Point(dx, dy)) 53 | dx = dirVector.getX() * SPEED 54 | dy = dirVector.getY() * SPEED 55 | velocity = gfx.Point(dx, dy) 56 | 57 | return proj, velocity -------------------------------------------------------------------------------- /scripts/state_enum.py: -------------------------------------------------------------------------------- 1 | STATE_MENU = 0 2 | STATE_PLAYING = 1 3 | STATE_GAME_OVER = 2 4 | EXIT = -1 -------------------------------------------------------------------------------- /scripts/state_gameover.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | 3 | from common import WINDOW_HEIGHT, WINDOW_WIDTH 4 | import state_enum as states 5 | 6 | import highscores 7 | import drawer 8 | import common 9 | import button 10 | import aabb 11 | 12 | def addMessage(window, message, size = 20, color = "black", reset = False, bold = False): 13 | '''message add''' 14 | if reset: 15 | addMessage.y = WINDOW_HEIGHT / 10 16 | msg = gfx.Text(gfx.Point(WINDOW_WIDTH / 2, addMessage.y), message) 17 | msg.setSize(size) 18 | msg.setFill(color) 19 | msg.draw(window) 20 | msg.setStyle("bold" if bold else "normal") 21 | addMessage.y += 40 22 | return msg 23 | addMessage.y = common.WINDOW_HEIGHT / 10 24 | 25 | def makeSubmitMenuGUI(score, window): 26 | '''Creates the GUI for the submission screen''' 27 | message = "Score To Submit: " + str(score) 28 | 29 | messText = gfx.Text (gfx.Point(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 6 * 2), message) 30 | nameText = gfx.Text (gfx.Point(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 6 * 3 - 50), "Enter your name:") 31 | inputBox = gfx.Entry(gfx.Point(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 6 * 3), 25) 32 | 33 | submitSprites, \ 34 | submitButton = button.create(WINDOW_HEIGHT// 6 * 4, "Submit", window) 35 | 36 | messText.setFill("red") 37 | messText.setSize(36) 38 | error = "Text must be between 3 and 25 chars, and contain no '`' character." 39 | errorMessage = gfx.Text (gfx.Point(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 6 * 3.50), error) 40 | errorMessage.setStyle("bold") 41 | errorMessage.setFill("red") 42 | 43 | return [messText, nameText] + submitSprites, errorMessage, inputBox, submitButton 44 | 45 | def submitScoreState(window, control, score): 46 | '''The playing screen for submitting a new score''' 47 | 48 | sprites, \ 49 | errorMessage, \ 50 | inputBox, \ 51 | submitScoreButton = makeSubmitMenuGUI(score, window) 52 | 53 | drawer.drawList(sprites[:-2] + [inputBox], window) 54 | isError = False 55 | 56 | while not window.closed: 57 | point = window.checkMouse() 58 | if button.isButtonPressed(point, submitScoreButton, window): 59 | name = inputBox.getText() 60 | if (len(name) < 3 or len(name) > 25) or "`" in name: 61 | if not isError: 62 | errorMessage.draw(window) 63 | isError = True 64 | else: 65 | name = inputBox.getText() 66 | highscores.submitScore(name, score) 67 | break 68 | gfx.update(common.UPDATE_SPEED) 69 | drawer.undrawList(sprites + [inputBox, errorMessage]) 70 | 71 | def makeGameOverButtons(messageLength, window): 72 | '''Makes buttons for game over screen''' 73 | guiY = WINDOW_HEIGHT / 10 + 50 * messageLength 74 | playAgainSprites, \ 75 | playAgainButton = button.create(guiY, "Play Again", window) 76 | 77 | guiY += button.HEIGHT + 10 78 | submitSprites, \ 79 | submitButton = button.create(guiY, "Submit Score", window) 80 | 81 | guiY += button.HEIGHT + 10 82 | exitSprites, \ 83 | exitButton = button.create(guiY, "Exit", window) 84 | 85 | return submitSprites + playAgainSprites + exitSprites, \ 86 | playAgainButton, submitButton, exitButton 87 | 88 | 89 | 90 | def runGameOverState(window, control, score, elapsed): 91 | '''Runs after the player has run out of lives''' 92 | bg = common.createCenteredImage("menu_bg") 93 | bg.draw(window) 94 | overallScore = score * round(elapsed) 95 | messages = [ 96 | addMessage(window, "GAME OVER", 30, "red", reset = True, bold = True), 97 | addMessage(window, "Score: " + str(score)), 98 | addMessage(window, "Time: " + str(round(elapsed)) + " seconds"), 99 | addMessage(window, "Final Score: " + str(overallScore)) 100 | ] 101 | 102 | sprites, \ 103 | playAgainButton, \ 104 | submitScoreButton, \ 105 | exitToMenuButton = makeGameOverButtons(len(messages), window) 106 | sprites += messages 107 | 108 | scoreSubmitted = False 109 | while control["state"] is states.STATE_GAME_OVER and not common.shouldExit(window, control): 110 | mouseClick = window.checkMouse() 111 | if button.isButtonPressed(mouseClick, playAgainButton, window): 112 | common.switchState(window, control, states.STATE_PLAYING) 113 | elif button.isButtonPressed(mouseClick,submitScoreButton, window) and not scoreSubmitted: 114 | drawer.undrawList(sprites) 115 | submitScoreState(window, control, overallScore) 116 | if window.closed: 117 | break 118 | drawer.drawList(sprites, window) 119 | scoreSubmitted = True 120 | sprites[0].setFill("dim gray") 121 | sprites[1].setFill("gray") 122 | elif button.isButtonPressed(mouseClick, exitToMenuButton, window): 123 | common.switchState(window, control, states.STATE_MENU) 124 | 125 | gfx.update(common.UPDATE_SPEED) 126 | 127 | drawer.undrawList(sprites + [bg]) -------------------------------------------------------------------------------- /scripts/state_menu.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | import state_enum as states 3 | import common 4 | import button 5 | import drawer 6 | import apple 7 | import highscores 8 | 9 | import time 10 | import random 11 | import math 12 | 13 | BACK_BTN_Y = common.WINDOW_HEIGHT - button.HEIGHT - 10 14 | 15 | def getRandX(): 16 | '''Gets a random X-Position not above the button bounds''' 17 | return random.randint(0, common.WINDOW_WIDTH) 18 | 19 | def addApple(apples, window): 20 | '''Adds an apple to the background of the menus''' 21 | x = getRandX() 22 | y = random.randint(-common.WINDOW_HEIGHT, 0) 23 | r = random.randint(10, 22) 24 | apples.append(apple.makeAppleSprite(x, y, "red", r, window)) 25 | apples[-1].setOutline("red") 26 | 27 | def updateApples(apples, window): 28 | '''Updates the apples, such as moves them as removes them when they hit the bottom''' 29 | for app in apples[:]: 30 | app.move(0, app.getRadius() / 4) 31 | #app.move(math.sin(elapsed) * app.getRadius(), app.getRadius() / 5) 32 | if app.getCenter().getY() > common.WINDOW_HEIGHT + apple.DIAMETER: 33 | app.undraw() 34 | apples.remove(app) 35 | addApple(apples, window) 36 | 37 | def createHowToPlayMenu(window): 38 | '''Creates the GUI for the "how to play" menu''' 39 | sprites = [ 40 | common.createTitle("How To Play") 41 | ] 42 | drawer.drawList(sprites, window) 43 | 44 | guiY = common.WINDOW_HEIGHT / 10 + 50 45 | 46 | controlsSprites, \ 47 | controlsButton = button.create(guiY, "Controls", window) 48 | 49 | guiY += button.HEIGHT * 2 50 | 51 | objectivesSprites, \ 52 | objectivesButton = button.create(guiY, "Objectives", window) 53 | 54 | guiY += button.HEIGHT * 2 55 | 56 | appleTypesSprites, \ 57 | appleTypesButton = button.create(guiY, "Apple Types", window) 58 | 59 | #This is a list of sprites that have no purpose but to be shown, hence stored in a list 60 | #for ease of undrawing. 61 | sprites += objectivesSprites + controlsSprites + appleTypesSprites 62 | 63 | return sprites, controlsButton, objectivesButton, appleTypesButton 64 | 65 | #TODO, clearly these 3 functions repeat code, need to find way to stop this (Harder than you think, see how this is used in `displayHowToPlayMenu()) 66 | def createHowToControls(window): 67 | sprites = [ 68 | common.createTitle("Controls"), 69 | common.createCenteredImage("controls") 70 | ] 71 | drawer.drawList(sprites, window) 72 | return sprites 73 | 74 | def createHowToObjectives(window): 75 | sprites = [ 76 | common.createTitle("Objectives"), 77 | common.createCenteredImage("objectives") 78 | ] 79 | drawer.drawList(sprites, window) 80 | return sprites 81 | 82 | def createHowToAppleTypes(window): 83 | sprites = [ 84 | common.createTitle("Apple Types", y= common.WINDOW_HEIGHT / 12), 85 | common.createCenteredImage("apple_types") 86 | ] 87 | drawer.drawList(sprites, window) 88 | return sprites 89 | 90 | def displayHowToPlayMenu(window, control, apples): 91 | '''Displays the how to play menu, eg it's buttons''' 92 | sprites, ctrlButton, objButton, typeButton = createHowToPlayMenu(window) 93 | backButtonSprites, \ 94 | backButtonBounds = button.create(BACK_BTN_Y, "Back", window) 95 | 96 | menu_top = common.createCenteredImage("menu_top") 97 | menu_top.draw(window) 98 | 99 | def displayMenu(guiCreateFunction): 100 | drawer.undrawList(sprites + backButtonSprites) 101 | showMenu(window, control, apples, guiCreateFunction) 102 | if window.closed: 103 | return True 104 | drawer.drawList(sprites + backButtonSprites, window) 105 | return False 106 | 107 | while not window.closed: 108 | mouseClickPoint = window.checkMouse() 109 | updateApples(apples, window) 110 | if button.isButtonPressed(mouseClickPoint, backButtonBounds, window): 111 | break 112 | if button.isButtonPressed(mouseClickPoint, ctrlButton, window): 113 | if displayMenu(createHowToControls): 114 | break 115 | if button.isButtonPressed(mouseClickPoint, objButton, window): 116 | if displayMenu(createHowToObjectives): 117 | break 118 | if button.isButtonPressed(mouseClickPoint, typeButton, window): 119 | if displayMenu(createHowToAppleTypes): 120 | break 121 | 122 | drawer.redrawSprite(menu_top, window) 123 | drawer.redrawList(sprites, window) 124 | gfx.update(common.UPDATE_SPEED) 125 | drawer.undrawList(sprites + backButtonSprites + [menu_top]) 126 | 127 | def showMenu(window, control, apples, guiCreateFunction): 128 | '''Shows a basic menu which only has a back button (eg how to play, highscores)''' 129 | sprites = guiCreateFunction(window) 130 | backButtonSprites, \ 131 | backButtonBounds = button.create(BACK_BTN_Y, "Back", window) 132 | 133 | menu_top = common.createCenteredImage("menu_top") 134 | menu_top.draw(window) 135 | 136 | sprites += backButtonSprites 137 | while not window.closed: 138 | mouseClickPoint = window.checkMouse() 139 | updateApples(apples, window) 140 | if button.isButtonPressed(mouseClickPoint, backButtonBounds, window): 141 | break 142 | drawer.redrawSprite(menu_top, window) 143 | drawer.redrawList(sprites, window) 144 | gfx.update(common.UPDATE_SPEED) 145 | drawer.undrawList(sprites + [menu_top]) 146 | 147 | def createFrontMenuButtons(window): 148 | '''Creates the main buttons for the main menu''' 149 | guiY = common.WINDOW_HEIGHT / 10 + 50 150 | 151 | playGameSprites, \ 152 | playGameButton = button.create(guiY, "Play Game", window) 153 | 154 | guiY += button.HEIGHT * 2 155 | howToPlaySprites, \ 156 | howToPlayButton = button.create(guiY, "How To Play", window) 157 | 158 | guiY += button.HEIGHT * 2 159 | highScoresSprites, \ 160 | highScoresButton = button.create(guiY, "Highscores", window) 161 | 162 | guiY += button.HEIGHT * 2 163 | exitSprites, \ 164 | exitButton = button.create(guiY, "Exit Game", window) 165 | 166 | #This is a list of sprites that have no purpose but to be shown, hence stored in a list 167 | #for ease of undrawing. 168 | sprites = playGameSprites + \ 169 | howToPlaySprites + \ 170 | highScoresSprites + \ 171 | exitSprites 172 | 173 | return sprites, playGameButton, howToPlayButton, highScoresButton, exitButton 174 | 175 | def runMenuState(window, control): 176 | '''Says it on the tin''' 177 | titleText = common.createTitle(common.GAME_NAME, window) 178 | 179 | sprites, \ 180 | playButton, \ 181 | howToPlayButton, \ 182 | highscoreButton, \ 183 | exitButton = createFrontMenuButtons(window) 184 | 185 | bg = common.createCenteredImage("menu_bg") 186 | bg.draw(window) 187 | 188 | menu_top = common.createCenteredImage("menu_top") 189 | menu_top.draw(window) 190 | 191 | apples = [] 192 | for i in range(50): 193 | addApple(apples, window) 194 | 195 | def displayMenu(guiCreateFunction = None): 196 | drawer.undrawList([titleText] + sprites) 197 | showMenu(window, control, apples, guiCreateFunction) 198 | if window.closed: 199 | return True 200 | drawer.drawList([titleText] + sprites, window) 201 | return False 202 | 203 | start = time.time() 204 | while control["state"] == states.STATE_MENU and not window.closed: 205 | key = common.getKeyPress(window) 206 | point = window.checkMouse() 207 | elapsed = common.calculateTime(start) 208 | 209 | if button.isButtonPressed(point, playButton, window): 210 | common.switchState(window, control, states.STATE_PLAYING) 211 | elif button.isButtonPressed(point, howToPlayButton, window): 212 | drawer.undrawList([titleText] + sprites) 213 | displayHowToPlayMenu(window, control, apples) 214 | if window.closed: 215 | break 216 | drawer.drawList([titleText] + sprites, window) 217 | elif button.isButtonPressed(point, highscoreButton, window): 218 | if displayMenu(highscores.createHighscoresDisplay): 219 | break 220 | elif button.isButtonPressed(point, exitButton, window): 221 | common.switchState(window, control, states.EXIT) 222 | 223 | updateApples(apples, window) 224 | #make it so the title is ALWAYS on front 225 | drawer.redrawSprite(menu_top, window) 226 | drawer.redrawList([titleText] + sprites, window) 227 | gfx.update(common.UPDATE_SPEED) 228 | 229 | drawer.undrawList([titleText, bg, menu_top] + sprites + apples) -------------------------------------------------------------------------------- /scripts/state_playing.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | import state_enum as states 3 | import apple as appleFuncs 4 | 5 | import projectile 6 | import common 7 | import player 8 | import drawer 9 | import tiles 10 | import aabb 11 | 12 | from state_enum import STATE_PLAYING 13 | 14 | import random 15 | import math 16 | import time 17 | 18 | def tryAddMoreApples(apples, elapsedTime, window): 19 | '''Adds apples''' 20 | notManyApples = len(apples) < (elapsedTime // 12) + 1 21 | if notManyApples: 22 | apples.append(appleFuncs.createRandomApple(window)) 23 | 24 | 25 | def playerFire(window, playerSprite, projectiles, projDirections, score): 26 | '''Tries to fire a player projectile if they click the mouse on the window''' 27 | fire, target = player.shouldFireProjectile(window) 28 | if fire and score > 0: 29 | playerPoint = playerSprite[1].getCenter() 30 | proj, \ 31 | velocity = projectile.create(playerPoint, target, window) 32 | projectiles.append(proj) 33 | projDirections.append(velocity) 34 | return True 35 | return False 36 | 37 | def doAppleEffect(apples, appleType, isTilesActive, tileSprites, window): 38 | '''Takes the apple type and does something depending on its type''' 39 | deltaLife = 0 40 | if appleType == appleFuncs.REPAIR: #"Repair apple": Repairs tiles 41 | tiles.repairTiles(tileSprites, isTilesActive, window) 42 | elif appleType == appleFuncs.BOOST: #"Boost Apple": Gives player upto 3 extra lives 43 | deltaLife = random.randint(1, 3) 44 | elif appleType == appleFuncs.APPLPOCALYPSE: #"APPLPOCALYPSE": Removes all apples 45 | for oldApple in apples[:]: 46 | appleFuncs.removeApple(apples, oldApple) 47 | return deltaLife 48 | 49 | 50 | def collectApple(apples, apple, isTilesActive, tileSprites, window): 51 | '''Collects a single apple''' 52 | appleType = int(apple.getRadius()) 53 | appleFuncs.removeApple(apples, apple) 54 | return doAppleEffect(apples, appleType, isTilesActive, tileSprites, window) 55 | 56 | 57 | def updateApples(apples, playerMinX, isTilesActive, tileSprites, window): 58 | '''Update the updates, and test for collisions''' 59 | deltaLife = 0 60 | deltaScore = 0 61 | for apple in apples[:]: 62 | appleFuncs.moveApple(apple) 63 | #Different apples have different effects 64 | if player.isTochingApple(apple, playerMinX): 65 | deltaScore += 1 66 | deltaLife += collectApple(apples, apple, isTilesActive, tileSprites, window) 67 | elif appleFuncs.isCollidingTile(apple, isTilesActive, tileSprites): 68 | appleFuncs.removeApple(apples, apple) 69 | elif appleFuncs.isOffScreen(apple): 70 | appleFuncs.removeApple(apples, apple) 71 | deltaLife -= 1 72 | return deltaLife, deltaScore 73 | 74 | def createStatsDisplay(window): 75 | '''Creates the rectangle that shows the player's score and how many lives they have left''' 76 | statsBG = gfx.Rectangle(gfx.Point(common.WINDOW_CENTER_X - 50, 25), gfx.Point(common.WINDOW_CENTER_X + 50, 125)) 77 | statsBG.setFill("gray") 78 | scoreDisplay = gfx.Text(gfx.Point(common.WINDOW_CENTER_X, 50), "Score: 0") 79 | livesDisplay = gfx.Text(gfx.Point(common.WINDOW_CENTER_X, 100), "Lives: 10") 80 | statsBG.draw(window) 81 | scoreDisplay.draw(window) 82 | livesDisplay.draw(window) 83 | 84 | return scoreDisplay, livesDisplay, [statsBG, scoreDisplay, livesDisplay] 85 | 86 | def runMainGame(window, control): 87 | '''The main function handling the actual gameplay of the game''' 88 | '''Also a shamefully long function :( ''' 89 | #Draw background image 90 | background = common.createCenteredImage("game_background") 91 | background.draw(window) 92 | 93 | treeTop = common.createCenteredImage("tree_top") 94 | treeTop.draw(window) 95 | 96 | #Set up score 97 | score = 0 98 | lives = 10 99 | scoreDisplay, livesDisplay, statSprites = createStatsDisplay(window) 100 | 101 | #Set up player 102 | playerXVel = 0.0 103 | playerAABB = aabb.create(500.0, 500.0, 60.0, 45.0) 104 | playerSprite = player.createAndroid(window) 105 | 106 | #Create tiles 107 | tileSprites, \ 108 | isTilesActive = tiles.createTiles(window) 109 | NUM_TILES = len(tileSprites) 110 | 111 | #Create apple list 112 | x = appleFuncs.getRandomAppleXPosition() 113 | apples = [appleFuncs.makeDefaultApple(x, 0, window)] 114 | 115 | projectiles = [] 116 | projectilesDirections = [] 117 | 118 | def updateScore(delta): 119 | nonlocal score 120 | score += delta 121 | scoreDisplay.setText("Score: " + str(score)) 122 | 123 | def updateLives(delta): 124 | nonlocal lives 125 | lives += delta 126 | livesDisplay.setText("Lives: " + str(lives)) 127 | 128 | #Begin timer 129 | startTime = time.time() 130 | elapsed = 0 131 | 132 | isGamePaused = False 133 | gamePausedDisplay = common.createTitle("Paused - Press E to exit", colour = "red", y = tiles.BASE_HEIGHT / 2) 134 | 135 | #Main loop section for the playing state 136 | while lives > 0 and not common.shouldExit(window, control): 137 | #Create data for this frame 138 | elapsed = common.calculateTime(startTime) 139 | playerMinX = playerAABB["x"] 140 | playerMaxX = playerAABB["x"] + playerAABB["w"] 141 | key = common.getKeyPress(window) 142 | 143 | #Handle game pausing 144 | if key == "p": 145 | isGamePaused = not isGamePaused 146 | if isGamePaused: 147 | gamePausedDisplay.draw(window) 148 | else: 149 | gamePausedDisplay.undraw() 150 | 151 | #Game logic itself 152 | if not isGamePaused: 153 | #Player input 154 | playerXVel = player.handleInput (key, playerXVel) 155 | playerXVel = player.clampVelocity (playerXVel) 156 | 157 | if (playerFire(window, playerSprite, projectiles, projectilesDirections, score)): 158 | updateScore(-1) 159 | 160 | #Fix for a glitch causing player to get stuck 161 | tileIndex = math.floor(playerSprite[1].getCenter().x / tiles.TILE_SIZE) 162 | if not isTilesActive[tileIndex]: 163 | isTilesActive[tileIndex] = True 164 | tileSprites[tileIndex].draw(window) 165 | 166 | #Update players, apples, and then projectiles 167 | playerXVel = player.tryCollideEdges(playerXVel, playerMinX, playerMaxX, isTilesActive) 168 | player.movePlayer(playerSprite, playerXVel) 169 | playerAABB["x"] += playerXVel 170 | 171 | tryAddMoreApples(apples, elapsed, window) 172 | deltaLives, deltaScore = updateApples(apples, playerMinX, 173 | isTilesActive, tileSprites, window) 174 | updateScore(deltaScore) 175 | updateLives(deltaLives) 176 | 177 | projectile.update(projectiles, projectilesDirections, apples) 178 | 179 | #Redraw fore-ground 180 | drawer.redrawSprite (treeTop, window) 181 | drawer.redrawList (statSprites, window) 182 | else: #is paused 183 | if key == "e": 184 | break 185 | gfx.update(common.UPDATE_SPEED * 2) 186 | 187 | #End of the game/ Game over 188 | drawer.undrawList(apples + projectiles + playerSprite + statSprites + [background]) 189 | tiles.undraw(tileSprites, isTilesActive) 190 | 191 | return score, elapsed 192 | 193 | 194 | def runPlayState(window, control): 195 | '''Runs the main game''' 196 | score = 0 197 | elapsed = 0 198 | while control["state"] == STATE_PLAYING and not common.shouldExit(window, control): 199 | score, elapsed = runMainGame(window, control) 200 | if common.shouldExit(window, control): 201 | return score, elapsed 202 | drawer.undrawAll(window) 203 | common.switchState(window, control, states.STATE_GAME_OVER) 204 | return score, elapsed 205 | 206 | -------------------------------------------------------------------------------- /scripts/tiles.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | import random as rng 3 | from common import WINDOW_HEIGHT, WINDOW_WIDTH 4 | import drawer 5 | 6 | TILE_SIZE = 50 7 | BASE_HEIGHT = WINDOW_HEIGHT - TILE_SIZE * 3 8 | 9 | def createTiles(window): 10 | y = BASE_HEIGHT + 50 11 | tiles = [] 12 | active = [] 13 | for x in range(WINDOW_WIDTH // TILE_SIZE): 14 | tiles.append(gfx.Image(gfx.Point(x * TILE_SIZE + 25, y), "../res/tile.gif")) 15 | active.append(True) 16 | 17 | drawer.drawList(tiles, window) 18 | 19 | return tiles, active 20 | 21 | 22 | 23 | def repairTiles(tileSprites, activeTiles, window): 24 | for i in range(len(tileSprites)): 25 | if not activeTiles[i]: 26 | tileSprites[i].draw(window) 27 | activeTiles[i] = True 28 | 29 | def undraw(tileSprites, activeTiles): 30 | '''undraws all the active tiles''' 31 | for i in range(len(tileSprites)): 32 | if activeTiles[i]: 33 | tileSprites[i].undraw() -------------------------------------------------------------------------------- /scripts/vector.py: -------------------------------------------------------------------------------- 1 | import graphics as gfx 2 | import math 3 | 4 | ''' 5 | Vector mathematics 6 | ''' 7 | 8 | def distance(x1, y1, x2, y2): 9 | '''Returns distance between two points''' 10 | dx = abs(x1 - x2) 11 | dy = abs(y1 - y2) 12 | return math.sqrt(dx ** 2 + dy ** 2) 13 | 14 | def distanceBetween(p1, p2): 15 | '''Returns distance between two points''' 16 | dx = abs(p1.x - p2.x) 17 | dy = abs(p1.y - p2.y) 18 | return math.sqrt(dx ** 2 + dy ** 2) 19 | 20 | def normalise(vect): 21 | '''Returns a normilised version of the vector passed in''' 22 | x = vect.x 23 | y = vect.y 24 | length = math.sqrt(x * x + y * y) 25 | return gfx.Point(-(x / length), -(y / length)) 26 | 27 | def getPointDifference(p1, p2): 28 | '''Returns dx, dy between two points''' 29 | return p1.x - p2.x, p1.y - p2.y --------------------------------------------------------------------------------