├── .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 |
--------------------------------------------------------------------------------