├── play ├── __init__.py ├── blank_image.png ├── exceptions.py ├── keypress.py ├── color.py └── play.py ├── example.gif ├── requirements.txt ├── MANIFEST.in ├── setup.py ├── MIT LICENSE └── README.md /play/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.23' 2 | 3 | from .play import * -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replit/play/HEAD/example.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.16.2 2 | pygame==1.9.4 3 | pymunk==5.4.2 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md requirements.txt example.gif play/blank_image.png -------------------------------------------------------------------------------- /play/blank_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replit/play/HEAD/play/blank_image.png -------------------------------------------------------------------------------- /play/exceptions.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | class Oops(Exception): 5 | def __init__(self, message): 6 | # for readability, always prepend exception messages in the library with two blank lines 7 | message = '\n\n\tOops!\n\n\t'+message.replace('\n', '\n\t')+'\n' 8 | super(Oops, self).__init__(message) 9 | 10 | class Hmm(UserWarning): 11 | pass 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup 3 | 4 | # The directory containing this file 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | # The text of the README file 8 | README = (HERE / "README.md").read_text() 9 | 10 | # This call to setup() does all the work 11 | setup( 12 | name="replit-play", 13 | version="0.0.23", 14 | description="The easiest way to make games and media projects in Python.", 15 | long_description=README, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/replit/play", 18 | author="Repl.it", 19 | author_email="gchiacchieri@gmail.com", 20 | license="MIT", 21 | classifiers=[ 22 | "License :: OSI Approved :: MIT License", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.7", 25 | ], 26 | python_requires=">=3.5", 27 | packages=["play"], 28 | include_package_data=True, 29 | install_requires=["pygame", "numpy", "pymunk"], 30 | ) 31 | -------------------------------------------------------------------------------- /MIT LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /play/keypress.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | keypress_map = { 3 | pygame.K_BACKSPACE: 'backspace', 4 | pygame.K_TAB: 'tab', 5 | pygame.K_CLEAR: 'clear', 6 | pygame.K_RETURN: 'enter', 7 | pygame.K_PAUSE: 'pause', 8 | pygame.K_ESCAPE: 'escape', 9 | pygame.K_SPACE: 'space', 10 | pygame.K_EXCLAIM: '!', 11 | pygame.K_QUOTEDBL: '"', 12 | pygame.K_HASH: '#', 13 | pygame.K_DOLLAR: '$', 14 | pygame.K_AMPERSAND: '&', 15 | pygame.K_QUOTE: "'", 16 | pygame.K_LEFTPAREN: '(', 17 | pygame.K_RIGHTPAREN: ')', 18 | pygame.K_ASTERISK: '*', 19 | pygame.K_PLUS: '+', 20 | pygame.K_COMMA: ',', 21 | pygame.K_MINUS: '-', 22 | pygame.K_PERIOD: '.', 23 | pygame.K_SLASH: '/', 24 | pygame.K_0: '0', 25 | pygame.K_1: '1', 26 | pygame.K_2: '2', 27 | pygame.K_3: '3', 28 | pygame.K_4: '4', 29 | pygame.K_5: '5', 30 | pygame.K_6: '6', 31 | pygame.K_7: '7', 32 | pygame.K_8: '8', 33 | pygame.K_9: '9', 34 | pygame.K_COLON: ':', 35 | pygame.K_SEMICOLON: ';', 36 | pygame.K_LESS: '<', 37 | pygame.K_EQUALS: '=', 38 | pygame.K_GREATER: '>', 39 | pygame.K_QUESTION: '?', 40 | pygame.K_AT: '@', 41 | pygame.K_LEFTBRACKET: '[', 42 | pygame.K_BACKSLASH: '\\', 43 | pygame.K_RIGHTBRACKET: ']', 44 | pygame.K_CARET: '^', 45 | pygame.K_UNDERSCORE: '_', 46 | pygame.K_BACKQUOTE: '`', 47 | pygame.K_a: 'a', 48 | pygame.K_b: 'b', 49 | pygame.K_c: 'c', 50 | pygame.K_d: 'd', 51 | pygame.K_e: 'e', 52 | pygame.K_f: 'f', 53 | pygame.K_g: 'g', 54 | pygame.K_h: 'h', 55 | pygame.K_i: 'i', 56 | pygame.K_j: 'j', 57 | pygame.K_k: 'k', 58 | pygame.K_l: 'l', 59 | pygame.K_m: 'm', 60 | pygame.K_n: 'n', 61 | pygame.K_o: 'o', 62 | pygame.K_p: 'p', 63 | pygame.K_q: 'q', 64 | pygame.K_r: 'r', 65 | pygame.K_s: 's', 66 | pygame.K_t: 't', 67 | pygame.K_u: 'u', 68 | pygame.K_v: 'v', 69 | pygame.K_w: 'w', 70 | pygame.K_x: 'x', 71 | pygame.K_y: 'y', 72 | pygame.K_z: 'z', 73 | pygame.K_DELETE: 'delete', 74 | # pygame.K_KP0: '', 75 | # pygame.K_KP1: '', 76 | # pygame.K_KP2: '', 77 | # pygame.K_KP3: '', 78 | # pygame.K_KP4: '', 79 | # pygame.K_KP5: '', 80 | # pygame.K_KP6: '', 81 | # pygame.K_KP7: '', 82 | # pygame.K_KP8: '', 83 | # pygame.K_KP9: '', 84 | # pygame.K_KP_PERIOD: '', 85 | # pygame.K_KP_DIVIDE: '', 86 | # pygame.K_KP_MULTIPLY: '', 87 | # pygame.K_KP_MINUS: '', 88 | # pygame.K_KP_PLUS: '', 89 | # pygame.K_KP_ENTER: '', 90 | # pygame.K_KP_EQUALS: '', 91 | pygame.K_UP: 'up', 92 | pygame.K_DOWN: 'down', 93 | pygame.K_RIGHT: 'right', 94 | pygame.K_LEFT: 'left', 95 | pygame.K_INSERT: 'insert', 96 | pygame.K_HOME: 'home', 97 | pygame.K_END: 'end', 98 | pygame.K_PAGEUP: 'pageup', 99 | pygame.K_PAGEDOWN: 'pagedown', 100 | pygame.K_F1: 'F1', 101 | pygame.K_F2: 'F2', 102 | pygame.K_F3: 'F3', 103 | pygame.K_F4: 'F4', 104 | pygame.K_F5: 'F5', 105 | pygame.K_F6: 'F6', 106 | pygame.K_F7: 'F7', 107 | pygame.K_F8: 'F8', 108 | pygame.K_F9: 'F9', 109 | pygame.K_F10: 'F10', 110 | pygame.K_F11: 'F11', 111 | pygame.K_F12: 'F12', 112 | pygame.K_F13: 'F13', 113 | pygame.K_F14: 'F14', 114 | pygame.K_F15: 'F15', 115 | pygame.K_NUMLOCK: 'numlock', 116 | pygame.K_CAPSLOCK: 'capslock', 117 | pygame.K_SCROLLOCK: 'scrollock', 118 | pygame.K_RSHIFT: 'shift', 119 | pygame.K_LSHIFT: 'shift', 120 | pygame.K_RCTRL: 'control', 121 | pygame.K_LCTRL: 'control', 122 | pygame.K_RALT: 'alt', 123 | pygame.K_LALT: 'alt', 124 | pygame.K_RMETA: 'meta', 125 | pygame.K_LMETA: 'meta', 126 | pygame.K_LSUPER: 'super', 127 | pygame.K_RSUPER: 'super', 128 | # pygame.K_MODE: '', 129 | # pygame.K_HELP: '', 130 | # pygame.K_PRINT: '', 131 | # pygame.K_SYSREQ: '', 132 | # pygame.K_BREAK: '', 133 | # pygame.K_MENU: '', 134 | # pygame.K_POWER: '', 135 | pygame.K_EURO: '€', 136 | } 137 | 138 | def pygame_key_to_name(pygame_key_event): 139 | english_name = keypress_map[pygame_key_event.key] 140 | if not pygame_key_event.mod and len(english_name) > 1: 141 | # use english names like 'space' instead of the space character ' ' 142 | return english_name 143 | return pygame_key_event.unicode 144 | # pygame_key_event.unicode is how we get e.g. # instead of 3 on US keyboards when shift+3 is pressed. 145 | # It also gives us capital letters and things like that. 146 | 147 | -------------------------------------------------------------------------------- /play/color.py: -------------------------------------------------------------------------------- 1 | from .exceptions import Oops 2 | # most color names from https://upload.wikimedia.org/wikipedia/commons/2/2b/SVG_Recognized_color_keyword_names.svg 3 | # except that list doesn't have obvious colors people might want to use like "light brown", so we add those manually 4 | color_names = { 5 | 'aliceblue': (240, 248, 255), 6 | 'antiquewhite': (250, 235, 215), 7 | 'aqua': ( 0, 255, 255), 8 | 'aquamarine': (127, 255, 212), 9 | 'azure': (240, 255, 255), 10 | 'beige': (245, 245, 220), 11 | 'bisque': (255, 228, 196), 12 | 'black': ( 0, 0, 0), 13 | 'blanchedalmond': (255, 235, 205), 14 | 'blue': ( 0, 0, 255), 15 | 'blueviolet': (138, 43, 226), 16 | 'brown': (165, 42, 42), 17 | 'burlywood': (222, 184, 135), 18 | 'cadetblue': ( 95, 158, 160), 19 | 'chartreuse': (127, 255, 0), 20 | 'chocolate': (210, 105, 30), 21 | 'coral': (255, 127, 80), 22 | 'cornflowerblue': (100, 149, 237), 23 | 'cornsilk': (255, 248, 220), 24 | 'crimson': (220, 20, 60), 25 | 'cyan': ( 0, 255, 255), 26 | 'darkblue': ( 0, 0, 139), 27 | 'darkcyan': ( 0, 139, 139), 28 | 'darkgoldenrod': (184, 134, 11), 29 | 'darkgray': (169, 169, 169), 30 | 'darkgreen': ( 0, 100, 0), 31 | 'darkgrey': (169, 169, 169), 32 | 'darkkhaki': (189, 183, 107), 33 | 'darkmagenta': (139, 0, 139), 34 | 'darkolivegreen': ( 85, 107, 47), 35 | 'darkorange': (255, 140, 0), 36 | 'darkorchid': (153, 50, 204), 37 | 'darkred': (139, 0, 0), 38 | 'darksalmon': (233, 150, 122), 39 | 'darkseagreen': (143, 188, 143), 40 | 'darkslateblue': ( 72, 61, 139), 41 | 'darkslategray': ( 47, 79, 79), 42 | 'darkslategrey': ( 47, 79, 79), 43 | 'darkturquoise': ( 0, 206, 209), 44 | 'darkviolet': (148, 0, 211), 45 | 'deeppink': (255, 20, 147), 46 | 'deepskyblue': ( 0, 191, 255), 47 | 'dimgray': (105, 105, 105), 48 | 'dimgrey': (105, 105, 105), 49 | 'dodgerblue': ( 30, 144, 255), 50 | 'firebrick': (178, 34, 34), 51 | 'floralwhite': (255, 250, 240), 52 | 'forestgreen': ( 34, 139, 34), 53 | 'fuchsia': (255, 0, 255), 54 | 'gainsboro': (220, 220, 220), 55 | 'ghostwhite': (248, 248, 255), 56 | 'gold': (255, 215, 0), 57 | 'goldenrod': (218, 165, 32), 58 | 'gray': (128, 128, 128), 59 | 'grey': (128, 128, 128), 60 | 'green': ( 0, 128, 0), 61 | 'greenyellow': (173, 255, 47), 62 | 'honeydew': (240, 255, 240), 63 | 'hotpink': (255, 105, 180), 64 | 'indianred': (205, 92, 92), 65 | 'indigo': ( 75, 0, 130), 66 | 'ivory': (255, 255, 240), 67 | 'khaki': (240, 230, 140), 68 | 'lavender': (230, 230, 250), 69 | 'lavenderblush': (255, 240, 245), 70 | 'lawngreen': (124, 252, 0), 71 | 'lemonchiffon': (255, 250, 205), 72 | 'lightblue': (173, 216, 230), 73 | 'lightcoral': (240, 128, 128), 74 | 'lightcyan': (224, 255, 255), 75 | 'lightgoldenrodyellow': (250, 250, 210), 76 | 'lightgray': (211, 211, 211), 77 | 'lightgreen': (144, 238, 144), 78 | 'lightgrey': (211, 211, 211), 79 | 'lightpink': (255, 182, 193), 80 | 'lightsalmon': (255, 160, 122), 81 | 'lightseagreen': ( 32, 178, 170), 82 | 'lightskyblue': (135, 206, 250), 83 | 'lightslategray': (119, 136, 153), 84 | 'lightslategrey': (119, 136, 153), 85 | 'lightsteelblue': (176, 196, 222), 86 | 'lightyellow': (255, 255, 224), 87 | 'lime': ( 0, 255, 0), 88 | 'limegreen': ( 50, 205, 50), 89 | 'linen': (250, 240, 230), 90 | 'magenta': (255, 0, 255), 91 | 'maroon': (128, 0, 0), 92 | 'mediumaquamarine': (102, 205, 170), 93 | 'mediumblue': ( 0, 0, 205), 94 | 'mediumorchid': (186, 85, 211), 95 | 'mediumpurple': (147, 112, 219), 96 | 'mediumseagreen': ( 60, 179, 113), 97 | 'mediumslateblue': (123, 104, 238), 98 | 'mediumspringgreen': ( 0, 250, 154), 99 | 'mediumturquoise': ( 72, 209, 204), 100 | 'mediumvioletred': (199, 21, 133), 101 | 'midnightblue': ( 25, 25, 112), 102 | 'mintcream': (245, 255, 250), 103 | 'mistyrose': (255, 228, 225), 104 | 'moccasin': (255, 228, 181), 105 | 'navajowhite': (255, 222, 173), 106 | 'navy': ( 0, 0, 128), 107 | 'oldlace': (253, 245, 230), 108 | 'olive': (128, 128, 0), 109 | 'olivedrab': (107, 142, 35), 110 | 'orange': (255, 165, 0), 111 | 'orangered': (255, 69, 0), 112 | 'orchid': (218, 112, 214), 113 | 'palegoldenrod': (238, 232, 170), 114 | 'palegreen': (152, 251, 152), 115 | 'paleturquoise': (175, 238, 238), 116 | 'palevioletred': (219, 112, 147), 117 | 'papayawhip': (255, 239, 213), 118 | 'peachpuff': (255, 218, 185), 119 | 'peru': (205, 133, 63), 120 | 'pink': (255, 192, 203), 121 | 'plum': (221, 160, 221), 122 | 'powderblue': (176, 224, 230), 123 | 'purple': (128, 0, 128), 124 | 'red': (255, 0, 0), 125 | 'rosybrown': (188, 143, 143), 126 | 'royalblue': ( 65, 105, 225), 127 | 'saddlebrown': (139, 69, 19), 128 | 'salmon': (250, 128, 114), 129 | 'sandybrown': (244, 164, 96), 130 | 'seagreen': ( 46, 139, 87), 131 | 'seashell': ( 46, 139, 87), 132 | 'sienna': (160, 82, 45), 133 | 'silver': (192, 192, 192), 134 | 'skyblue': (135, 206, 235), 135 | 'slateblue': (106, 90, 205), 136 | 'slategray': (112, 128, 144), 137 | 'slategrey': (112, 128, 144), 138 | 'snow': (255, 250, 250), 139 | 'springgreen': ( 0, 255, 127), 140 | 'steelblue': ( 70, 130, 180), 141 | 'tan': (210, 180, 140), 142 | 'teal': ( 0, 128, 128), 143 | 'thistle': (216, 191, 216), 144 | 'tomato': (255, 99, 71), 145 | 'turquoise': ( 64, 224, 208), 146 | 'violet': (238, 130, 238), 147 | 'wheat': (245, 222, 179), 148 | 'white': (255, 255, 255), 149 | 'whitesmoke': (245, 245, 245), 150 | 'yellow': (255, 255, 0), 151 | 'yellowgreen': (154, 205, 50), 152 | 'transparent': ( 0, 0, 0, 0), 153 | } 154 | 155 | def color_name_to_rgb(name): 156 | """ 157 | Turn an English color name into an RGB value. 158 | 159 | lightBlue 160 | light-blue 161 | light blue 162 | 163 | are all valid and will produce the rgb value for lightblue. 164 | """ 165 | if type(name) == tuple: 166 | return name 167 | 168 | try: 169 | return color_names[name.lower().strip().replace('-', '').replace(' ', '')] 170 | except KeyError as exception: 171 | raise Oops(f"""You gave a color name we didn't understand: '{name}' 172 | If this our mistake, please let us know. Otherwise, try using the RGB number form of the color e.g. '(0, 255, 255)'. 173 | You can find the RGB form of a color on websites like this: https://www.rapidtables.com/web/color/RGB_Color.html\n""") from exception 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Play (beta) 2 | 3 | [![Try on repl.it](https://repl-badge.jajoosam.repl.co/try.png)](https://repl.it/@glench/Python-Play-sample-game) 4 | 5 | ## The easiest way to start coding games and graphics projects in Python 6 | 7 | Python Play is an open-source code library for the Python programming language that makes it as easy as possible to start making games. Here's the code to make a simple game using Play: 8 | 9 | ```python 10 | import play 11 | 12 | cat = play.new_text('=^.^=', font_size=70) 13 | 14 | @play.repeat_forever 15 | async def move_cat(): 16 | cat.x = play.random_number(-200, 200) 17 | cat.y = play.random_number(-200, 200) 18 | cat.color = play.random_color() 19 | 20 | cat.show() 21 | 22 | await play.timer(seconds=0.4) 23 | 24 | cat.hide() 25 | 26 | await play.timer(seconds=0.4) 27 | 28 | @cat.when_clicked 29 | def win_function(): 30 | cat.show() 31 | cat.words = 'You won!' 32 | 33 | play.start_program() 34 | ``` 35 | 36 | The code above makes a game where you have to click the cat to win: 37 | 38 | ![Clicking a cat game](example.gif) 39 | 40 | **[You can try playing and changing this game on repl.it!](https://repl.it/@glench/Python-Play-sample-game)** 41 | 42 | Python Play is an excellent choice for beginner programmers to get started with graphics programming. It was designed to have similar commands and simplicity to [MIT's Scratch](https://scratch.mit.edu) and is distinguished from such projects as Pygame, Arcade, or Pygame Zero because of its lack of boiler plate code, its easy-to-understand plain-english commands, and intuitive API. [Read more about its design at the bottom of this document](#why-use-python-play). 43 | 44 | # How to install Python Play 45 | 46 | Run the following command in your terminal: 47 | 48 | pip install replit-play 49 | 50 | Or you can just go to [repl.it](https://repl.it/@glench/Python-Play-sample-game) and you won't have to install anything :) 51 | 52 | # How to use Python Play 53 | 54 | All Python Play programs start with `import play` and end with `play.start_program()`, like this: 55 | 56 | ```python 57 | import play # this is the first line in the program 58 | 59 | 60 | 61 | play.start_program() # this is the last line in the program 62 | ``` 63 | 64 | All other commands go between those two commands. 65 | 66 | To try any of the following examples, go to **[repl.it and try pasting code in](https://repl.it/@glench/Replit-Play-Template)**. 67 | 68 | ## Commands 69 | 70 | The rest of this document is divided into the following sections: 71 | 72 | - [Basic Commands](#basic-commands) - Getting graphics, shapes, and text on the screen. Also changing the backdrop. 73 | - [Animation and Control Commands](#animation-and-control-commands) - Animating and controlling graphics, shapes, and text. 74 | - [Sprite Commands](#sprite-commands) - Controlling sprites. 75 | - [Mouse Commands](#mouse-commands) - Detecting mouse actions (clicks, movement). 76 | - [Keyboard Commands](#keyboard-commands) - Detecting keyboard actions. 77 | - [Physics Commands](#physics-commands) - Making physics objects. 78 | - [Other Useful Commands](#other-useful-commands) - General commands. 79 | - [Why use Python Play?](#why-use-python-play) - How this library is different from other graphics libraries. 80 | 81 | ## Basic Commands 82 | 83 | To get images or text on the screen, use the following commands. (Copy and paste the code below to try it out.) 84 | 85 | #### `play.new_box()` 86 | ```python 87 | box = play.new_box( 88 | color='black', 89 | x=0, 90 | y=0, 91 | width=100, 92 | height=200, 93 | border_color="light blue", 94 | border_width=10 95 | ) 96 | ``` 97 | 98 | This will put a tall, black box in the middle of the screen. 99 | 100 | If you want to change where the image is on the screen, try changing `x=0` (horizontal position) and `y=0` (vertical position). Just like Scratch, the middle of the screen is x=0, y=0. Increasing x moves the image right and decreasing x moves the image left. Likeswise, increasing y moves the image up and decreasing y moves the image down. You can also change the color by changing `'black'` to another color name, like `'orange'`. 101 | 102 | 103 | #### `play.new_image()` 104 | ```python 105 | character = play.new_image( 106 | image='character.png', 107 | x=0, 108 | y=0, 109 | angle=0, 110 | size=100, 111 | transparency=100 112 | ) 113 | ``` 114 | 115 | This will place an image in the middle of the screen. Make sure you have a file named `character.png` in your project files for the code above to work. You can find images online at sites like http://icons.iconarchive.com/icons/icojam/animals/64/01-bull-icon.png 116 | 117 | 118 | 119 | #### `play.new_text()` 120 | ```python 121 | greeting = play.new_text( 122 | words='hi there', 123 | x=0, 124 | y=0, 125 | angle=0, 126 | font=None, 127 | font_size=50, 128 | color='black', 129 | transparency=100 130 | ) 131 | ``` 132 | 133 | This will put some text on the screen. 134 | 135 | If you want to change the font, you'll need a font file (usually named something like `Arial.ttf`) in your project files. Then you can change `font=None` to `font='Arial.ttf'`. You can find font files at sites like [DaFont](https://www.dafont.com). 136 | 137 | 138 | 139 | #### `play.new_circle()` 140 | ```python 141 | ball = play.new_circle( 142 | color='black', 143 | x=0, 144 | y=0, 145 | radius=100, 146 | border_color="light blue", 147 | border_width=10, 148 | transparency=100 149 | ) 150 | ``` 151 | 152 | This will put a black circle in the middle of the screen. 153 | 154 | 155 | 156 | #### `play.new_line()` 157 | ```python 158 | line = play.new_line( 159 | color='black', 160 | x=0, 161 | y=0, 162 | length=100, 163 | angle=0, 164 | thickness=1, 165 | x1=None, 166 | y1=None 167 | ) 168 | ``` 169 | 170 | This will create a thin line on the screen. 171 | 172 | 173 | 174 | #### `play.set_backdrop()` 175 | You can change the background color with the `play.set_backdrop()` command: 176 | 177 | ```python 178 | play.set_backdrop('light blue') 179 | ``` 180 | 181 | There are [lots of named colors to choose from](https://upload.wikimedia.org/wikipedia/commons/2/2b/SVG_Recognized_color_keyword_names.svg). Additionally, if you want to set colors by RGB (Red Green Blue) values, you can do that like this: 182 | 183 | ```python 184 | # Sets the background to white. Each number can go from 0 to 255 185 | play.set_backdrop( (255, 255, 255) ) 186 | ``` 187 | 188 | Anywhere you can set a color in Python Play, you can do it using a named color like `'red'` or an RGB value above like `(255, 255, 255)` or even an RGBA value like `(0, 0, 0, 127)` (the fourth number is transparency from 0 to 255). You can get the current background color with `play.backdrop`. 189 | 190 | 191 | 192 | 193 | ## Animation and Control Commands 194 | 195 | #### `@play.repeat_forever` 196 | To make things move around, you can start by using `@play.repeat_forever`, like this: 197 | 198 | ```python 199 | cat = play.new_text('=^.^=') 200 | 201 | @play.repeat_forever 202 | def do(): 203 | 204 | cat.turn(10) 205 | ``` 206 | 207 | The above code will make the cat turn around forever. Sprites have other commands that you can see in the next section called Sprite Commands. 208 | 209 | #### `@play.when_program_starts` 210 | 211 | To make some code run just at the beginning of your project, use `@play.when_program_starts`, like this: 212 | 213 | ```python 214 | cat = play.new_text('=^.^=') 215 | 216 | @play.when_program_starts 217 | def do(): 218 | 219 | cat.turn(180) 220 | ``` 221 | 222 | This will make the cat turn upside down instantly when the program starts. 223 | 224 | 225 | #### `await play.timer(seconds=1)` 226 | 227 | To run code after a waiting period, you can use the `await play.timer()` command like this: 228 | 229 | ```python 230 | cat = play.new_text('=^.^=') 231 | 232 | @play.when_program_starts 233 | async def do(): 234 | 235 | cat.turn(180) 236 | await play.timer(seconds=2) 237 | cat.turn(180) 238 | ``` 239 | 240 | This will make the cat turn upside down instantly when the program starts, wait 2 seconds, then turn back up again. 241 | 242 | 243 | #### `play.repeat()` and `await play.animate()` 244 | 245 | To smoothly animate a character a certain number of times, you can use `play.repeat()` with `await play.animate()`, like this: 246 | 247 | 248 | ```python 249 | cat = play.new_text('=^.^=') 250 | 251 | @play.when_program_starts 252 | async def do(): 253 | for count in play.repeat(180): 254 | cat.turn(1) 255 | await play.animate() 256 | ``` 257 | 258 | This code will animate the cat turning upside down smoothly when the program starts. 259 | 260 | To break down the code: 261 | - `for count in play.repeat(180):` runs the code 180 times. 262 | - `cat.turn(1)` turns that cat 1 degree each time. 263 | - `await play.animate()` makes the cat animate smoothly. Without this command, the cat would just turn upside down instantly. 264 | 265 | Note: to use `await play.animate()` and `await play.timer()`, the word `async` must be included before `def` in your function definition. 266 | 267 | 268 | 269 | 270 | 271 | 272 | ## Sprite Commands 273 | 274 | 275 | #### Simple commands 276 | 277 | Sprites (images and text) have a few simple commands: 278 | 279 | - **`sprite.move(10)`** — moves the sprite 10 pixels in the direction it's facing (starts facing right). Use negative numbers (-10) to go backward. 280 | - **`sprite.turn(20)`** — Turns the sprite 20 degrees counter-clockwise. Use negative numbers (-20) to turn the other way. 281 | - **`sprite.go_to(other_sprite)`** — Makes `sprite` jump to another sprite named `other_sprite`'s position on the screen. Can also be used to make the sprite follow the mouse: `sprite.go_to(play.mouse)`. 282 | - **`sprite.go_to(x=100, y=50)`** — Makes `sprite` jump to x=100, y=50 (right and up a little). 283 | - **`sprite.point_towards(other_sprite)`** — Turns `sprite` so it points at another sprite called `other_sprite`. 284 | - **`sprite.point_towards(x=100, y=50)`** — Turns `sprite` so it points toward x=100, y=50 (right and up a little). 285 | - **`sprite.hide()`** — Hides `sprite`. It can't be clicked when it's hidden. 286 | - **`sprite.show()`** — Shows `sprite` if it's hidden. 287 | - **`sprite.clone()`** — Makes a copy or clone of the sprite and returns it. 288 | - **`sprite.remove()`** — Removes a sprite from the screen permanently. Calling sprite commands on a removed sprite won't do anything. 289 | - **`sprite.start_physics()`** — Turn on physics for a sprite. See the [Physics Commands section](#physics-commands) for details. 290 | - **`sprite.stop_physics()`** — Turn off physics for a sprite. See the [Physics Commands section](#physics-commands) for details. 291 | 292 | 293 | #### Properties 294 | 295 | Sprites also have properties that can be changed to change how the sprite looks. Here they are: 296 | 297 | - **`sprite.x`** — The sprite's horizontal position on the screen. Positive numbers are right, negative numbers are left. The default is 0. 298 | - **`sprite.y`** — The sprite's vertical position on the screen. Positive numbers are up, negative numbers are down. The default is 0. 299 | - **`sprite.size`** — How big the sprite is. The default is 100, but it can be made bigger or smaller. 300 | - **`sprite.angle`** — How much the sprite is turned. Positive numbers are counter-clockwise. The default is 0 degrees (pointed to the right). 301 | - **`sprite.transparency`** — How see-through the sprite is from 0 to 100. 0 is completely see-through, 100 is not see-through at all. The default is 100. 302 | - **`sprite.is_hidden`** — `True` if the sprite has been hidden with the `sprite.hide()` command. Otherwise `False`. 303 | - **`sprite.is_shown`** — `True` if the sprite has not been hidden with the `sprite.hide()` command. Otherwise `False`. 304 | - **`sprite.left`** — The x position of the left-most part of the sprite. 305 | - **`sprite.right`** — The x position of the right-most part of the sprite. 306 | - **`sprite.top`** — The y position of the top-most part of the sprite. 307 | - **`sprite.bottom`** — The y position of the bottom-most part of the sprite. 308 | - **`sprite.physics`** — Contains the physics properties of an object if physics has been turned on. The default is `None`. See the [Physics Commands section](#physics-commands) for details. 309 | 310 | 311 | 312 | Image-sprite-only properties: 313 | 314 | - **`sprite.image`** — The filename of the image shown. If `None` is provided initially, a blank image will show up. 315 | 316 | Text-sprite-only properties: 317 | 318 | - **`text.words`** — The displayed text content. The default is `'hi :)'`. 319 | - **`text.font`** — The filename of the font e.g. 'Arial.ttf'. The default is `None`, which will use a built-in font. 320 | - **`text.font_size`** — The text's size. The default is `50` (pt). 321 | - **`text.color`** — The text's color. The default is black. 322 | 323 | Box-sprite-only properties: 324 | - **`box.color`** — The color filling the box. The default is `black`. 325 | - **`box.width`** — The width of the box. The default is `100` pixels. 326 | - **`box.height`** — The height of the box. The default is `200` pixels. 327 | - **`box.border_width`** — The width of the box's border, the line around it. The default is `0`. 328 | - **`box.border_color`** — The color of the box's border. The default is `'light blue'`. 329 | 330 | If the box has a border, the box's total width, including the border, will be the width defined by the `width` property. 331 | 332 | Circle-sprite-only properties: 333 | - **`circle.color`** — The color filling the circle. The default is `black`. 334 | - **`circle.radius`** — How big the circle is, measured from the middle to the outside. The default is `100` pixels, making a 200-pixel-wide circle. 335 | - **`circle.border_width`** — The width of the circle's border, the line around it. The default is `0`. 336 | - **`circle.border_color`** — The color of the circle's border. The default is `'light blue'`. 337 | 338 | If the circle has a border, the circle's total width, including the border, will be the width defined by the `radius` property. 339 | 340 | 341 | Line-sprite-only properties: 342 | - **`line.color`** — The line's color. The default is `black`. 343 | - **`line.length`** — How long the line is. Defaults to `100` (pixels). 344 | - **`line.angle`** — The angle the line points in. Defaults to `0` (degrees). 345 | - **`line.x1`** — The `x` coordinate of the end of the line. 346 | - **`line.y1`** — The `y` coordinate of the end of the line. 347 | 348 | For lines, the `x` and `y` coordinates are where the start of the line is. You can set either the `length` and `angle` or the `x1` and `y1` properties to change where the line points. If you update one, the others will be updated automatically. 349 | 350 | 351 | 352 | These properties can changed to do the same things as the sprite commands above. For example, 353 | 354 | ```python 355 | sprite.go_to(other_sprite) 356 | 357 | # the line above is the same as the two lines below 358 | 359 | sprite.x = other_sprite.x 360 | sprite.y = other_sprite.y 361 | ``` 362 | 363 | You can change the properties to animate the sprites. The code below makes the cat turn around. 364 | 365 | ```python 366 | cat = play.new_text('=^.^=') 367 | 368 | @play.repeat_forever 369 | def do(): 370 | cat.angle += 1 371 | # the line above is the same as cat.turn(1) 372 | ``` 373 | 374 | 375 | 376 | 377 | #### Other info 378 | 379 | Sprites also have some other useful info: 380 | 381 | - **`sprite.width`** — Gets how wide the sprite is in pixels. 382 | - **`sprite.height`** — Gets how tall the sprite is in pixels. 383 | - **`sprite.distance_to(other_sprite)`** — Gets the distance in pixels to `other_sprite`. 384 | - **`sprite.distance_to(x=100, y=100)`** — Gets the distance to the point x=100, y=100. 385 | - **`sprite.is_clicked`** — `True` if the sprite has just been clicked, otherwise `False`. 386 | - **`sprite.is_touching(other_sprite)`** — Returns True if `sprite` is touching the `other_sprite`. Otherwise `False`. 387 | - **`sprite.is_touching(point)`** — Returns True if the sprite is touching the point (anything with an `x` and `y` coordinate). For example: `sprite.is_touching(play.mouse)` 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | ## Mouse Commands 396 | 397 | Working with the mouse in Python Play is easy. Here's a simple program that points a sprite at the mouse: 398 | 399 | ```python 400 | arrow = play.new_text('-->', font_size=100) 401 | 402 | @play.repeat_forever 403 | def do(): 404 | arrow.point_towards(play.mouse) 405 | ``` 406 | 407 | `play.mouse` has the following properties: 408 | 409 | - **`play.mouse.x`** — The horizontal x position of the mouse. 410 | - **`play.mouse.y`** — The vertical y position of the mouse. 411 | - **`play.mouse.is_clicked`** — `True` if the mouse is clicked down, or `False` if it's not. 412 | - **`play.mouse.is_touching(sprite)`** — Returns `True` if the mouse is touching a sprite, or `False` if it's not. 413 | 414 | 415 | 416 | #### `@sprite.when_clicked` 417 | 418 | Probably the easiest way to detect clicks is to use `@sprite.when_clicked`. 419 | 420 | In the program below, when the face is clicked it changes for 1 second then turns back to normal: 421 | 422 | ```python 423 | face = play.new_text('^.^', font_size=100) 424 | 425 | @face.when_clicked 426 | async def do(): 427 | face.words = '*o*' 428 | await play.timer(seconds=1) 429 | face.words = '^.^' 430 | ``` 431 | 432 | 433 | 434 | 435 | #### `@play.when_sprite_clicked()` 436 | 437 | If you wanted to run the same code when multiple sprites are clicked, you can use `@play.when_sprite_clicked()`: 438 | 439 | ```python 440 | face1 = play.new_text('^.^', x=-100, font_size=100) 441 | face2 = play.new_text('^_^', x=100, font_size=100) 442 | 443 | @play.when_sprite_clicked(face1, face2) # takes as many sprites as you want 444 | async def do(sprite): 445 | starting_words = sprite.words 446 | sprite.words = '*o*' 447 | await play.timer(seconds=1) 448 | sprite.words = starting_words 449 | ``` 450 | 451 | In the above program, clicking `face1` or `face2` will run the code for each sprite separately. Note that the function is defined with a parameter e.g. `def do(sprite):` instead of just `def do():`. 452 | 453 | 454 | #### `@play.mouse.when_clicked` or `@play.when_mouse_clicked` 455 | 456 | To run code when the mouse is clicked anywhere, use `@play.mouse.when_clicked` or `@play.when_mouse_clicked` (they do the same exact thing). 457 | 458 | In the code below, when a click is detected, the text will move to the click location and the coordinates will be shown: 459 | 460 | ```python 461 | text = play.new_text('0, 0') 462 | 463 | @play.mouse.when_clicked 464 | def do(): 465 | text.words = f'{play.mouse.x}, {play.mouse.y}' 466 | text.go_to(play.mouse) 467 | ``` 468 | 469 | #### `@play.mouse.when_click_released` or `@play.when_click_released` 470 | 471 | To run code when the mouse button is released, use `@play.mouse.when_click_released` `@play.when_click_released` (they do the same exact thing). 472 | 473 | In the code below, the cat can be dragged around when it's clicked by the mouse: 474 | 475 | ```python 476 | cat = play.new_text('=^.^= drag me!') 477 | cat.is_being_dragged = False 478 | 479 | @cat.when_clicked 480 | def do(): 481 | cat.is_being_dragged = True 482 | 483 | @play.mouse.when_click_released 484 | def do(): 485 | cat.is_being_dragged = False 486 | 487 | @play.repeat_forever 488 | def do(): 489 | if cat.is_being_dragged: 490 | cat.go_to(play.mouse) 491 | ``` 492 | 493 | 494 | 495 | 496 | 497 | 498 | ## Keyboard Commands 499 | 500 | 501 | #### `play.key_is_pressed()` 502 | 503 | You can use `play.key_is_pressed()` to detect keypresses. 504 | 505 | In the code below, pressing the `arrow` keys or `w/a/s/d` will make the cat go in the desired direction. 506 | 507 | ```python 508 | cat = play.new_text('=^.^=') 509 | 510 | @play.repeat_forever 511 | def do(): 512 | if play.key_is_pressed('up', 'w'): 513 | cat.y += 15 514 | if play.key_is_pressed('down', 's'): 515 | cat.y -= 15 516 | 517 | if play.key_is_pressed('right', 'd'): 518 | cat.x += 15 519 | if play.key_is_pressed('left', 'a'): 520 | cat.x -= 15 521 | ``` 522 | 523 | #### `@play.when_key_pressed()` 524 | 525 | You can use `@play.when_key_pressed()` to run code when specific keys are pressed. 526 | 527 | In the code below, pressing the `space` key will change the cat's face, and pressing the `enter` key will change it to a different face. 528 | 529 | ```python 530 | cat = play.new_text('=^.^=') 531 | 532 | @play.when_key_pressed('space', 'enter') # if either the space key or enter key are pressed... 533 | def do(key): 534 | if key == 'enter': 535 | cat.words = '=-.-=' 536 | if key == 'space': 537 | cat.words = '=*_*=' 538 | ``` 539 | 540 | 541 | 542 | 543 | #### `@play.when_any_key_pressed` 544 | 545 | If you just want to detect when any key is pressed, you can use `@play.when_any_key_pressed`. 546 | 547 | In the code below, any key you press will be displayed on the screen: 548 | 549 | ```python 550 | text = play.new_text('') 551 | 552 | @play.when_any_key_pressed 553 | def do(key): 554 | text.words = f'{key} pressed!' 555 | ``` 556 | 557 | #### `@play.when_key_released()` 558 | 559 | Exactly like `@play.when_key_pressed()` but runs the code when specific keys are released. 560 | 561 | In the code below, text will appear on screen only if the `up` arrow is pressed. 562 | 563 | ```python 564 | text = play.new_text('') 565 | 566 | @play.when_key_released('up') 567 | async def do(key): 568 | text.words = 'up arrow released!' 569 | await play.timer(seconds=1) 570 | text.words = '' 571 | ``` 572 | 573 | #### `@play.when_any_key_released` 574 | 575 | Exactly like `@play.when_any_key_pressed` but runs the code when any key is released. 576 | 577 | In the code below, the name of the most recently released key will show up on screen. 578 | 579 | ```python 580 | text = play.new_text('') 581 | 582 | @play.when_any_key_pressed 583 | def do(key): 584 | text.words = f'{key} key released!'' 585 | ``` 586 | 587 | 588 | 589 | ## Physics Commands 590 | 591 | Python Play uses the [Pymunk](http://www.pymunk.org/en/master/) physics library to turn sprites into physics objects that can collide with each other, fall with gravity, and more. 592 | 593 | ### `sprite.start_physics()` 594 | 595 | To turn a sprite into a physics object, use the `start_physics()` command: 596 | 597 | ```python 598 | sprite.start_physics(can_move=True, stable=False, x_speed=0, y_speed=0, obeys_gravity=True, bounciness=1, mass=10, friction=0.1) 599 | ``` 600 | 601 | This will cause the sprite to start being affected by gravity, collide with other sprites that have physics, and more. 602 | 603 | 604 | ### `sprite.physics` properties 605 | 606 | Once `sprite.start_physics()` has been called, the sprite will have a `sprite.physics` property. `sprite.physics` has the following properties: 607 | 608 | - **`sprite.physics.can_move`** — Whether the sprite can move around the screen (`True`) or is stuck in place (`False`). Defaults to `True`. 609 | - **`sprite.physics.stable`** — Whether the sprite is a stable object (one that can't be knocked about). A pong paddle is a stable object (`True`) but a box or ball that can be knocked around is not (`False`). Defaults to `False`. 610 | - **`sprite.physics.x_speed`** — How fast the sprite is moving horizontally (negative numbers mean the sprite moves to the left and positive numbers mean the sprite moves to the right). Defaults to `0`. 611 | - **`sprite.physics.y_speed`** — How fast the sprite is moving vertically (negative numbers mean the sprite moves down and positive numbers mean the sprite moves up). Defaults to `0`. 612 | - **`sprite.physics.obeys_gravity`** — If the sprite is affected by gravity. Defaults to `True`. 613 | - **`sprite.physics.bounciness`** — How bouncy the sprite is from 0 (doesn't bounce at all) to 1 (bounces a lot). Defaults to `1`. 614 | - **`sprite.physics.mass`** — How heavy the sprite is. Defaults to `10`. Heavier objects will knock lighter objects around more. 615 | - **`sprite.physics.friction`** — How much the sprite slides around on other objects. Starts at 0 (slides like on ice) to big numbers (very rough sprite that doesn't slide at all). Defaults to `0.1`. 616 | 617 | Changing any of these properties will immediately change how the sprite acts as a physics object. Try experimenting with all these properties if you don't fully understand them. 618 | 619 | `sprite.physics` also has two commands that could be helpful: 620 | 621 | - **`sprite.physics.pause()`** — Temporarily stop the sprite from being a physics object. The sprite's speed and other physics properties will be saved until the `unpause()` command is used. 622 | - **`sprite.physics.unpause()`** — Resume physics on a sprite that has been paused. It will continue with the exact speed and physics settings it had before `physics.pause()` was called. 623 | 624 | Calling `sprite.stop_physics()` will immediately stop the sprite from moving and colliding and `sprite.physics` will be set to `None`. 625 | 626 | 627 | ### `sprite.stop_physics()` 628 | 629 | To get a sprite to stop moving around and colliding, you can call `sprite.stop_physics`: 630 | 631 | ```python 632 | sprite.stop_physics() 633 | ``` 634 | 635 | This will immediately stop the sprite. 636 | 637 | 638 | ### `play.set_gravity()` 639 | 640 | To set how much gravity there is for sprites that have had `start_physics()` called on them, use the `play.set_gravity()` command: 641 | 642 | ```python 643 | play.set_gravity(vertical=-100, horizontal=None) 644 | ``` 645 | 646 | You can access the current gravity with `play.gravity.vertical` (default is `-100`) and `play.gravity.horizontal` (default is `0`). 647 | 648 | 649 | 650 | 651 | ## Other Useful Commands 652 | 653 | 654 | #### `play.screen` 655 | 656 | The way to get information about the screen. `play.screen` has these properties: 657 | 658 | - `play.screen.width` - Defaults to 800 (pixels total). Changing this will change the screen's size. 659 | - `play.screen.height` - Defaults to 600 (pixels total). Changing this will change the screen's size. 660 | - `play.screen.left` - The `x` coordinate for the left edge of the screen. 661 | - `play.screen.right` - The `x` coordinate for the right edge of the screen. 662 | - `play.screen.top` - The `y` coordinate for the top of the screen. 663 | - `play.screen.bottom` - The `y` coordinate for the bottom of the screen. 664 | 665 | 666 | #### `play.all_sprites` 667 | 668 | A list of all the sprites (images, shapes, text) in the program. 669 | 670 | 671 | #### `play.random_number()` 672 | 673 | A function that makes random numbers. 674 | 675 | If two whole numbers are given, `play.random_number()` will give a whole number back: 676 | 677 | ```python 678 | play.random_number(lowest=0, highest=100) 679 | 680 | # example return value: 42 681 | ``` 682 | (You can also do `play.random_number(0, 100)` without `lowest` and `highest`.) 683 | 684 | If non-whole numbers are given, non-whole numbers are given back: 685 | 686 | ```python 687 | play.random_number(0, 1.0) 688 | # example return value: 0.84 689 | ``` 690 | 691 | `play.random_number()` is also inclusive, which means `play.random_number(0,1)` will return `0` and `1`. 692 | 693 | 694 | #### `play.random_color()` 695 | 696 | Returns a random RGB color, including white and black. 697 | 698 | ```python 699 | play.random_color() 700 | # example return value: (201, 17, 142) 701 | ``` 702 | 703 | Each value varies from 0 to 255. 704 | 705 | #### `play.random_position()` 706 | 707 | Returns a random position on the screen. A position object has an `x` and `y` component. 708 | 709 | ```python 710 | text = play.text('WOO') 711 | @play.repeat_forever 712 | def do(): 713 | text.go_to(play.random_position()) 714 | 715 | # the above is equivalent to: 716 | position = play.random_position() 717 | text.x = position.x 718 | text.y = position.y 719 | ``` 720 | 721 | #### `play.repeat()` 722 | 723 | `play.repeat()` is the same as Python's built-in `range` function, except it starts at 1. 'Repeat' is just a friendlier and more descriptive name than 'range'. 724 | 725 | ```python 726 | list(play.repeat(10)) 727 | # return value: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 728 | ``` 729 | 730 | #### `await play.animate()` 731 | 732 | When used in a loop, this command will animate any sprite changes that happen. 733 | 734 | ```python 735 | cat = play.new_text('=^.^=') 736 | 737 | @play.when_program_starts 738 | async def do(): 739 | for count in play.repeat(360): 740 | cat.turn(1) 741 | await play.animate() 742 | ``` 743 | 744 | `await play.animate()` is the same as `await asyncio.sleep(0)` except it has a friendlier name for beginners. 745 | 746 | 747 | ## What's with all this `async`/`await` stuff? Is this Python? 748 | 749 | Yes, this is Python! Python added `async` and `await` as special keywords in Python 3.7. It's part of the [asyncio module](https://docs.python.org/3/library/asyncio.html). 750 | 751 | Using async functions means we can use the `await play.timer()` and `await play.animate()` functions, which makes some code a lot simpler and appear to run in-parallel, which new programmers find intuitive. 752 | 753 | ```python 754 | import play 755 | 756 | cat = play.new_text('=^.^=') 757 | 758 | # this code block uses async so it can use the 'await play.timer()' function 759 | @play.repeat_forever 760 | async def change_bg(): 761 | play.set_backdrop('pink') 762 | await play.timer(seconds=1) 763 | 764 | play.set_backdrop('purple') 765 | await play.timer(seconds=1) 766 | 767 | play.set_backdrop('light blue') 768 | await play.timer(seconds=1) 769 | 770 | # this code block doesn't need async because it doesn't have `await play.timer()` or `await play.animate()` 771 | @play.repeat_forever 772 | def do(): 773 | cat.turn(1) 774 | 775 | play.start_program() 776 | ``` 777 | 778 | In the above program, the backdrop will change and the cat will appear to turn at the same time even though the code is running single-threaded. 779 | 780 | The `async` keyword isn't necessary to write unless you want to use `await` functions. If you try to use an `await` command inside a non-async function, Python will show you an error like this: 781 | 782 | ``` 783 | File "example.py", line 31 784 | await play.timer(seconds=1) 785 | ^ 786 | SyntaxError: 'await' outside async function 787 | ``` 788 | To fix that error, just put `async` before `def`. 789 | 790 | If you don't understand any of this, it's generally safe to just include `async` before `def`. 791 | 792 | ## Why use Python Play? 793 | 794 | Python Play was designed to be an excellent starting point for brand new programmers. The goal of the project is to give someone that has never programmed before a compelling and successful experience in their first few minutes of programming. We aimed to make graphics programming as accessible as possible to as young an audience as possible. 795 | 796 | We found that many existing programming languages and graphics libraries presented unnecessary difficulties for new programmers — difficulties making simple things happen, confusing language, confusing program flow, unexplained concepts, etc. We know that even one initial disagreeable experience can turn people away from programming forever, and we wanted to prevent that outcome as much as possible. 797 | 798 | Python Play was inspired by [MIT's Scratch](https://scratch.mit.edu), which has introduced millions of children and adults to programming and helped them to create and share personally meaningful computational projects. In fact, Python Play's main designer worked on Scratch professionally for a brief period. But we found that for some learners, Scratch — with its graphical blocks and colorful interface — didn't feel like "real programming". For those learners wishing to use a mainstream textual programming language while removing the difficulties of graphics programming in these languages, we made Python Play. 799 | 800 | 801 | Python Play was designed with the following principles in mind: 802 | 803 | - No boilerplate - every line of code should do something meaningful and understandable. We want to limit the number of times a learner needs to ask "why do we have to include this line of code?" 804 | 805 | - As much as possible, commands should have immediate visual effects. For example, if a programmer types a `new_image` command the sprite should show up immediately on the screen. They shouldn't need to understand the invisible distinction between initializing a sprite and drawing the sprite. 806 | 807 | - Lines of code should be easily copy and pasted. 808 | 809 | - Command values should have descriptive labels that make it as clear as possible what the value means. Instead of `play.new_image('character.png', 50, 100)`, `play.new_image(image='character.png', x=50, y=100)`. 810 | 811 | - Use plain English as much as possible. For mathematical concepts, try to use language programmers might see in math classes. Try to use short names that are easier for younger people to type and spell. Make errors as clear and constructive as possible. Many of the commands and names were borrowed from Scratch, whose designers have spent decades working with children and observing what language makes sense to them. 812 | 813 | 814 | Python Play was also designed with a custom Repl.it IDE in mind (coming soon), one that significantly lowers the usability problems of programming (installing the language, using a text editor, using the terminal, running programs, showing which commands are available, etc). 815 | 816 | While the learning curve for Python and Python Play are still far from ideal for new programmers, we still think Python Play provides a great way for new programmers to start programming with graphics. 817 | 818 | <3 819 | -------------------------------------------------------------------------------- /play/play.py: -------------------------------------------------------------------------------- 1 | import os as _os 2 | import logging as _logging 3 | import warnings as _warnings 4 | import inspect as _inspect 5 | 6 | import pygame 7 | pygame.init() 8 | import pygame.gfxdraw 9 | import pymunk as _pymunk 10 | 11 | import asyncio as _asyncio 12 | import random as _random 13 | import math as _math 14 | from statistics import mean as _mean 15 | 16 | from .keypress import pygame_key_to_name as _pygame_key_to_name # don't pollute user-facing namespace with library internals 17 | from .color import color_name_to_rgb as _color_name_to_rgb 18 | from .exceptions import Oops, Hmm 19 | 20 | def _clamp(num, min_, max_): 21 | if num < min_: 22 | return min_ 23 | elif num > max_: 24 | return max_ 25 | return num 26 | 27 | def _point_touching_sprite(point, sprite): 28 | # todo: custom code for circle, line, rotated rectangley sprites 29 | return sprite.left <= point.x <= sprite.right and sprite.bottom <= point.y <= sprite.top 30 | 31 | def _sprite_touching_sprite(a, b): 32 | # todo: custom code for circle, line, rotated rectangley sprites 33 | # use physics engine if both sprites have physics on 34 | # if a.physics and b.physics: 35 | if a.left >= b.right or a.right <= b.left or a.top <= b.bottom or a.bottom >= b.top: return False 36 | return True 37 | 38 | 39 | 40 | class _screen(object): 41 | def __init__(self, width=800, height=600): 42 | self._width = width 43 | self._height = height 44 | 45 | @property 46 | def width(self): 47 | return self._width 48 | @width.setter 49 | def width(self, _width): 50 | self._width = _width 51 | 52 | _remove_walls() 53 | _create_walls() 54 | 55 | pygame.display.set_mode((self._width, self._height)) 56 | 57 | @property 58 | def height(self): 59 | return self._height 60 | @height.setter 61 | def height(self, _height): 62 | self._height = _height 63 | 64 | _remove_walls() 65 | _create_walls() 66 | 67 | pygame.display.set_mode((self._width, self._height)) 68 | 69 | @property 70 | def top(self): 71 | return self.height / 2 72 | 73 | @property 74 | def bottom(self): 75 | return self.height / -2 76 | 77 | @property 78 | def left(self): 79 | return self.width / -2 80 | 81 | @property 82 | def right(self): 83 | return self.width / 2 84 | 85 | screen = _screen() 86 | 87 | # _pygame_display = pygame.display.set_mode((screen_width, screen_height), pygame.DOUBLEBUF | pygame.OPENGL) 88 | _pygame_display = pygame.display.set_mode((screen.width, screen.height), pygame.DOUBLEBUF) 89 | pygame.display.set_caption("Python Play") 90 | 91 | 92 | class _mouse(object): 93 | def __init__(self): 94 | self.x = 0 95 | self.y = 0 96 | self._is_clicked = False 97 | self._when_clicked_callbacks = [] 98 | self._when_click_released_callbacks = [] 99 | 100 | @property 101 | def is_clicked(self): 102 | # this is a property instead of a method because if a new programmer does: 103 | # if play.mouse.is_clicked: # <-- forgetting parentheses causes bad behavior 104 | # ... 105 | # and is_clicked is a method (they forgot the parens), then it will always 106 | # return True. Better to eliminate the need for parens. 107 | return self._is_clicked 108 | 109 | def is_touching(self, other): 110 | return _point_touching_sprite(self, other) 111 | 112 | # @decorator 113 | def when_clicked(self, func): 114 | async_callback = _make_async(func) 115 | async def wrapper(): 116 | await async_callback() 117 | self._when_clicked_callbacks.append(wrapper) 118 | return wrapper 119 | 120 | # @decorator 121 | def when_click_released(self, func): 122 | async_callback = _make_async(func) 123 | async def wrapper(): 124 | await async_callback() 125 | self._when_click_released_callbacks.append(wrapper) 126 | return wrapper 127 | 128 | def distance_to(self, x=None, y=None): 129 | assert(not x is None) 130 | 131 | try: 132 | # x can either by a number or a sprite. If it's a sprite: 133 | x = x.x 134 | y = x.y 135 | except AttributeError: 136 | x = x 137 | y = y 138 | 139 | dx = self.x - x 140 | dy = self.y - y 141 | 142 | return _math.sqrt(dx**2 + dy**2) 143 | 144 | # @decorator 145 | def when_mouse_clicked(func): 146 | return mouse.when_clicked(func) 147 | # @decorator 148 | def when_click_released(func): 149 | return mouse.when_click_released(func) 150 | 151 | mouse = _mouse() 152 | 153 | 154 | all_sprites = [] 155 | 156 | _debug = True 157 | def debug(on_or_off): 158 | global _debug 159 | if on_or_off == 'on': 160 | _debug = True 161 | elif on_or_off == 'off': 162 | _debug = False 163 | 164 | backdrop = (255, 255, 255) 165 | def set_backdrop(color_or_image_name): 166 | global backdrop 167 | 168 | # I chose to make set_backdrop a function so that we can give 169 | # good error messages at the call site if a color isn't recognized. 170 | # If we didn't have a function and just set backdrop like this: 171 | # 172 | # play.backdrop = 'gbluereen' 173 | # 174 | # then any errors resulting from that statement would appear somewhere 175 | # deep in this library instead of in the user code. 176 | 177 | # this line will raise a useful exception 178 | _color_name_to_rgb(color_or_image_name) 179 | 180 | backdrop = color_or_image_name 181 | 182 | def random_number(lowest=0, highest=100): 183 | # if user supplies whole numbers, return whole numbers 184 | if type(lowest) == int and type(highest) == int: 185 | return _random.randint(lowest, highest) 186 | else: 187 | # if user supplied any floats, return decimals 188 | return round(_random.uniform(lowest, highest), 2) 189 | 190 | def random_color(): 191 | return (random_number(0, 255), random_number(0, 255), random_number(0, 255)) 192 | 193 | class _Position(object): 194 | def __init__(self, x, y): 195 | self.x = x 196 | self.y = y 197 | def __getitem__(self, indices): 198 | if indices == 0: 199 | return self.x 200 | elif indices == 1: 201 | return self.y 202 | raise IndexError() 203 | def __iter__(self): 204 | yield self.x 205 | yield self.y 206 | def __len__(self): 207 | return 2 208 | def __setitem__(self, i, value): 209 | if i == 0: 210 | self.x = value 211 | elif i == 1: 212 | self.y = value 213 | else: 214 | raise IndexError() 215 | 216 | def random_position(): 217 | """ 218 | Returns a random position on the screen. A position has an `x` and `y` e.g.: 219 | position = play.random_position() 220 | sprite.x = position.x 221 | sprite.y = position.y 222 | 223 | or equivalently: 224 | sprite.go_to(play.random_position()) 225 | """ 226 | return _Position( 227 | random_number(screen.left, screen.right), 228 | random_number(screen.bottom, screen.top) 229 | ) 230 | 231 | def _raise_on_await_warning(func): 232 | """ 233 | If someone doesn't put 'await' before functions that require 'await' 234 | like play.timer() or play.animate(), raise an exception. 235 | """ 236 | async def f(*args, **kwargs): 237 | with _warnings.catch_warnings(record=True) as w: 238 | await func(*args, **kwargs) 239 | for warning in w: 240 | str_message = warning.message.args[0] # e.g. "coroutine 'timer' was never awaited" 241 | if 'was never awaited' in str_message: 242 | unawaited_function_name = str_message.split("'")[1] 243 | raise Oops(f"""Looks like you forgot to put "await" before play.{unawaited_function_name} on line {warning.lineno} of file {warning.filename}. 244 | To fix this, just add the word 'await' before play.{unawaited_function_name} on line {warning.lineno} of file {warning.filename} in the function {func.__name__}.""") 245 | else: 246 | print(warning.message) 247 | return f 248 | 249 | def _make_async(func): 250 | """ 251 | Turn a non-async function into an async function. 252 | Used mainly in decorators like @repeat_forever. 253 | """ 254 | if _asyncio.iscoroutinefunction(func): 255 | # if it's already async just return it 256 | return _raise_on_await_warning(func) 257 | @_raise_on_await_warning 258 | async def async_func(*args, **kwargs): 259 | return func(*args, **kwargs) 260 | return async_func 261 | 262 | class _MetaGroup(type): 263 | def __iter__(cls): 264 | # items added via class variables, e.g. 265 | # class Button(play.Group): 266 | # text = play.new_text('click me') 267 | for item in cls.__dict__.values(): 268 | if isinstance(item, Sprite): 269 | yield item 270 | 271 | def __getattr__(cls, attr): 272 | """ 273 | E.g. 274 | class group(play.Group): 275 | t = play.new_text() 276 | group.move(10) # calls move(10) on all the group's sprites 277 | """ 278 | 279 | def f(*args, **kwargs): 280 | results = [] 281 | for sprite in cls: 282 | result = getattr(sprite, attr) 283 | if callable(result): 284 | result(*args, **kwargs) 285 | else: 286 | results.append(attr) 287 | if results: 288 | return results 289 | return f 290 | 291 | @property 292 | def x(cls): 293 | return _mean(sprite.x for sprite in cls) 294 | @x.setter 295 | def x(cls, new_x): 296 | x_offset = new_x - cls.x 297 | for sprite in cls: 298 | sprite.x += x_offset 299 | 300 | @property 301 | def y(cls): 302 | return _mean(sprite.y for sprite in cls) 303 | @y.setter 304 | def y(cls, new_y): 305 | y_offset = new_y - cls.y 306 | for sprite in cls: 307 | sprite.y += y_offset 308 | 309 | 310 | class Group(metaclass=_MetaGroup): 311 | """ 312 | A way to group sprites together. A group can either be made like this: 313 | 314 | class button(play.Group): 315 | bg = play.new_box(width=60, height=30) 316 | text = play.new_text('hi') 317 | 318 | or like this: 319 | 320 | bg = play.new_box(width=60, height=30) 321 | text = play.new_text('hi') 322 | button = play.new_group(bg, text) 323 | 324 | TODO: 325 | - Button.move() (make work with instance or class) 326 | - Button.angle = 10 (sets all sprite's angles to 10 in group) 327 | - for sprite in Button: (make iteration work) 328 | - play.new_group(bg=bg, text=text) (add keyword args) 329 | - group.append(), group.remove()? 330 | """ 331 | def __init__(self, *sprites): 332 | self.sprites_ = sprites 333 | 334 | 335 | @classmethod 336 | def sprites(cls): 337 | for item in cls.__dict__.values(): 338 | # items added via class variables, e.g. 339 | # class Button(play.Group): 340 | # text = play.new_text('click me') 341 | if isinstance(item, Sprite): 342 | yield item 343 | 344 | def sprites(self): 345 | for sprite in self.sprites_: 346 | yield sprite 347 | print(self.__class__.sprites) 348 | for sprite in type(self).sprites(): 349 | yield sprite 350 | 351 | def __iter__(self): 352 | for sprite in self.sprites: 353 | yield sprite 354 | 355 | def go_to(self, x_or_sprite, y): 356 | try: 357 | x = x_or_sprite.x 358 | y = x_or_sprite.y 359 | except AttributeError: 360 | x = x_or_sprite 361 | y = y 362 | 363 | max_x = max(sprite.x for sprite in self) 364 | min_x = min(sprite.x for sprite in self) 365 | max_y = max(sprite.y for sprite in self) 366 | min_y = min(sprite.y for sprite in self) 367 | 368 | center_x = (max_x - min_x) / 2 369 | center_y = (min_y - max_y) / 2 370 | offset_x = x - center_x 371 | offset_y = y - center_y 372 | 373 | for sprite in self: 374 | sprite.x += offset_x 375 | sprite.y += offset_y 376 | 377 | @property 378 | def right(self): 379 | return max(sprite.right for sprite in self) 380 | 381 | @property 382 | def left(self): 383 | return min(sprite.left for sprite in self) 384 | 385 | @property 386 | def width(self): 387 | return self.right - self.left 388 | 389 | 390 | def new_group(*sprites): 391 | return Group(*sprites) 392 | 393 | def new_image(image=None, x=0, y=0, size=100, angle=0, transparency=100): 394 | return Sprite(image=image, x=x, y=y, size=size, angle=angle, transparency=transparency) 395 | 396 | class Sprite(object): 397 | def __init__(self, image=None, x=0, y=0, size=100, angle=0, transparency=100): 398 | self._image = image or _os.path.join(_os.path.split(__file__)[0], 'blank_image.png') 399 | self._x = x 400 | self._y = y 401 | self._angle = angle 402 | self._size = size 403 | self._transparency = transparency 404 | 405 | self.physics = None 406 | self._is_clicked = False 407 | self._is_hidden = False 408 | 409 | 410 | self._compute_primary_surface() 411 | 412 | self._when_clicked_callbacks = [] 413 | 414 | all_sprites.append(self) 415 | 416 | 417 | def _compute_primary_surface(self): 418 | try: 419 | self._primary_pygame_surface = pygame.image.load(_os.path.join(self._image)) 420 | except pygame.error as exc: 421 | raise Oops(f"""We couldn't find the image file you provided named "{self._image}". 422 | If the file is in a folder, make sure you add the folder name, too.""") from exc 423 | self._primary_pygame_surface.set_colorkey((255,255,255, 255)) # set background to transparent 424 | 425 | self._should_recompute_primary_surface = False 426 | 427 | # always recompute secondary surface if the primary surface changes 428 | self._compute_secondary_surface(force=True) 429 | 430 | def _compute_secondary_surface(self, force=False): 431 | 432 | self._secondary_pygame_surface = self._primary_pygame_surface.copy() 433 | 434 | # transparency 435 | if self._transparency != 100 or force: 436 | try: 437 | # for text and images with transparent pixels 438 | array = pygame.surfarray.pixels_alpha(self._secondary_pygame_surface) 439 | array[:, :] = (array[:, :] * (self._transparency/100.)).astype(array.dtype) # modify surface pixels in-place 440 | del array # I think pixels are written when array leaves memory, so delete it explicitly here 441 | except Exception as e: 442 | # this works for images without alpha pixels in them 443 | self._secondary_pygame_surface.set_alpha(round((self._transparency/100.) * 255)) 444 | 445 | # scale 446 | if (self.size != 100) or force: 447 | ratio = self.size/100. 448 | self._secondary_pygame_surface = pygame.transform.scale( 449 | self._secondary_pygame_surface, 450 | (round(self._secondary_pygame_surface.get_width() * ratio), # width 451 | round(self._secondary_pygame_surface.get_height() * ratio))) # height 452 | 453 | 454 | # rotate 455 | if (self.angle != 0) or force: 456 | self._secondary_pygame_surface = pygame.transform.rotate(self._secondary_pygame_surface, self._angle) 457 | 458 | 459 | self._should_recompute_secondary_surface = False 460 | 461 | @property 462 | def is_clicked(self): 463 | return self._is_clicked 464 | 465 | def move(self, steps=3): 466 | angle = _math.radians(self.angle) 467 | self.x += steps * _math.cos(angle) 468 | self.y += steps * _math.sin(angle) 469 | 470 | def turn(self, degrees=10): 471 | self.angle += degrees 472 | 473 | @property 474 | def x(self): 475 | return self._x 476 | @x.setter 477 | def x(self, _x): 478 | prev_x = self._x 479 | self._x = _x 480 | if self.physics: 481 | self.physics._pymunk_body.position = self._x, self._y 482 | if prev_x != _x: 483 | # setting velocity makes the simulation more realistic usually 484 | self.physics._pymunk_body.velocity = _x - prev_x, self.physics._pymunk_body.velocity.y 485 | if self.physics._pymunk_body.body_type == _pymunk.Body.STATIC: 486 | _physics_space.reindex_static() 487 | 488 | @property 489 | def y(self): 490 | return self._y 491 | @y.setter 492 | def y(self, _y): 493 | prev_y = self._y 494 | self._y = _y 495 | if self.physics: 496 | self.physics._pymunk_body.position = self._x, self._y 497 | if prev_y != _y: 498 | # setting velocity makes the simulation more realistic usually 499 | self.physics._pymunk_body.velocity = self.physics._pymunk_body.velocity.x, _y - prev_y 500 | if self.physics._pymunk_body.body_type == _pymunk.Body.STATIC: 501 | _physics_space.reindex_static() 502 | 503 | @property 504 | def transparency(self): 505 | return self._transparency 506 | 507 | @transparency.setter 508 | def transparency(self, alpha): 509 | if not isinstance(alpha, float) and not isinstance(alpha, int): 510 | raise Oops(f"""Looks like you're trying to set {self}'s transparency to '{alpha}', which isn't a number. 511 | Try looking in your code for where you're setting transparency for {self} and change it a number. 512 | """) 513 | if alpha > 100 or alpha < 0: 514 | _warnings.warn(f"""The transparency setting for {self} is being set to {alpha} and it should be between 0 and 100. 515 | You might want to look in your code where you're setting transparency and make sure it's between 0 and 100. """, Hmm) 516 | 517 | 518 | self._transparency = _clamp(alpha, 0, 100) 519 | self._should_recompute_secondary_surface = True 520 | 521 | @property 522 | def image(self): 523 | return self._image 524 | 525 | @image.setter 526 | def image(self, image_filename): 527 | self._image = image_filename 528 | self._should_recompute_primary_surface = True 529 | 530 | @property 531 | def angle(self): 532 | return self._angle 533 | 534 | @angle.setter 535 | def angle(self, _angle): 536 | self._angle = _angle 537 | self._should_recompute_secondary_surface = True 538 | 539 | if self.physics: 540 | self.physics._pymunk_body.angle = _math.radians(_angle) 541 | 542 | @property 543 | def size(self): 544 | return self._size 545 | 546 | @size.setter 547 | def size(self, percent): 548 | self._size = percent 549 | self._should_recompute_secondary_surface = True 550 | if self.physics: 551 | self.physics._remove() 552 | self.physics._make_pymunk() 553 | 554 | def hide(self): 555 | self._is_hidden = True 556 | if self.physics: 557 | self.physics.pause() 558 | 559 | def show(self): 560 | self._is_hidden = False 561 | if self.physics: 562 | self.physics.unpause() 563 | 564 | @property 565 | def is_hidden(self): 566 | return self._is_hidden 567 | 568 | @is_hidden.setter 569 | def is_hidden(self, hide): 570 | self._is_hidden = hide 571 | 572 | @property 573 | def is_shown(self): 574 | return not self._is_hidden 575 | 576 | @is_shown.setter 577 | def is_shown(self, show): 578 | self._is_hidden = not show 579 | 580 | def is_touching(self, sprite_or_point): 581 | rect = self._secondary_pygame_surface.get_rect() 582 | if isinstance(sprite_or_point, Sprite): 583 | return _sprite_touching_sprite(sprite_or_point, self) 584 | else: 585 | return _point_touching_sprite(sprite_or_point, self) 586 | 587 | def point_towards(self, x, y=None): 588 | try: 589 | x, y = x.x, x.y 590 | except AttributeError: 591 | x, y = x, y 592 | self.angle = _math.degrees(_math.atan2(y-self.y, x-self.x)) 593 | 594 | 595 | def go_to(self, x=None, y=None): 596 | """ 597 | Example: 598 | 599 | # text will follow around the mouse 600 | text = play.new_text('yay') 601 | 602 | @play.repeat_forever 603 | async def do(): 604 | text.go_to(play.mouse) 605 | """ 606 | assert(not x is None) 607 | 608 | try: 609 | # users can call e.g. sprite.go_to(play.mouse), so x will be an object with x and y 610 | self.x = x.x 611 | self.y = x.y 612 | except AttributeError: 613 | self.x = x 614 | self.y = y 615 | 616 | def distance_to(self, x, y=None): 617 | assert(not x is None) 618 | 619 | try: 620 | # x can either be a number or a sprite. If it's a sprite: 621 | x1 = x.x 622 | y1 = x.y 623 | except AttributeError: 624 | x1 = x 625 | y1 = y 626 | 627 | dx = self.x - x1 628 | dy = self.y - y1 629 | 630 | return _math.sqrt(dx**2 + dy**2) 631 | 632 | 633 | def remove(self): 634 | if self.physics: 635 | self.physics._remove() 636 | all_sprites.remove(self) 637 | 638 | @property 639 | def width(self): 640 | return self._secondary_pygame_surface.get_width() 641 | 642 | @property 643 | def height(self): 644 | return self._secondary_pygame_surface.get_height() 645 | 646 | @property 647 | def right(self): 648 | return self.x + self.width/2 649 | @right.setter 650 | def right(self, x): 651 | self.x = x - self.width/2 652 | 653 | @property 654 | def left(self): 655 | return self.x - self.width/2 656 | @left.setter 657 | def left(self, x): 658 | self.x = x + self.width/2 659 | 660 | @property 661 | def top(self): 662 | return self.y + self.height/2 663 | @top.setter 664 | def top(self, y): 665 | self.y = y - self.height/2 666 | 667 | @property 668 | def bottom(self): 669 | return self.y - self.height/2 670 | @bottom.setter 671 | def bottom(self, y): 672 | self.y = y + self.height/2 673 | 674 | def _pygame_x(self): 675 | return self.x + (screen.width/2.) - (self._secondary_pygame_surface.get_width()/2.) 676 | 677 | def _pygame_y(self): 678 | return (screen.height/2.) - self.y - (self._secondary_pygame_surface.get_height()/2.) 679 | 680 | # @decorator 681 | def when_clicked(self, callback, call_with_sprite=False): 682 | async_callback = _make_async(callback) 683 | async def wrapper(): 684 | wrapper.is_running = True 685 | if call_with_sprite: 686 | await async_callback(self) 687 | else: 688 | await async_callback() 689 | wrapper.is_running = False 690 | wrapper.is_running = False 691 | self._when_clicked_callbacks.append(wrapper) 692 | return wrapper 693 | 694 | def _common_properties(self): 695 | # used with inheritance to clone 696 | return {'x': self.x, 'y': self.y, 'size': self.size, 'transparency': self.transparency, 'angle': self.angle} 697 | 698 | def clone(self): 699 | # TODO: make work with physics 700 | return self.__class__(image=self.image, **self._common_properties()) 701 | 702 | # def __getattr__(self, key): 703 | # # TODO: use physics as a proxy object so users can do e.g. sprite.x_speed 704 | # if not self.physics: 705 | # return getattr(self, key) 706 | # else: 707 | # return getattr(self.physics, key) 708 | 709 | # def __setattr__(self, name, value): 710 | # if not self.physics: 711 | # return setattr(self, name, value) 712 | # elif self.physics and name in : 713 | # return setattr(self.physics, name, value) 714 | 715 | 716 | def start_physics(self, can_move=True, stable=False, x_speed=0, y_speed=0, obeys_gravity=True, bounciness=1.0, mass=10, friction=0.1): 717 | if not self.physics: 718 | self.physics = _Physics( 719 | self, 720 | can_move, 721 | stable, 722 | x_speed, 723 | y_speed, 724 | obeys_gravity, 725 | bounciness, 726 | mass, 727 | friction, 728 | ) 729 | 730 | def stop_physics(self): 731 | self.physics._remove() 732 | self.physics = None 733 | 734 | _SPEED_MULTIPLIER = 10 735 | class _Physics(object): 736 | 737 | def __init__(self, sprite, can_move, stable, x_speed, y_speed, obeys_gravity, bounciness, mass, friction): 738 | """ 739 | 740 | Examples of objects with the different parameters: 741 | 742 | Blocks that can be knocked over (the default): 743 | can_move = True 744 | stable = False 745 | obeys_gravity = True 746 | Jumping platformer character: 747 | can_move = True 748 | stable = True (doesn't fall over) 749 | obeys_gravity = True 750 | Moving platform: 751 | can_move = True 752 | stable = True 753 | obeys_gravity = False 754 | Stationary platform: 755 | can_move = False 756 | (others don't matter) 757 | """ 758 | self.sprite = sprite 759 | self._can_move = can_move 760 | self._stable = stable 761 | self._x_speed = x_speed * _SPEED_MULTIPLIER 762 | self._y_speed = y_speed * _SPEED_MULTIPLIER 763 | self._obeys_gravity = obeys_gravity 764 | self._bounciness = bounciness 765 | self._mass = mass 766 | self._friction = friction 767 | 768 | self._make_pymunk() 769 | 770 | def _make_pymunk(self): 771 | mass = self.mass if self.can_move else 0 772 | 773 | # non-moving line shapes are platforms and it's easier to take care of them less-generically 774 | if not self.can_move and isinstance(self.sprite, line): 775 | self._pymunk_body = _physics_space.static_body.copy() 776 | self._pymunk_shape = _pymunk.Segment(self._pymunk_body, (self.sprite.x, self.sprite.y), (self.sprite.x1, self.sprite.y1), self.sprite.thickness) 777 | else: 778 | if self.stable: 779 | moment = _pymunk.inf 780 | elif isinstance(self.sprite, Circle): 781 | moment = _pymunk.moment_for_circle(mass, 0, self.sprite.radius, (0, 0)) 782 | elif isinstance(self.sprite, line): 783 | moment = _pymunk.moment_for_box(mass, (self.sprite.length, self.sprite.thickness)) 784 | else: 785 | moment = _pymunk.moment_for_box(mass, (self.sprite.width, self.sprite.height)) 786 | 787 | if self.can_move and not self.stable: 788 | body_type = _pymunk.Body.DYNAMIC 789 | elif self.can_move and self.stable: 790 | if self.obeys_gravity or _physics_space.gravity == 0: 791 | body_type = _pymunk.Body.DYNAMIC 792 | else: 793 | body_type = _pymunk.Body.KINEMATIC 794 | else: 795 | body_type = _pymunk.Body.STATIC 796 | self._pymunk_body = _pymunk.Body(mass, moment, body_type=body_type) 797 | 798 | if isinstance(self.sprite, line): 799 | self._pymunk_body.position = self.sprite.x + (self.sprite.x1 - self.sprite.x)/2, self.sprite.y + (self.sprite.y1 - self.sprite.y)/2 800 | else: 801 | self._pymunk_body.position = self.sprite.x, self.sprite.y 802 | 803 | self._pymunk_body.angle = _math.radians(self.sprite.angle) 804 | 805 | if self.can_move: 806 | self._pymunk_body.velocity = (self._x_speed, self._y_speed) 807 | 808 | if not self.obeys_gravity: 809 | self._pymunk_body.velocity_func = lambda body, gravity, damping, dt: None 810 | 811 | if isinstance(self.sprite, Circle): 812 | self._pymunk_shape = _pymunk.Circle(self._pymunk_body, self.sprite.radius, (0,0)) 813 | elif isinstance(self.sprite, line): 814 | self._pymunk_shape = _pymunk.Segment(self._pymunk_body, (self.sprite.x, self.sprite.y), (self.sprite.x1, self.sprite.y1), self.sprite.thickness) 815 | else: 816 | self._pymunk_shape = _pymunk.Poly.create_box(self._pymunk_body, (self.sprite.width, self.sprite.height)) 817 | 818 | self._pymunk_shape.elasticity = _clamp(self.bounciness, 0, .99) 819 | self._pymunk_shape.friction = self._friction 820 | _physics_space.add(self._pymunk_body, self._pymunk_shape) 821 | 822 | 823 | def clone(self, sprite): 824 | # TODO: finish filling out params 825 | return self.__class__(sprite=sprite, can_move=self.can_move, x_speed=self.x_speed, 826 | y_speed=self.y_speed, obeys_gravity=self.obeys_gravity) 827 | 828 | def pause(self): 829 | self._remove() 830 | def unpause(self): 831 | if not self._pymunk_body and not self._pymunk_shape: 832 | _physics_space.add(self._pymunk_body, self._pymunk_shape) 833 | def _remove(self): 834 | if self._pymunk_body: 835 | _physics_space.remove(self._pymunk_body) 836 | if self._pymunk_shape: 837 | _physics_space.remove(self._pymunk_shape) 838 | 839 | @property 840 | def can_move(self): 841 | return self._can_move 842 | @can_move.setter 843 | def can_move(self, _can_move): 844 | prev_can_move = self._can_move 845 | self._can_move = _can_move 846 | if prev_can_move != _can_move: 847 | self._remove() 848 | self._make_pymunk() 849 | 850 | @property 851 | def x_speed(self): 852 | return self._x_speed / _SPEED_MULTIPLIER 853 | @x_speed.setter 854 | def x_speed(self, _x_speed): 855 | self._x_speed = _x_speed * _SPEED_MULTIPLIER 856 | self._pymunk_body.velocity = self._x_speed, self._pymunk_body.velocity[1] 857 | 858 | @property 859 | def y_speed(self): 860 | return self._y_speed / _SPEED_MULTIPLIER 861 | @y_speed.setter 862 | def y_speed(self, _y_speed): 863 | self._y_speed = _y_speed * _SPEED_MULTIPLIER 864 | self._pymunk_body.velocity = self._pymunk_body.velocity[0], self._y_speed 865 | 866 | @property 867 | def bounciness(self): 868 | return self._bounciness 869 | @bounciness.setter 870 | def bounciness(self, _bounciness): 871 | self._bounciness = _bounciness 872 | self._pymunk_shape.elasticity = _clamp(self._bounciness, 0, .99) 873 | 874 | @property 875 | def stable(self): 876 | return self._stable 877 | @stable.setter 878 | def stable(self, _stable): 879 | prev_stable = self._stable 880 | self._stable = _stable 881 | if self._stable != prev_stable: 882 | self._remove() 883 | self._make_pymunk() 884 | 885 | @property 886 | def mass(self): 887 | return self._mass 888 | @mass.setter 889 | def mass(self, _mass): 890 | self._mass = _mass 891 | self._pymunk_body.mass = _mass 892 | 893 | @property 894 | def obeys_gravity(self): 895 | return self._obeys_gravity 896 | @obeys_gravity.setter 897 | def obeys_gravity(self, _obeys_gravity): 898 | self._obeys_gravity = _obeys_gravity 899 | if _obeys_gravity: 900 | self._pymunk_body.velocity_func = _pymunk.Body.update_velocity 901 | else: 902 | self._pymunk_body.velocity_func = lambda body, gravity, damping, dt: None 903 | 904 | class _Gravity(object): 905 | # TODO: make this default to vertical if horizontal is 0? 906 | vertical = -100 * _SPEED_MULTIPLIER 907 | horizontal = 0 908 | 909 | gravity = _Gravity() 910 | _physics_space = _pymunk.Space() 911 | _physics_space.sleep_time_threshold = 0.5 912 | _physics_space.idle_speed_threshold = 0 # pymunk estimates good threshold based on gravity 913 | _physics_space.gravity = gravity.horizontal, gravity.vertical 914 | 915 | def set_gravity(vertical=-100, horizontal=None): 916 | global gravity 917 | gravity.vertical = vertical*_SPEED_MULTIPLIER 918 | if horizontal != None: 919 | gravity.horizontal = horizontal*_SPEED_MULTIPLIER 920 | 921 | _physics_space.gravity = gravity.horizontal, gravity.vertical 922 | 923 | def _create_wall(a, b): 924 | segment = _pymunk.Segment(_physics_space.static_body, a, b, 0.0) 925 | segment.elasticity = 1.0 926 | segment.friction = .1 927 | _physics_space.add(segment) 928 | return segment 929 | _walls = [] 930 | def _create_walls(): 931 | _walls.append(_create_wall([screen.left, screen.top], [screen.right, screen.top])) # top 932 | _walls.append(_create_wall([screen.left, screen.bottom], [screen.right, screen.bottom])) # bottom 933 | _walls.append(_create_wall([screen.left, screen.bottom], [screen.left, screen.top])) # left 934 | _walls.append(_create_wall([screen.right, screen.bottom], [screen.right, screen.top])) # right 935 | _create_walls() 936 | def _remove_walls(): 937 | _physics_space.remove(_walls) 938 | _walls.clear() 939 | 940 | def new_box(color='black', x=0, y=0, width=100, height=200, border_color='light blue', border_width=0, angle=0, transparency=100, size=100): 941 | return Box(color=color, x=x, y=y, width=width, height=height, border_color=border_color, border_width=border_width, angle=angle, transparency=transparency, size=size) 942 | 943 | class Box(Sprite): 944 | def __init__(self, color='black', x=0, y=0, width=100, height=200, border_color='light blue', border_width=0, transparency=100, size=100, angle=0): 945 | self._x = x 946 | self._y = y 947 | self._width = width 948 | self._height = height 949 | self._color = color 950 | self._border_color = border_color 951 | self._border_width = border_width 952 | 953 | self._transparency = transparency 954 | self._size = size 955 | self._angle = angle 956 | self._is_clicked = False 957 | self._is_hidden = False 958 | self.physics = None 959 | 960 | self._when_clicked_callbacks = [] 961 | 962 | self._compute_primary_surface() 963 | 964 | all_sprites.append(self) 965 | 966 | def _compute_primary_surface(self): 967 | self._primary_pygame_surface = pygame.Surface((self._width, self._height), pygame.SRCALPHA) 968 | 969 | 970 | if self._border_width and self._border_color: 971 | # draw border rectangle 972 | self._primary_pygame_surface.fill(_color_name_to_rgb(self._border_color)) 973 | # draw fill rectangle over border rectangle at the proper position 974 | pygame.draw.rect(self._primary_pygame_surface, _color_name_to_rgb(self._color), (self._border_width,self._border_width,self._width-2*self._border_width,self._height-2*self.border_width)) 975 | 976 | else: 977 | self._primary_pygame_surface.fill(_color_name_to_rgb(self._color)) 978 | 979 | self._should_recompute_primary_surface = False 980 | self._compute_secondary_surface(force=True) 981 | 982 | 983 | ##### width ##### 984 | @property 985 | def width(self): 986 | return self._width 987 | 988 | @width.setter 989 | def width(self, _width): 990 | self._width = _width 991 | self._should_recompute_primary_surface = True 992 | 993 | 994 | ##### height ##### 995 | @property 996 | def height(self): 997 | return self._height 998 | 999 | @height.setter 1000 | def height(self, _height): 1001 | self._height = _height 1002 | self._should_recompute_primary_surface = True 1003 | 1004 | 1005 | ##### color ##### 1006 | @property 1007 | def color(self): 1008 | return self._color 1009 | 1010 | @color.setter 1011 | def color(self, _color): 1012 | self._color = _color 1013 | self._should_recompute_primary_surface = True 1014 | 1015 | ##### border_color ##### 1016 | @property 1017 | def border_color(self): 1018 | return self._border_color 1019 | 1020 | @border_color.setter 1021 | def border_color(self, _border_color): 1022 | self._border_color = _border_color 1023 | self._should_recompute_primary_surface = True 1024 | 1025 | ##### border_width ##### 1026 | @property 1027 | def border_width(self): 1028 | return self._border_width 1029 | 1030 | @border_width.setter 1031 | def border_width(self, _border_width): 1032 | self._border_width = _border_width 1033 | self._should_recompute_primary_surface = True 1034 | 1035 | def clone(self): 1036 | return self.__class__(color=self.color, width=self.width, height=self.height, border_color=self.border_color, border_width=self.border_width, **self._common_properties()) 1037 | 1038 | def new_circle(color='black', x=0, y=0, radius=100, border_color='light blue', border_width=0, transparency=100, size=100, angle=0): 1039 | return Circle(color=color, x=x, y=y, radius=radius, border_color=border_color, border_width=border_width, 1040 | transparency=transparency, size=size, angle=angle) 1041 | 1042 | class Circle(Sprite): 1043 | def __init__(self, color='black', x=0, y=0, radius=100, border_color='light blue', border_width=0, transparency=100, size=100, angle=0): 1044 | self._x = x 1045 | self._y = y 1046 | self._color = color 1047 | self._radius = radius 1048 | self._border_color = border_color 1049 | self._border_width = border_width 1050 | 1051 | self._transparency = transparency 1052 | self._size = size 1053 | self._angle = angle 1054 | self._is_clicked = False 1055 | self._is_hidden = False 1056 | self.physics = None 1057 | 1058 | self._when_clicked_callbacks = [] 1059 | 1060 | self._compute_primary_surface() 1061 | 1062 | all_sprites.append(self) 1063 | 1064 | def clone(self): 1065 | return self.__class__(color=self.color, radius=self.radius, border_color=self.border_color, border_width=self.border_width, **self._common_properties()) 1066 | 1067 | def _compute_primary_surface(self): 1068 | total_diameter = (self._radius + self._border_width) * 2 1069 | self._primary_pygame_surface = pygame.Surface((total_diameter, total_diameter), pygame.SRCALPHA) 1070 | 1071 | 1072 | center = self._radius + self._border_width 1073 | 1074 | if self._border_width and self._border_color: 1075 | # draw border circle 1076 | pygame.draw.circle(self._primary_pygame_surface, _color_name_to_rgb(self._border_color), (center, center), self._radius) 1077 | # draw fill circle over border circle 1078 | pygame.draw.circle(self._primary_pygame_surface, _color_name_to_rgb(self._color), (center, center), self._radius-self._border_width) 1079 | else: 1080 | pygame.draw.circle(self._primary_pygame_surface, _color_name_to_rgb(self._color), (center, center), self._radius) 1081 | 1082 | self._should_recompute_primary_surface = False 1083 | self._compute_secondary_surface(force=True) 1084 | 1085 | ##### color ##### 1086 | @property 1087 | def color(self): 1088 | return self._color 1089 | 1090 | @color.setter 1091 | def color(self, _color): 1092 | self._color = _color 1093 | self._should_recompute_primary_surface = True 1094 | 1095 | ##### radius ##### 1096 | @property 1097 | def radius(self): 1098 | return self._radius 1099 | 1100 | @radius.setter 1101 | def radius(self, _radius): 1102 | self._radius = _radius 1103 | self._should_recompute_primary_surface = True 1104 | if self.physics: 1105 | self.physics._pymunk_shape.unsafe_set_radius(self._radius) 1106 | 1107 | ##### border_color ##### 1108 | @property 1109 | def border_color(self): 1110 | return self._border_color 1111 | 1112 | @border_color.setter 1113 | def border_color(self, _border_color): 1114 | self._border_color = _border_color 1115 | self._should_recompute_primary_surface = True 1116 | 1117 | ##### border_width ##### 1118 | @property 1119 | def border_width(self): 1120 | return self._border_width 1121 | 1122 | @border_width.setter 1123 | def border_width(self, _border_width): 1124 | self._border_width = _border_width 1125 | self._should_recompute_primary_surface = True 1126 | 1127 | def new_line(color='black', x=0, y=0, length=None, angle=None, thickness=1, x1=None, y1=None, transparency=100, size=100): 1128 | return line(color=color, x=x, y=y, length=length, angle=angle, thickness=thickness, x1=x1, y1=y1, transparency=transparency, size=size) 1129 | 1130 | class line(Sprite): 1131 | def __init__(self, color='black', x=0, y=0, length=None, angle=None, thickness=1, x1=None, y1=None, transparency=100, size=100): 1132 | self._x = x 1133 | self._y = y 1134 | self._color = color 1135 | self._thickness = thickness 1136 | 1137 | # can set either (length, angle) or (x1,y1), otherwise a default is used 1138 | if length != None and angle != None: 1139 | self._length = length 1140 | self._angle = angle 1141 | self._x1, self._y1 = self._calc_endpoint() 1142 | elif x1 != None and y1 != None: 1143 | self._x1 = x1 1144 | self._y1 = y1 1145 | self._length, self._angle = self._calc_length_angle() 1146 | else: 1147 | # default values 1148 | self._length = length or 100 1149 | self._angle = angle or 0 1150 | self._x1, self._y1 = self._calc_endpoint() 1151 | 1152 | self._transparency = transparency 1153 | self._size = size 1154 | self._is_hidden = False 1155 | self._is_clicked = False 1156 | self.physics = None 1157 | 1158 | self._when_clicked_callbacks = [] 1159 | 1160 | self._compute_primary_surface() 1161 | 1162 | all_sprites.append(self) 1163 | 1164 | def clone(self): 1165 | return self.__class__(color=self.color, length=self.length, thickness=self.thickness, **self._common_properties()) 1166 | 1167 | def _compute_primary_surface(self): 1168 | # Make a surface that just contains the line and no white-space around the line. 1169 | # If line isn't horizontal, this surface will be drawn rotated. 1170 | width = self.length 1171 | height = self.thickness+1 1172 | 1173 | self._primary_pygame_surface = pygame.Surface((width, height), pygame.SRCALPHA) 1174 | # self._primary_pygame_surface.set_colorkey((255,255,255, 255)) # set background to transparent 1175 | 1176 | # # @hack 1177 | # if self.thickness == 1: 1178 | # pygame.draw.aaline(self._primary_pygame_surface, _color_name_to_rgb(self.color), (0,1), (width,1), True) 1179 | # else: 1180 | # pygame.draw.line(self._primary_pygame_surface, _color_name_to_rgb(self.color), (0,_math.floor(height/2)), (width,_math.floor(height/2)), self.thickness) 1181 | 1182 | 1183 | # line is actually drawn in _game_loop because coordinates work different 1184 | 1185 | self._should_recompute_primary_surface = False 1186 | self._compute_secondary_surface(force=True) 1187 | 1188 | def _compute_secondary_surface(self, force=False): 1189 | self._secondary_pygame_surface = self._primary_pygame_surface.copy() 1190 | 1191 | if force or self._transparency != 100: 1192 | self._secondary_pygame_surface.set_alpha(round((self._transparency/100.) * 255)) 1193 | 1194 | self._should_recompute_secondary_surface = False 1195 | 1196 | ##### color ##### 1197 | @property 1198 | def color(self): 1199 | return self._color 1200 | 1201 | @color.setter 1202 | def color(self, _color): 1203 | self._color = _color 1204 | self._should_recompute_primary_surface = True 1205 | 1206 | ##### thickness ##### 1207 | @property 1208 | def thickness(self): 1209 | return self._thickness 1210 | 1211 | @thickness.setter 1212 | def thickness(self, _thickness): 1213 | self._thickness = _thickness 1214 | self._should_recompute_primary_surface = True 1215 | 1216 | def _calc_endpoint(self): 1217 | radians = _math.radians(self._angle) 1218 | return self._length * _math.cos(radians) + self.x, self._length * _math.sin(radians) + self.y 1219 | 1220 | ##### length ##### 1221 | @property 1222 | def length(self): 1223 | return self._length 1224 | 1225 | @length.setter 1226 | def length(self, _length): 1227 | self._length = _length 1228 | self._x1, self._y1 = self._calc_endpoint() 1229 | self._should_recompute_primary_surface = True 1230 | 1231 | ##### angle ##### 1232 | @property 1233 | def angle(self): 1234 | return self._angle 1235 | 1236 | @angle.setter 1237 | def angle(self, _angle): 1238 | self._angle = _angle 1239 | self._x1, self._y1 = self._calc_endpoint() 1240 | if self.physics: 1241 | self.physics._pymunk_body.angle = _math.radians(_angle) 1242 | 1243 | 1244 | def _calc_length_angle(self): 1245 | dx = self.x1 - self.x 1246 | dy = self.y1 - self.y 1247 | 1248 | # TODO: this doesn't work at all 1249 | return _math.sqrt(dx**2 + dy**2), _math.degrees(_math.atan2(dy, dx)) 1250 | 1251 | ##### x1 ##### 1252 | @property 1253 | def x1(self): 1254 | return self._x1 1255 | 1256 | @x1.setter 1257 | def x1(self, _x1): 1258 | self._x1 = _x1 1259 | self._length, self._angle = self._calc_length_angle() 1260 | self._should_recompute_primary_surface = True 1261 | 1262 | ##### y1 ##### 1263 | @property 1264 | def y1(self): 1265 | return self._y1 1266 | 1267 | @y1.setter 1268 | def y1(self, _y1): 1269 | self._angle = _y1 1270 | self._length, self._angle = self._calc_length_angle() 1271 | self._should_recompute_primary_surface = True 1272 | 1273 | def new_text(words='hi :)', x=0, y=0, font=None, font_size=50, color='black', angle=0, transparency=100, size=100): 1274 | return text(words=words, x=x, y=y, font=font, font_size=font_size, color=color, angle=angle, transparency=transparency, size=size) 1275 | 1276 | class text(Sprite): 1277 | def __init__(self, words='hi :)', x=0, y=0, font=None, font_size=50, color='black', angle=0, transparency=100, size=100): 1278 | self._words = words 1279 | self._x = x 1280 | self._y = y 1281 | self._font = font 1282 | self._font_size = font_size 1283 | self._color = color 1284 | self._size = size 1285 | self._angle = angle 1286 | self.transparency = transparency 1287 | 1288 | self._is_clicked = False 1289 | self._is_hidden = False 1290 | self.physics = None 1291 | 1292 | self._compute_primary_surface() 1293 | 1294 | self._when_clicked_callbacks = [] 1295 | 1296 | all_sprites.append(self) 1297 | 1298 | def clone(self): 1299 | return self.__class__(words=self.words, font=self.font, font_size=self.font_size, color=self.color, **self._common_properties()) 1300 | 1301 | def _compute_primary_surface(self): 1302 | try: 1303 | self._pygame_font = pygame.font.Font(self._font, self._font_size) 1304 | except: 1305 | _warnings.warn(f"""We couldn't find the font file '{self._font}'. We'll use the default font instead for now. 1306 | To fix this, either set the font to None, or make sure you have a font file (usually called something like Arial.ttf) in your project folder.\n""", Hmm) 1307 | self._pygame_font = pygame.font.Font(None, self._font_size) 1308 | 1309 | self._primary_pygame_surface = self._pygame_font.render(self._words, True, _color_name_to_rgb(self._color)) 1310 | self._should_recompute_primary_surface = False 1311 | 1312 | self._compute_secondary_surface(force=True) 1313 | 1314 | 1315 | @property 1316 | def words(self): 1317 | return self._words 1318 | 1319 | @words.setter 1320 | def words(self, string): 1321 | self._words = str(string) 1322 | self._should_recompute_primary_surface = True 1323 | 1324 | @property 1325 | def font(self): 1326 | return self._font 1327 | 1328 | @font.setter 1329 | def font(self, font_name): 1330 | self._font = str(font_name) 1331 | self._should_recompute_primary_surface = True 1332 | 1333 | @property 1334 | def font_size(self): 1335 | return self._font_size 1336 | 1337 | @font_size.setter 1338 | def font_size(self, size): 1339 | self._font_size = size 1340 | self._should_recompute_primary_surface = True 1341 | 1342 | @property 1343 | def color(self): 1344 | return self._color 1345 | 1346 | @color.setter 1347 | def color(self, color_): 1348 | self._color = color_ 1349 | self._should_recompute_primary_surface = True 1350 | 1351 | 1352 | 1353 | 1354 | # @decorator 1355 | def when_sprite_clicked(*sprites): 1356 | def wrapper(func): 1357 | for sprite in sprites: 1358 | sprite.when_clicked(func, call_with_sprite=True) 1359 | return func 1360 | return wrapper 1361 | 1362 | pygame.key.set_repeat(200, 16) 1363 | _pressed_keys = {} 1364 | _keypress_callbacks = [] 1365 | _keyrelease_callbacks = [] 1366 | 1367 | # @decorator 1368 | def when_any_key_pressed(func): 1369 | if not callable(func): 1370 | raise Oops("""@play.when_any_key_pressed doesn't use a list of keys. Try just this instead: 1371 | 1372 | @play.when_any_key_pressed 1373 | async def do(key): 1374 | print("This key was pressed!", key) 1375 | """) 1376 | async_callback = _make_async(func) 1377 | async def wrapper(*args, **kwargs): 1378 | wrapper.is_running = True 1379 | await async_callback(*args, **kwargs) 1380 | wrapper.is_running = False 1381 | wrapper.keys = None 1382 | wrapper.is_running = False 1383 | _keypress_callbacks.append(wrapper) 1384 | return wrapper 1385 | 1386 | # @decorator 1387 | def when_key_pressed(*keys): 1388 | def decorator(func): 1389 | async_callback = _make_async(func) 1390 | async def wrapper(*args, **kwargs): 1391 | wrapper.is_running = True 1392 | await async_callback(*args, **kwargs) 1393 | wrapper.is_running = False 1394 | wrapper.keys = keys 1395 | wrapper.is_running = False 1396 | _keypress_callbacks.append(wrapper) 1397 | return wrapper 1398 | return decorator 1399 | 1400 | # @decorator 1401 | def when_any_key_released(func): 1402 | if not callable(func): 1403 | raise Oops("""@play.when_any_key_released doesn't use a list of keys. Try just this instead: 1404 | 1405 | @play.when_any_key_released 1406 | async def do(key): 1407 | print("This key was released!", key) 1408 | """) 1409 | async_callback = _make_async(func) 1410 | async def wrapper(*args, **kwargs): 1411 | wrapper.is_running = True 1412 | await async_callback(*args, **kwargs) 1413 | wrapper.is_running = False 1414 | wrapper.keys = None 1415 | wrapper.is_running = False 1416 | _keyrelease_callbacks.append(wrapper) 1417 | return wrapper 1418 | 1419 | # @decorator 1420 | def when_key_released(*keys): 1421 | def decorator(func): 1422 | async_callback = _make_async(func) 1423 | async def wrapper(*args, **kwargs): 1424 | wrapper.is_running = True 1425 | await async_callback(*args, **kwargs) 1426 | wrapper.is_running = False 1427 | wrapper.keys = keys 1428 | wrapper.is_running = False 1429 | _keyrelease_callbacks.append(wrapper) 1430 | return wrapper 1431 | return decorator 1432 | 1433 | def key_is_pressed(*keys): 1434 | """ 1435 | Returns True if any of the given keys are pressed. 1436 | 1437 | Example: 1438 | 1439 | @play.repeat_forever 1440 | async def do(): 1441 | if play.key_is_pressed('up', 'w'): 1442 | print('up or w pressed') 1443 | """ 1444 | # Called this function key_is_pressed instead of is_key_pressed so it will 1445 | # sound more english-like with if-statements: 1446 | # 1447 | # if play.key_is_pressed('w', 'up'): ... 1448 | 1449 | for key in keys: 1450 | if key in _pressed_keys.values(): 1451 | return True 1452 | return False 1453 | 1454 | _NUM_SIMULATION_STEPS = 3 1455 | def _simulate_physics(): 1456 | # more steps means more accurate simulation, but more processing time 1457 | for _ in range(_NUM_SIMULATION_STEPS): 1458 | # the smaller the simulation step, the more accurate the simulation 1459 | _physics_space.step(1/(60.0*_NUM_SIMULATION_STEPS)) 1460 | 1461 | _loop = _asyncio.get_event_loop() 1462 | _loop.set_debug(False) 1463 | 1464 | _keys_pressed_this_frame = [] 1465 | _keys_released_this_frame = [] 1466 | _keys_to_skip = (pygame.K_MODE,) 1467 | pygame.event.set_allowed([pygame.QUIT, pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION]) 1468 | _clock = pygame.time.Clock() 1469 | def _game_loop(): 1470 | _keys_pressed_this_frame.clear() # do this instead of `_keys_pressed_this_frame = []` to save a tiny bit of memory 1471 | _keys_released_this_frame.clear() 1472 | click_happened_this_frame = False 1473 | click_release_happened_this_frame = False 1474 | 1475 | _clock.tick(60) 1476 | for event in pygame.event.get(): 1477 | if event.type == pygame.QUIT or ( 1478 | event.type == pygame.KEYDOWN and event.key == pygame.K_q and ( 1479 | pygame.key.get_mods() & pygame.KMOD_META or pygame.key.get_mods() & pygame.KMOD_CTRL 1480 | )): 1481 | # quitting by clicking window's close button or pressing ctrl+q / command+q 1482 | _loop.stop() 1483 | return False 1484 | if event.type == pygame.MOUSEBUTTONDOWN: 1485 | click_happened_this_frame = True 1486 | mouse._is_clicked = True 1487 | if event.type == pygame.MOUSEBUTTONUP: 1488 | click_release_happened_this_frame = True 1489 | mouse._is_clicked = False 1490 | if event.type == pygame.MOUSEMOTION: 1491 | mouse.x, mouse.y = (event.pos[0] - screen.width/2.), (screen.height/2. - event.pos[1]) 1492 | if event.type == pygame.KEYDOWN: 1493 | if not (event.key in _keys_to_skip): 1494 | name = _pygame_key_to_name(event) 1495 | _pressed_keys[event.key] = name 1496 | _keys_pressed_this_frame.append(name) 1497 | if event.type == pygame.KEYUP: 1498 | if not (event.key in _keys_to_skip) and event.key in _pressed_keys: 1499 | _keys_released_this_frame.append(_pressed_keys[event.key]) 1500 | del _pressed_keys[event.key] 1501 | 1502 | 1503 | 1504 | ############################################################ 1505 | # @when_any_key_pressed and @when_key_pressed callbacks 1506 | ############################################################ 1507 | for key in _keys_pressed_this_frame: 1508 | for callback in _keypress_callbacks: 1509 | if not callback.is_running and (callback.keys is None or key in callback.keys): 1510 | _loop.create_task(callback(key)) 1511 | 1512 | ############################################################ 1513 | # @when_any_key_released and @when_key_released callbacks 1514 | ############################################################ 1515 | for key in _keys_released_this_frame: 1516 | for callback in _keyrelease_callbacks: 1517 | if not callback.is_running and (callback.keys is None or key in callback.keys): 1518 | _loop.create_task(callback(key)) 1519 | 1520 | 1521 | #################################### 1522 | # @mouse.when_clicked callbacks 1523 | #################################### 1524 | if click_happened_this_frame and mouse._when_clicked_callbacks: 1525 | for callback in mouse._when_clicked_callbacks: 1526 | _loop.create_task(callback()) 1527 | 1528 | ######################################## 1529 | # @mouse.when_click_released callbacks 1530 | ######################################## 1531 | if click_release_happened_this_frame and mouse._when_click_released_callbacks: 1532 | for callback in mouse._when_click_released_callbacks: 1533 | _loop.create_task(callback()) 1534 | 1535 | ############################# 1536 | # @repeat_forever callbacks 1537 | ############################# 1538 | for callback in _repeat_forever_callbacks: 1539 | if not callback.is_running: 1540 | _loop.create_task(callback()) 1541 | 1542 | ############################# 1543 | # physics simulation 1544 | ############################# 1545 | _loop.call_soon(_simulate_physics) 1546 | 1547 | 1548 | # 1. get pygame events 1549 | # - set mouse position, clicked, keys pressed, keys released 1550 | # 2. run when_program_starts callbacks 1551 | # 3. run physics simulation 1552 | # 4. compute new pygame_surfaces (scale, rotate) 1553 | # 5. run repeat_forever callbacks 1554 | # 6. run mouse/click callbacks (make sure more than one isn't running at a time) 1555 | # 7. run keyboard callbacks (make sure more than one isn't running at a time) 1556 | # 8. run when_touched callbacks 1557 | # 9. render background 1558 | # 10. render sprites (with correct z-order) 1559 | # 11. call event loop again 1560 | 1561 | 1562 | 1563 | _pygame_display.fill(_color_name_to_rgb(backdrop)) 1564 | 1565 | # BACKGROUND COLOR 1566 | # note: cannot use screen.fill((1, 1, 1)) because pygame's screen 1567 | # does not support fill() on OpenGL surfaces 1568 | # gl.glClearColor(_background_color[0], _background_color[1], _background_color[2], 1) 1569 | # gl.glClear(gl.GL_COLOR_BUFFER_BIT) 1570 | 1571 | for sprite in all_sprites: 1572 | 1573 | sprite._is_clicked = False 1574 | 1575 | if sprite.is_hidden: 1576 | continue 1577 | 1578 | ###################################################### 1579 | # update sprites with results of physics simulation 1580 | ###################################################### 1581 | if sprite.physics and sprite.physics.can_move: 1582 | 1583 | body = sprite.physics._pymunk_body 1584 | angle = _math.degrees(body.angle) 1585 | if isinstance(sprite, line): 1586 | sprite._x = body.position.x - (sprite.length/2) * _math.cos(angle) 1587 | sprite._y = body.position.y - (sprite.length/2) * _math.sin(angle) 1588 | sprite._x1 = body.position.x + (sprite.length/2) * _math.cos(angle) 1589 | sprite._y1 = body.position.y + (sprite.length/2) * _math.sin(angle) 1590 | # sprite._length, sprite._angle = sprite._calc_length_angle() 1591 | else: 1592 | if str(body.position.x) != 'nan': # this condition can happen when changing sprite.physics.can_move 1593 | sprite._x = body.position.x 1594 | if str(body.position.y) != 'nan': 1595 | sprite._y = body.position.y 1596 | 1597 | sprite.angle = angle # needs to be .angle, not ._angle so surface gets recalculated 1598 | sprite.physics._x_speed, sprite.physics._y_speed = body.velocity 1599 | 1600 | ################################# 1601 | # @sprite.when_clicked events 1602 | ################################# 1603 | if mouse.is_clicked and not type(sprite) == line: 1604 | if _point_touching_sprite(mouse, sprite): 1605 | # only run sprite clicks on the frame the mouse was clicked 1606 | if click_happened_this_frame: 1607 | sprite._is_clicked = True 1608 | for callback in sprite._when_clicked_callbacks: 1609 | if not callback.is_running: 1610 | _loop.create_task(callback()) 1611 | 1612 | 1613 | # do sprite image transforms (re-rendering images/fonts, scaling, rotating, etc) 1614 | 1615 | # we put it in the event loop instead of just recomputing immediately because if we do it 1616 | # synchronously then the data and rendered image may get out of sync 1617 | if sprite._should_recompute_primary_surface: 1618 | # recomputing primary surface also recomputes secondary surface 1619 | _loop.call_soon(sprite._compute_primary_surface) 1620 | elif sprite._should_recompute_secondary_surface: 1621 | _loop.call_soon(sprite._compute_secondary_surface) 1622 | 1623 | if type(sprite) == line: 1624 | # @hack: Line-drawing code should probably be in the line._compute_primary_surface function 1625 | # but the coordinates work different for lines than other sprites. 1626 | 1627 | 1628 | # x = screen.width/2 + sprite.x 1629 | # y = screen.height/2 - sprite.y - sprite.thickness 1630 | # _pygame_display.blit(sprite._secondary_pygame_surface, (x,y) ) 1631 | 1632 | x = screen.width/2 + sprite.x 1633 | y = screen.height/2 - sprite.y 1634 | x1 = screen.width/2 + sprite.x1 1635 | y1 = screen.height/2 - sprite.y1 1636 | if sprite.thickness == 1: 1637 | pygame.draw.aaline(_pygame_display, _color_name_to_rgb(sprite.color), (x,y), (x1,y1), True) 1638 | else: 1639 | pygame.draw.line(_pygame_display, _color_name_to_rgb(sprite.color), (x,y), (x1,y1), sprite.thickness) 1640 | else: 1641 | _pygame_display.blit(sprite._secondary_pygame_surface, (sprite._pygame_x(), sprite._pygame_y()) ) 1642 | 1643 | pygame.display.flip() 1644 | _loop.call_soon(_game_loop) 1645 | return True 1646 | 1647 | 1648 | async def timer(seconds=1.0): 1649 | """ 1650 | Wait a number of seconds. Used with the await keyword like this: 1651 | 1652 | @play.repeat_forever 1653 | async def do(): 1654 | await play.timer(seconds=2) 1655 | print('hi') 1656 | 1657 | """ 1658 | await _asyncio.sleep(seconds) 1659 | return True 1660 | 1661 | async def animate(): 1662 | 1663 | await _asyncio.sleep(0) 1664 | 1665 | # def _get_class_that_defined_method(meth): 1666 | # if inspect.ismethod(meth): 1667 | # for cls in inspect.getmro(meth.__self__.__class__): 1668 | # if cls.__dict__.get(meth.__name__) is meth: 1669 | # return cls 1670 | # meth = meth.__func__ # fallback to __qualname__ parsing 1671 | # if inspect.isfunction(meth): 1672 | # cls = getattr(inspect.getmodule(meth), 1673 | # meth.__qualname__.split('.', 1)[0].rsplit('.', 1)[0]) 1674 | # if isinstance(cls, type): 1675 | # return cls 1676 | # return getattr(meth, '__objclass__', None) # handle special descriptor objects 1677 | 1678 | _repeat_forever_callbacks = [] 1679 | # @decorator 1680 | def repeat_forever(func): 1681 | """ 1682 | Calls the given function repeatedly in the game loop. 1683 | 1684 | Example: 1685 | 1686 | text = play.new_text(words='hi there!', x=0, y=0, font='Arial.ttf', font_size=20, color='black') 1687 | 1688 | @play.repeat_forever 1689 | async def do(): 1690 | text.turn(degrees=15) 1691 | 1692 | """ 1693 | async_callback = _make_async(func) 1694 | async def repeat_wrapper(): 1695 | repeat_wrapper.is_running = True 1696 | await async_callback() 1697 | repeat_wrapper.is_running = False 1698 | 1699 | repeat_wrapper.is_running = False 1700 | _repeat_forever_callbacks.append(repeat_wrapper) 1701 | return func 1702 | 1703 | 1704 | _when_program_starts_callbacks = [] 1705 | # @decorator 1706 | def when_program_starts(func): 1707 | """ 1708 | Call code right when the program starts. 1709 | 1710 | Used like this: 1711 | 1712 | @play.when_program_starts 1713 | def do(): 1714 | print('the program just started!') 1715 | """ 1716 | async_callback = _make_async(func) 1717 | async def wrapper(*args, **kwargs): 1718 | return await async_callback(*args, **kwargs) 1719 | _when_program_starts_callbacks.append(wrapper) 1720 | return func 1721 | 1722 | def repeat(number_of_times): 1723 | """ 1724 | Repeat a set of commands a certain number of times. 1725 | 1726 | Equivalent to `range(1, number_of_times+1)`. 1727 | 1728 | Used like this: 1729 | 1730 | @play.repeat_forever 1731 | async def do(): 1732 | for count in play.repeat(10): 1733 | print(count) 1734 | """ 1735 | return range(1, number_of_times+1) 1736 | 1737 | def start_program(): 1738 | """ 1739 | Calling this function starts your program running. 1740 | 1741 | play.start_program() should almost certainly go at the very end of your program. 1742 | """ 1743 | for func in _when_program_starts_callbacks: 1744 | _loop.create_task(func()) 1745 | 1746 | _loop.call_soon(_game_loop) 1747 | try: 1748 | _loop.run_forever() 1749 | finally: 1750 | _logging.getLogger("asyncio").setLevel(_logging.CRITICAL) 1751 | pygame.quit() --------------------------------------------------------------------------------