├── .gitignore ├── COPYING ├── README ├── game └── __init__.py ├── images ├── enemy_01.png ├── enemy_02.png └── ship.png ├── index.html └── tests ├── browser.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | brython/* 2 | Brython3.0.2-20150102-140610/* 3 | Brython3.0.2-20150102-140610.tar.bz2 4 | *pyc 5 | *-swp 6 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ===================== 2 | BROWSER INVADERS 3 | ===================== 4 | 5 | Concept vintage game using Brython - Python on Browser project 6 | =============================================================== 7 | 8 | This is a complete game, if limited, using 9 | html5's Canvas with Python script, which is translated 10 | into javascript in runtime, on the client browser. 11 | 12 | How to Run 13 | ============ 14 | 15 | Unpack the game files, and 16 | in the same directory, download Brython 3.0.2 or later from 17 | https://github.com/brython-dev/brython/releases, unpack it, 18 | and rename the Brython directory (in the form of Brython- 19 | for example Brython3.0.2-20150102-140610) as simply "brython". 20 | Then start a web server so that a browser can fetch the files 21 | in the local computer. 22 | 23 | You can use "traditional" C python for this, 24 | typing: "python -m SimpleHTTPServer 8000" at the terminal. 25 | Afterwards. point your browser to http://localhost:8000, and enjoy 26 | 27 | 28 | License and Copyright 29 | ====================== 30 | 31 | This game is Copyright 2015 João S. O. Bueno, 32 | licensed under the LGPL license V 3.0 or later (see COPYING). 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /game/__init__.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from browser import document, html, timer, window 4 | 5 | 6 | SIZE = WIDTH, HEIGHT = 640, 480 7 | SCREEN = None 8 | CTX = None 9 | SHIPSIZE = 30 10 | 11 | SHIPCOLOR = "#0fd" 12 | SHOTCOLOR = "#d00" 13 | ENEMYCOLOR = "#fff" 14 | 15 | images = {} 16 | 17 | K_LEFT = 37 18 | K_RIGHT = 39 19 | K_UP = 38 20 | K_DOWN = 40 21 | K_SPACE = 32 22 | K_ESCAPE = 27 23 | 24 | KEYBOARD_LISTENER = None 25 | 26 | 27 | def init(): 28 | global SCREEN, CTX 29 | document.body.append(html.H1("Browser Invaders - a Python adventure")) 30 | SCREEN = html.CANVAS(width=WIDTH, height=HEIGHT) 31 | SCREEN.style = {"background": "black"} 32 | document.body.append(SCREEN) 33 | CTX = SCREEN.getContext("2d") 34 | for image_name in "ship", "enemy_01", "enemy_02": 35 | images[image_name] = html.IMG(src="images/{}.png".format(image_name)) 36 | print("loaded {}.png".format(image_name)) 37 | 38 | def gameover(): 39 | global game 40 | document.get(selector="h1")[0].text= "Game Over" 41 | 42 | 43 | class GameObject: 44 | def __init__(self, pos=None): 45 | self.pos = pos 46 | self.width = SHIPSIZE 47 | self.height = SHIPSIZE 48 | self.update_rect() 49 | 50 | self.image_counter = 0 51 | self.image_index = 0 52 | # Put object animation image names in a "self.image_names" list 53 | # and the starting image object in self.image 54 | 55 | def update(self): 56 | self.update_screen() 57 | 58 | self.image_counter += 1 59 | if hasattr(self, "image_names") and not self.image_counter % 40: 60 | self.image_index += 1 61 | if self.image_index >= len(self.image_names): 62 | self.image_index = 0 63 | self.image = images[self.image_names[self.image_index]] 64 | 65 | def update_screen(self): 66 | if hasattr(self, "image"): 67 | CTX.drawImage(self.image, self.pos[0], self.pos[1]) 68 | else: 69 | CTX.fillStyle = self.color 70 | # call with *self.rect not working with Brython 3.0.2 71 | CTX.fillRect(self.rect[0], self.rect[1], self.rect[2], self.rect[3]) 72 | 73 | def update_rect(self): 74 | self.rect = (self.pos[0], self.pos[1], self.width, self.height) 75 | 76 | def intersect(self, other): 77 | left = self.rect[0] 78 | right = left + self.rect[2] 79 | top = self.rect[1] 80 | botton = self.rect[1] + self.rect[3] 81 | return ( 82 | left >= other.rect[0] and left <= other.rect[0] + other.rect[2] or 83 | right >= other.rect[0] and right <= other.rect[0] + other.rect[2] or 84 | left <= other.rect[0] and right >= other.rect[0]) and ( 85 | top >= other.rect[1] and top <= other.rect[1] + other.rect[3] or 86 | botton >= other.rect[1] and botton <= other.rect[1] + other.rect[3] or 87 | top <= other.rect[1] and botton >= other.rect[1]) 88 | 89 | 90 | class Shot(GameObject): 91 | def __init__(self, pos): 92 | pos = [pos[0] - SHIPSIZE / 8, pos[1] - SHIPSIZE] 93 | self.speed = -15 94 | self.color = SHOTCOLOR 95 | super(Shot, self).__init__(pos) 96 | self.width = SHIPSIZE / 4 97 | self.update_rect() 98 | 99 | 100 | def update(self): 101 | super(Shot, self).update() 102 | self.pos[1] += self.speed 103 | if self.pos[1] <= 0: 104 | return False 105 | self.update_rect() 106 | return True 107 | 108 | def hit_any_enemy(self, enemy_list): 109 | finished = [] 110 | for i, enemy in enumerate(enemy_list): 111 | if self.intersect(enemy): 112 | finished.append(i) 113 | enemy.die() 114 | for i in reversed(finished): 115 | del enemy_list[i] 116 | 117 | 118 | class Enemy(GameObject): 119 | def __init__(self, game, pos, speed=5): 120 | self.game = game 121 | self.speed = speed 122 | self.color = ENEMYCOLOR 123 | super(Enemy, self).__init__(pos) 124 | self.image = images["enemy_01"] 125 | self.image_names = ["enemy_01", "enemy_02"] 126 | 127 | 128 | def update(self): 129 | super(Enemy, self).update() 130 | self.pos[0] += self.speed 131 | if self.pos[0] + self.width > WIDTH or self.pos[0] < 0: 132 | self.speed = -self.speed 133 | self.pos[0] += self.speed 134 | self.pos[1] += SHIPSIZE * 2 135 | if self.pos[1] >= HEIGHT: 136 | self.game.gameover() 137 | self.update_rect() 138 | 139 | def die(self): 140 | self.game.score += 100 141 | 142 | 143 | class Ship(GameObject): 144 | def __init__(self, game): 145 | global KEYBOARD_LISTENER 146 | self.game = game 147 | pos = [(WIDTH - SHIPSIZE) / 2, HEIGHT - SHIPSIZE] 148 | 149 | super(Ship, self).__init__(pos) 150 | 151 | self.aceleration = 2 152 | self.speed = 0 153 | 154 | self.max_speed = 7 155 | 156 | self.image = images["ship"] 157 | KEYBOARD_LISTENER = self.keypress 158 | document.body.addEventListener("keydown", KEYBOARD_LISTENER) 159 | 160 | def update(self): 161 | super(Ship, self).update() 162 | 163 | self.speed *= 0.95 164 | 165 | if self.speed > self.max_speed: 166 | self.speed = self.max_speed 167 | elif self.speed < -self.max_speed: 168 | self.speed = - self.max_speed 169 | 170 | self.pos[0] += self.speed 171 | if self.pos[0] > WIDTH - SHIPSIZE: 172 | self.speed = 0 173 | self.pos[0] = WIDTH - SHIPSIZE 174 | elif self.pos[0] < 0: 175 | self.speed = 0 176 | self.pos[0] = 0 177 | 178 | def keypress(self, event): 179 | if event.keyCode == K_RIGHT: 180 | self.speed += self.aceleration 181 | elif event.keyCode == K_LEFT: 182 | self.speed -= self.aceleration 183 | elif event.keyCode == K_UP: 184 | self.speed = 0 185 | elif event.keyCode == K_SPACE and len(self.game.shots) < 3: 186 | self.game.shots.append(Shot((self.pos[0] + SHIPSIZE / 2, self.pos[1]))) 187 | elif event.keyCode == K_ESCAPE: 188 | self.game.gameover() 189 | 190 | def remove(self): 191 | document.body.removeEventListener("keydown", KEYBOARD_LISTENER) 192 | 193 | 194 | class Game: 195 | 196 | high_score = 0 197 | 198 | def __init__(self): 199 | self.game_over_marker = False 200 | self.score = 0 201 | 202 | self.ship = Ship(self) 203 | self.shots = [] 204 | self.enemies = [] 205 | self.next_enemy_wave = self.enemy_wave_size() 206 | self.populate_enemies() 207 | self.scheduled_enemies = False 208 | 209 | 210 | def clear_screen(self): 211 | SCREEN.width = WIDTH 212 | 213 | def display_score(self): 214 | CTX.fillStyle = SHOTCOLOR 215 | CTX.font = "bold 40px Sans" 216 | CTX.fillText("%6d" % self.score, 10, 40) 217 | CTX.fillText("%6d" % self.high_score, WIDTH - 200, 40) 218 | 219 | def main(self): 220 | self.clear_screen() 221 | self.ship.update() 222 | 223 | for enemy in self.enemies: 224 | enemy.update() 225 | 226 | finished = [] 227 | 228 | for i, shot in enumerate(self.shots): 229 | if not shot.update(): 230 | finished.append(i) 231 | shot.hit_any_enemy(self.enemies) 232 | 233 | for i in reversed(finished): 234 | del self.shots[i] 235 | 236 | if not self.enemies and not self.scheduled_enemies: 237 | self.scheduled_enemies = True 238 | timer.set_timeout(self.populate_enemies, 2000) 239 | 240 | self.display_score() 241 | 242 | if not self.game_over_marker: 243 | timer.set_timeout(self.main, 30) 244 | else: 245 | self.display_game_over() 246 | 247 | def enemy_wave_size(self): 248 | quantity, speed = 12, 4 249 | while True: 250 | yield(quantity, speed) 251 | quantity += 5 252 | speed += 2 253 | 254 | def populate_enemies(self): 255 | quantity, speed = next(self.next_enemy_wave) 256 | 257 | self.ship.aceleration += 1 258 | self.ship.max_speed = max(self.ship.max_speed, speed + 2) 259 | 260 | enemies_per_line = 10 261 | x_pos = 20 262 | y_pos = 60 263 | x_step = (WIDTH - 20 - SHIPSIZE) / enemies_per_line 264 | enemies_current_line = 0 265 | odd_line = 1 266 | for i in range(quantity): 267 | self.enemies.append(Enemy(self, [x_pos, y_pos], speed * odd_line)) 268 | print(x_pos, y_pos) 269 | x_pos += x_step * odd_line 270 | enemies_current_line += 1 271 | if enemies_current_line >= enemies_per_line: 272 | enemies_current_line = 0 273 | odd_line *= -1 274 | y_pos += SHIPSIZE * 2 275 | x_pos = 20 if odd_line == 1 else (WIDTH - 20 - SHIPSIZE) 276 | print(odd_line, x_pos) 277 | 278 | self.scheduled_enemies = False 279 | 280 | 281 | def display_game_over(self): 282 | CTX.font = "bold 80px Sans" 283 | CTX.fillStyle = SHOTCOLOR 284 | message = "GAME OVER" 285 | text_width = CTX.measureText(message).width 286 | print("tamanho: ", text_width) 287 | text_left = (WIDTH - text_width) / 2 288 | text_botton = (HEIGHT / 2) + 40 289 | CTX.fillText(message, text_left, text_botton) 290 | 291 | def gameover(self): 292 | self.game_over_marker = True 293 | self.ship.remove() 294 | if self.score > self.high_score: 295 | self.__class__.high_score = self.score 296 | document.body.onclick = self.restart 297 | 298 | def restart(self, event): 299 | # TODO: remove event listener for game restart 300 | if self.game_over_marker: 301 | self.__init__() 302 | self.main() 303 | 304 | 305 | 306 | 307 | 308 | init() 309 | # menu() 310 | game = Game() 311 | game.main() 312 | -------------------------------------------------------------------------------- /images/enemy_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbueno/browser_invaders/fa307242d78279e308ab8c07101abd4a804a6666/images/enemy_01.png -------------------------------------------------------------------------------- /images/enemy_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbueno/browser_invaders/fa307242d78279e308ab8c07101abd4a804a6666/images/enemy_02.png -------------------------------------------------------------------------------- /images/ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsbueno/browser_invaders/fa307242d78279e308ab8c07101abd4a804a6666/images/ship.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/browser.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | html = Mock() 4 | window = Mock() 5 | timer = Mock() 6 | document = Mock() 7 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | To run these tests: set the Pythonpath to both the parent dir 4 | (where the module 'game' is found), and to this dir - 5 | (so that the "game" imports our mocked-up "browser" module) 6 | Run this file with (c)Python >= 3.3 7 | """ 8 | 9 | import unittest 10 | from unittest.mock import Mock 11 | import sys 12 | 13 | class TestGame(unittest.TestCase): 14 | def setUp(self): 15 | self.stdout = sys.stdout 16 | sys.stdout = open("/tmp/testoutput", "at") 17 | 18 | def tearDown(self): 19 | sys.stdout.close() 20 | sys.stdout = self.stdout 21 | sys.modules.pop("game", "") 22 | 23 | def test_game_module_is_loaded_with_no_syntax_error(self): 24 | import game 25 | 26 | def test_screen_is_created(self): 27 | import game 28 | self.assertIsInstance(game.SCREEN, Mock) 29 | self.assertIsInstance(game.SCREEN.getContext, Mock) 30 | game.SCREEN.getContext.assert_called_with("2d") 31 | 32 | if __name__ == "__main__": 33 | unittest.main() 34 | 35 | 36 | --------------------------------------------------------------------------------