pinball.py
Minimal pinball game in MicroPython based on code from Ten Minute Physics Tutorial “How to write a pinball simulation.”
Tutorial Links: https://matthias-research.github.io/pages/tenMinutePhysics/ https://youtu.be/NhVUCsXp-Uo
Gameplay Video: https://youtu.be/y0B3i_UmEU8
- Requires:
tft_config.py for display configuration. See examples/configs tft_buttons.py for button configuration. See examples/configs OR modify the code for your own display and buttons.
This file incorporates work covered by the following copyright and permission notice. Modifications and additions Copyright (c) 2022 Russ Hughes and released under the same terms as the original code.
Copyright 2021 Matthias Müller - Ten Minute Physics
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1"""
2pinball.py
3==========
4
5.. figure:: /_static/pinball.png
6 :align: center
7
8 Minimal pinball game
9
10Minimal pinball game in MicroPython based on code from Ten Minute Physics Tutorial "How to write a
11pinball simulation."
12
13Tutorial Links:
14https://matthias-research.github.io/pages/tenMinutePhysics/
15https://youtu.be/NhVUCsXp-Uo
16
17Gameplay Video:
18https://youtu.be/y0B3i_UmEU8
19
20Requires:
21 tft_config.py for display configuration. See examples/configs
22 tft_buttons.py for button configuration. See examples/configs
23 OR modify the code for your own display and buttons.
24
25This file incorporates work covered by the following copyright and permission notice.
26Modifications and additions Copyright (c) 2022 Russ Hughes and released under the same
27terms as the original code.
28
29
30Copyright 2021 Matthias Müller - Ten Minute Physics
31
32Permission is hereby granted, free of charge, to any person obtaining a copy of
33this software and associated documentation files (the "Software"), to deal in
34the Software without restriction, including without limitation the rights to
35use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
36of the Software, and to permit persons to whom the Software is furnished to do
37so, subject to the following conditions:
38
39The above copyright notice and this permission notice shall be included in all
40copies or substantial portions of the Software.
41
42THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
48SOFTWARE.
49"""
50
51# pylint: disable=missing-function-docstring, missing-class-docstring, invalid-name
52
53import time
54import random
55import math
56import vga1_8x8 as font
57import vga1_bold_16x32 as bold_font
58import gc9a01
59import tft_config
60import tft_buttons
61
62
63# ------------ set constants ------------
64
65#
66# Adjust these constants to change the game play
67#
68
69FLIPPER_SPEED = const(12) # flipper speed
70GRAVITY = -0.8 # gravity (negative y direction)
71
72# If the ball passes thru your flippers you may need to chage
73# the FPS value. You can uncomment the print statement in the
74# start_game() method to see how many frames per second your
75# device is running.
76
77FPS = 20 # frames per second
78
79BALL_SLOW = const(2) # launch speed for slow ball
80BALL_FAST = const(3) # launch speed for fast ball
81
82BACKGROUND = gc9a01.BLUE # My favorite color
83MULTIBALL_SCORE = const(30) # points to start multiball
84PRESSED = 0 # value when button is pressed
85
86# ------------ end constants ------------
87
88
89def color_wheel(WheelPos):
90 """returns a 565 color from the given position of the color wheel"""
91 WheelPos = (255 - WheelPos) % 255
92
93 if WheelPos < 85:
94 return gc9a01.color565(255 - WheelPos * 3, 0, WheelPos * 3)
95
96 if WheelPos < 170:
97 WheelPos -= 85
98 return gc9a01.color565(0, WheelPos * 3, 255 - WheelPos * 3)
99
100 WheelPos -= 170
101 return gc9a01.color565(WheelPos * 3, 255 - WheelPos * 3, 0)
102
103
104def text_color():
105 """return the next color from the color wheel"""
106 color = 0
107 while True:
108 yield color_wheel(color)
109 color = (color + 1) % 255
110
111
112def scale_x(pos):
113 """scale position to gc9a01.en x coordinate"""
114 return int(pos.x * SCALE_X) + HOFS_X
115
116
117def scale_y(pos):
118 """scale position to gc9a01.en y coordinate"""
119 return int(HEIGHT - pos.y * SCALE_Y) + HOFS_Y
120
121
122def pos_to_tuple(pos):
123 """convert pos to gc9a01.en (x,y) coordinate tuple"""
124 return (scale_x(pos), scale_y(pos))
125
126
127# vector math ---------------------------------------
128
129
130class Vector2:
131 def __init__(self, x=0.0, y=0.0, c=gc9a01.WHITE):
132 """create a new vector"""
133 self.x = x
134 self.y = y
135 self.color = c
136
137 def __str__(self):
138 """return a string representation of this vector"""
139 return f"({self.x}, {self.y})"
140
141 def set(self, v):
142 """set this vector to the values of another vector"""
143 self.x = v.x
144 self.y = v.y
145 self.color = v.color
146
147 def clone(self):
148 """return a copy of this vector"""
149 return Vector2(self.x, self.y, self.color)
150
151 def add(self, v, s=1.0):
152 """add a vector scaled by s to this vector"""
153 self.x += v.x * s
154 self.y += v.y * s
155 return self
156
157 def add_vectors(self, a, b):
158 """add two vectors"""
159 self.x = a.x + b.x
160 self.y = a.y + b.y
161 return self
162
163 def subtract(self, v, s=1.0):
164 """subtract a vector scaled by s from this vector"""
165 self.x -= v.x * s
166 self.y -= v.y * s
167 return self
168
169 def subtract_vectors(self, a, b):
170 """subtract two vectors"""
171 self.x = a.x - b.x
172 self.y = a.y - b.y
173 return self
174
175 def length(self):
176 """return the length of this vector"""
177 return math.sqrt(self.x * self.x + self.y * self.y)
178
179 def scale(self, s):
180 """scale this vector by s"""
181 self.x *= s
182 self.y *= s
183 return self
184
185 def dot(self, v):
186 """return the dot product of this vector and another vector"""
187 return self.x * v.x + self.y * v.y
188
189 def perp(self):
190 """return a perpendicular vector"""
191 return Vector2(-self.y, self.x, self.color)
192
193
194# ----------------------------------------------
195
196
197def closest_point_on_segment(p, a, b):
198 """return the closest point on the segment ab to point p"""
199 ab = Vector2()
200 ab.subtract_vectors(b, a)
201 t = ab.dot(ab)
202 if t == 0.0:
203 return a.clone()
204 t = max(0.0, min(1.0, (p.dot(ab) - a.dot(ab)) / t))
205 closest = a.clone()
206 return closest.add(ab, t)
207
208
209# object classes ---------------------------------------
210
211
212class Ball:
213 """Class to track a ball"""
214
215 def __init__(self, radius, mass, pos, vel, restitution):
216 """create a new ball"""
217 self.radius = radius
218 self.mass = mass
219 self.restitution = restitution
220 self.pos = pos.clone()
221 self.last = pos.clone()
222 self.vel = vel.clone()
223 self.size = int(radius * SCALE_RADIUS)
224 self.wheel = 0
225
226 def simulate(self, dt, gravity):
227 """update the ball position in dt seconds"""
228 self.last.set(self.pos)
229 self.vel.add(gravity, dt)
230 self.pos.add(self.vel, dt)
231
232
233class Obstacle:
234 """Class to contain an obstacle"""
235
236 def __init__(self, radius, pos, pushVel):
237 """create a new obstacle"""
238 self.radius = radius
239 self.size = int(radius * SCALE_RADIUS)
240 self.pos = pos.clone()
241 self.pushVel = pushVel
242
243
244class Flipper:
245 """Class to contain a flipper"""
246
247 def __init__(self, radius, pos, length, restAngle, maxRotation, angularVelocity):
248 """create a new flipper"""
249
250 # fixed
251 self.radius = radius
252 self.size = int(radius * SCALE_RADIUS)
253 self.pos = pos.clone()
254 self.length = length
255 self.restAngle = restAngle
256 self.maxRotation = abs(maxRotation)
257 self.sign = math.copysign(1, maxRotation)
258 self.angularVelocity = angularVelocity
259
260 # variable
261 self.rotation = 0.0
262 self.prevRotation = 0.0
263 self.currentAngularVelocity = 0.0
264 self.pressed = False
265
266 def simulate(self, dt):
267 """update the flipper position in dt seconds"""
268 self.prevRotation = self.rotation
269 if self.pressed:
270 self.rotation = min(
271 self.rotation + dt * self.angularVelocity, self.maxRotation
272 )
273 else:
274 self.rotation = max(self.rotation - dt * self.angularVelocity, 0.0)
275 self.currentAngularVelocity = (
276 self.sign * (self.rotation - self.prevRotation) / dt
277 )
278
279 def select(self, pos):
280 """return True if pos is within the flipper"""
281 d = Vector2()
282 d.subtract_vectors(self.pos, pos)
283 return d.length() < self.length
284
285 def getTip(self, rotation=None):
286 """return the tip position of the flipper"""
287 if rotation is None:
288 rotation = self.rotation
289 angle = self.restAngle + self.sign * rotation
290 direction = Vector2(math.cos(angle), math.sin(angle))
291 tip = self.pos.clone()
292 return tip.add(direction, self.length)
293
294 def rot_pos_in_dir(self, pos, direction):
295 """rotate pos in direction"""
296 pos.add(direction, self.length)
297 result = self.pos.clone()
298 result.add(Vector2(0.0, self.radius))
299 return result
300
301 def direction_from_rot(self, rotation):
302 """return the direction of the flipper at rotation"""
303 if rotation is None:
304 rotation = self.rotation
305 angle = self.restAngle + self.sign * rotation
306 cos_angle = math.cos(angle)
307 sin_angle = math.sin(angle)
308 return Vector2(cos_angle, sin_angle)
309
310 def draw(self, rotation=None, color=gc9a01.WHITE):
311 """draw the flipper"""
312 if rotation is None:
313 rotation = self.rotation
314 angle = self.restAngle + self.sign * rotation
315 direction = Vector2(math.cos(angle), math.sin(angle))
316 tip = self.pos.clone()
317 tip.add(direction, self.length)
318
319 direction = self.direction_from_rot(rotation)
320 tip = self.pos.clone()
321 tip_top = self.rot_pos_in_dir(tip, direction)
322 tip_top.add(direction, self.length)
323
324 tip_bot = self.pos.clone()
325 tip_bot.add(Vector2(0.0, -self.radius))
326 base_top = self.rot_pos_in_dir(tip_bot, direction)
327 base_top.add(direction, 0)
328
329 base_bot = self.pos.clone()
330 base_bot.add(Vector2(0.0, -self.radius))
331 base_bot.add(direction, 0)
332
333 tft.fill_circle(scale_x(self.pos), scale_y(self.pos), self.size, color)
334
335 tft.line(
336 scale_x(base_top),
337 scale_y(base_top),
338 scale_x(tip_top),
339 scale_y(tip_top),
340 color,
341 )
342
343 tft.line(
344 scale_x(self.pos), scale_y(self.pos), scale_x(tip), scale_y(tip), color
345 )
346
347 tft.line(
348 scale_x(base_bot),
349 scale_y(base_bot),
350 scale_x(tip_bot),
351 scale_y(tip_bot),
352 color,
353 )
354
355 tft.fill_circle(scale_x(tip), scale_y(tip), self.size, color)
356
357
358class Table:
359 """Class to contain the table"""
360
361 def __init__(self):
362 """create a new table"""
363 self.gravity = Vector2(0.0, GRAVITY)
364 self.dt = 1.0 / FPS
365 self.ticks = int(self.dt * 1000)
366 self.game_over = False
367 self.score = 0
368 self.multiball = 0
369 self.ball = 3
370 self.gutter = 0
371 self.balls = []
372
373 self.border = [Vector2(0.74, 0.25), Vector2(0.995, 0.4), Vector2(0.995, 1.4)]
374
375 # draw arc from left wall to right wall using line segments
376 arcRadius = 0.5
377 arcCenter = Vector2(0.5, 1.375)
378 arcStartAngle = 0
379 arcEndAngle = math.pi
380 arcSegments = 11
381 arcAngleStep = (arcEndAngle - arcStartAngle) / arcSegments
382 for i in range(arcSegments + 1):
383 angle = arcStartAngle + i * arcAngleStep
384 x = arcCenter.x + arcRadius * math.cos(angle)
385 y = arcCenter.y + arcRadius * math.sin(angle)
386 self.border.append(Vector2(x, y))
387
388 self.border.append(Vector2(0, 0.4))
389 self.border.append(Vector2(0.26, 0.25))
390 self.gutter = len(self.border)
391 self.border.append(Vector2(0.26, 0.0)) # gutter floor
392 self.border.append(Vector2(0.74, 0.0)) # gutter floor
393
394 # convert border to scaled polygon
395 self.wall = list(map(pos_to_tuple, self.border))
396 self.wall.append(pos_to_tuple(self.border[0]))
397
398 # obstacles
399 self.obstacles = [
400 Obstacle(0.04, Vector2(0.10, 1.68), 0.8),
401 Obstacle(0.08, Vector2(0.5, 1.45, gc9a01.RED), 1.5),
402 Obstacle(0.08, Vector2(0.74, 1.2, gc9a01.RED), 1.5),
403 Obstacle(0.08, Vector2(0.26, 1.2, gc9a01.RED), 1.5),
404 Obstacle(0.08, Vector2(0.5, 0.95, gc9a01.RED), 1.5),
405 Obstacle(0.04, Vector2(0.13, 0.8, gc9a01.YELLOW), 1.3),
406 Obstacle(0.04, Vector2(0.87, 0.8, gc9a01.YELLOW), 1.3),
407 Obstacle(0.04, Vector2(0.15, 0.6, gc9a01.GREEN), 1.2),
408 Obstacle(0.04, Vector2(0.85, 0.6, gc9a01.GREEN), 1.2),
409 ]
410
411 # flippers
412 radius = 0.035
413 length = 0.185
414 maxRotation = 1.0
415 restAngle = 0.5
416 angularVelocity = FLIPPER_SPEED
417
418 left_pos = Vector2(0.26, 0.22)
419 right_pos = Vector2(0.74, 0.22)
420
421 self.flippers = [
422 Flipper(radius, left_pos, length, -restAngle, maxRotation, angularVelocity),
423 Flipper(
424 radius,
425 right_pos,
426 length,
427 math.pi + restAngle,
428 -maxRotation,
429 angularVelocity,
430 ),
431 ]
432
433 def reset(self):
434 """reset the table"""
435 self.game_over = False
436 self.ball = 3
437 self.score = 0
438
439 print_right("M ", 0.25)
440
441 update_score()
442 update_ball()
443
444 def add_ball(self):
445 """add a ball to the table"""
446 radius = 0.03
447 mass = math.pi * radius * radius
448 pos = Vector2(0.95, 0.5, gc9a01.WHITE)
449 vel = Vector2(0, random.uniform(BALL_SLOW, BALL_FAST))
450 self.balls.append(Ball(radius, mass, pos, vel, 0.2))
451
452 def ball_reset(self):
453 """reset the ball"""
454 table.multiball = 0
455 self.draw_border()
456 self.draw_obstacles()
457 self.draw_flippers()
458
459 update_ball()
460 update_score()
461 ball_countdown()
462 self.add_ball()
463
464 def draw_border(self):
465 """draw the walls of the table"""
466 tft.polygon(self.wall, 0, 0, gc9a01.WHITE)
467
468 def draw_balls(self):
469 """draw the balls on the table"""
470 for ball in self.balls:
471 x = scale_x(ball.pos)
472 y = scale_y(ball.pos)
473
474 tft.fill_circle(
475 scale_x(ball.last), scale_y(ball.last), ball.size, BACKGROUND
476 )
477
478 tft.fill_circle(x, y, ball.size, ball.pos.color)
479
480 def draw_obstacles(self):
481 """draw the obstacles on the table"""
482 for obstacle in self.obstacles:
483 tft.fill_circle(
484 scale_x(obstacle.pos),
485 scale_y(obstacle.pos),
486 obstacle.size,
487 obstacle.pos.color,
488 )
489
490 def draw_flippers(self):
491 """draw the flippers on the table"""
492 for flipper in self.flippers:
493 if flipper.currentAngularVelocity != 0:
494 flipper.draw(flipper.prevRotation, BACKGROUND)
495 flipper.draw()
496
497 def simulate(self):
498 """simulate the table"""
499 for flipper in self.flippers:
500 flipper.simulate(self.dt)
501
502 for i, ball in enumerate(self.balls):
503 ball.simulate(self.dt, self.gravity)
504
505 for j in range(i + 1, len(self.balls)):
506 ball2 = self.balls[j]
507 handle_ball_ball_collision(ball, ball2)
508
509 for obstacle in self.obstacles:
510 handle_ball_obstacle_collision(ball, obstacle)
511
512 for flipper in self.flippers:
513 handle_ball_flipper_collision(ball, flipper)
514
515 if handle_ball_border_collision(ball, self.border) == self.gutter:
516 tft.fill_circle(
517 scale_x(ball.last), scale_y(ball.last), ball.size, BACKGROUND
518 )
519
520 balls = len(self.balls)
521 if balls == 1:
522 self.ball -= 1
523 update_ball()
524
525 if self.ball > 0:
526 self.balls.remove(ball)
527 if balls == 1:
528 self.ball_reset()
529 else:
530 self.game_over = True
531
532
533# ------------ collision handling ------------
534
535
536def handle_ball_ball_collision(ball1, ball2):
537 """handle a collision between two balls"""
538 restitution = min(ball1.restitution, ball2.restitution)
539 direction = Vector2()
540 direction.subtract_vectors(ball2.pos, ball1.pos)
541 d = direction.length()
542 if d == 0.0 or d >= ball1.radius + ball2.radius:
543 return
544
545 tft.fill_circle(scale_x(ball1.pos), scale_y(ball1.pos), ball1.size, BACKGROUND)
546 tft.fill_circle(scale_x(ball2.pos), scale_y(ball2.pos), ball2.size, BACKGROUND)
547
548 direction.scale(1.0 / d)
549
550 corr = (ball1.radius + ball2.radius - d) / 2.0
551 ball1.pos.add(direction, -corr)
552 ball2.pos.add(direction, corr)
553
554 v1 = ball1.vel.dot(direction)
555 v2 = ball2.vel.dot(direction)
556
557 m1 = ball1.mass
558 m2 = ball2.mass
559
560 newV1 = (m1 * v1 + m2 * v2 - m2 * (v1 - v2) * restitution) / (m1 + m2)
561 newV2 = (m1 * v1 + m2 * v2 - m1 * (v2 - v1) * restitution) / (m1 + m2)
562
563 ball1.vel.add(direction, newV1 - v1)
564 ball2.vel.add(direction, newV2 - v2)
565
566
567# ------------------------
568
569
570def handle_ball_obstacle_collision(ball, obstacle):
571 """handle a collision between a ball and an obstacle"""
572 direction = Vector2()
573 direction.subtract_vectors(ball.pos, obstacle.pos)
574 d = direction.length()
575 if d == 0.0 or d >= ball.radius + obstacle.radius:
576 return
577
578 direction.scale(1.0 / d)
579
580 corr = ball.radius + obstacle.radius - d
581 ball.pos.add(direction, corr)
582
583 v = ball.vel.dot(direction)
584 ball.vel.add(direction, obstacle.pushVel - v)
585
586 table.score += 1
587
588 ball_count = len(table.balls)
589 if ball_count == 1:
590 table.multiball += 1
591 if table.multiball == MULTIBALL_SCORE:
592 table.multiball = 0
593 table.add_ball()
594
595
596# ------------------------
597
598
599def handle_ball_flipper_collision(ball, flipper):
600 """handle a collision between a ball and a flipper"""
601 closest = closest_point_on_segment(ball.pos, flipper.pos, flipper.getTip())
602 direction = Vector2()
603 direction.subtract_vectors(ball.pos, closest)
604 d = direction.length()
605 if d == 0.0 or d >= ball.radius + flipper.radius:
606 return
607
608 direction.scale(1.0 / d)
609
610 corr = ball.radius + flipper.radius - d
611 ball.pos.add(direction, corr)
612
613 # update velocitiy
614 radius = closest.clone()
615 radius.add(direction, flipper.radius)
616 radius.subtract(flipper.pos)
617 surfaceVel = radius.perp()
618 surfaceVel.scale(flipper.currentAngularVelocity)
619 v = ball.vel.dot(direction)
620 vnew = surfaceVel.dot(direction)
621 ball.vel.add(direction, vnew - v)
622
623
624# ------------------------
625
626
627def handle_ball_border_collision(ball, border):
628 """handle a collision between a ball and a border"""
629 # find closest segment
630 d = Vector2()
631 closest = Vector2()
632 ab = Vector2()
633 normal = Vector2()
634 wall = 0
635 minDist = 0.0
636
637 for i, a in enumerate(border):
638 b = border[(i + 1) % len(border)]
639 c = closest_point_on_segment(ball.pos, a, b)
640 d.subtract_vectors(ball.pos, c)
641 dist = d.length()
642 if i == 0 or dist < minDist:
643 wall = i
644 minDist = dist
645 closest.set(c)
646 ab.subtract_vectors(b, a)
647 normal = ab.perp()
648
649 # push out
650 d.subtract_vectors(ball.pos, closest)
651 dist = d.length()
652 if dist == 0.0:
653 d.set(normal)
654 dist = normal.length()
655
656 d.scale(1.0 / dist)
657
658 if d.dot(normal) >= 0.0:
659 if dist > ball.radius:
660 return 0
661 ball.pos.add(d, ball.radius - dist)
662
663 else:
664 ball.pos.add(d, -(dist + ball.radius))
665
666 # update velocity
667 v = ball.vel.dot(d)
668 vnew = abs(v) * ball.restitution
669
670 ball.vel.add(d, vnew - v)
671 return wall
672
673
674# ------------ Text routines ------------
675
676
677def center_on(text, y, color=gc9a01.WHITE, fnt=font):
678 """center text on y"""
679 x = (WIDTH >> 1) - ((fnt.WIDTH * len(text)) >> 1)
680 tft.text(
681 fnt, text, x + HOFS_X, int(HEIGHT - y * SCALE_Y) + HOFS_Y, color, BACKGROUND
682 )
683
684
685def print_at(text, x, y, color=gc9a01.WHITE, fnt=font):
686 """print text at x,y"""
687 tft.text(
688 fnt,
689 text,
690 int(x * SCALE_X) + HOFS_X,
691 int(HEIGHT - y * SCALE_Y) + HOFS_Y,
692 color,
693 BACKGROUND,
694 )
695
696
697def print_right(text, y, color=gc9a01.WHITE, fnt=font):
698 """print text right aligned at y"""
699 x = WIDTH - (fnt.WIDTH * len(text))
700 tft.text(
701 fnt, text, x + HOFS_X, int(HEIGHT - y * SCALE_Y) + HOFS_Y, color, BACKGROUND
702 )
703
704
705def update_score():
706 """update the score"""
707 print_right(f"{MULTIBALL_SCORE-table.multiball:2}", 0.25)
708 print_right(f"{table.score:04}", 0.08)
709
710
711def update_ball():
712 """update the ball count"""
713 print_at(f"B {table.ball}", 0.0, 0.08)
714
715
716def ball_countdown():
717 """countdown before the ball is released"""
718 for i in range(3, 0, -1):
719 center_on(f"{i}", 1.3, gc9a01.WHITE, bold_font)
720 time.sleep(1)
721
722 center_on(" ", 1.3, gc9a01.WHITE, bold_font)
723 table.draw_border()
724 table.draw_obstacles()
725
726
727def print_game_over(color=gc9a01.RED):
728 """print game over"""
729 center_on("GAME", 1.65, color, bold_font)
730 center_on("OVER", 1.30, color, bold_font)
731 center_on(" Press ", 0.82, color, font)
732 center_on(" Button ", 0.72, color, font)
733 center_on(" To ", 0.62, color, font)
734 center_on(" Start ", 0.52, color, font)
735
736
737def game_over():
738 """game over, man, game over"""
739 print_game_over()
740 time.sleep(1)
741
742 # wait for buttons to be released
743 if left_flipper and right_flipper:
744 while left_flipper.value() == PRESSED or right_flipper.value() == PRESSED:
745 time.sleep(0.1)
746
747 # Color cycle game over message while waitiing for a button to be pressed
748 color = text_color()
749
750 if left_flipper and right_flipper:
751 while left_flipper.value() != PRESSED and right_flipper.value() != PRESSED:
752 print_game_over(next(color))
753 time.sleep(0.01)
754
755 tft.fill(BACKGROUND)
756
757
758# ------------ the big show ------------
759
760
761def start_game():
762 """start the game and loop"""
763 try:
764 while True:
765 table.reset()
766 table.ball_reset()
767
768 while table.game_over is False:
769 last = time.ticks_ms()
770
771 table.flippers[0].pressed = (
772 left_flipper and left_flipper.value() == PRESSED
773 )
774 table.flippers[1].pressed = (
775 right_flipper and right_flipper.value() == PRESSED
776 )
777
778 table.simulate()
779
780 table.draw_border()
781 table.draw_obstacles()
782
783 table.draw_flippers()
784 table.draw_balls()
785 update_score()
786
787 # print("FPS: ", 1000 / (time.ticks_ms() - last))
788 if time.ticks_ms() - last < table.ticks:
789 time.sleep_ms(table.ticks - (time.ticks_ms() - last))
790
791 game_over()
792
793 finally:
794 if hasattr(tft, "deinit") and callable(tft.deinit):
795 tft.deinit()
796
797
798# ------------ set up the buttons ------------
799
800OFS_WIDTH = 92
801OFS_HEIGHT = 48
802
803buttons = tft_buttons.Buttons()
804
805left_flipper = buttons.left
806right_flipper = buttons.right
807
808# ------------ set up the display ------------
809
810tft = tft_config.config()
811tft.init()
812tft.fill(BACKGROUND)
813
814HEIGHT = tft.height() - OFS_HEIGHT
815WIDTH = tft.width() - OFS_WIDTH
816MAX_HEIGHT = 1.88
817HOFS_X = OFS_WIDTH >> 1
818HOFS_Y = OFS_HEIGHT >> 1
819SCALE_X = WIDTH
820SCALE_Y = HEIGHT / MAX_HEIGHT
821SCALE_RADIUS = min(SCALE_X, SCALE_Y)
822
823# ------- set up the table and start game ---------
824
825table = Table()
826start_game()