Skip to content

Commit b8a55e3

Browse files
authored
Merge pull request #296 from isaacbernat/master
Adding 2pddl42ppl game to arcade
2 parents 671c36b + 2da56b5 commit b8a55e3

File tree

3 files changed

+241
-0
lines changed

3 files changed

+241
-0
lines changed

2pddl42ppl/2pddl42ppl.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import time
2+
import math
3+
import random
4+
import thumby as tb
5+
from thumby import display as dp
6+
7+
8+
class Stats:
9+
def __init__(self):
10+
self.wall_bounces = 0
11+
self.paddle_bounces = 0
12+
self.start_time = time.ticks_ms()
13+
self.winner = None
14+
15+
16+
class Paddle:
17+
WIDTH, HEIGHT, SPEED = 1, dp.height//5, 1
18+
19+
def __init__(self, x, y):
20+
self.x = x
21+
self.y = y
22+
self.length = self.HEIGHT
23+
self.width = self.WIDTH
24+
self.direction = 0 # -1 up, 0 still, 1 down
25+
26+
def update(self):
27+
self.y += self.direction * self.SPEED
28+
self.y = max(self.y, 0)
29+
self.y = min(self.y, dp.height - self.length)
30+
dp.drawFilledRectangle(self.x, int(self.y), self.width, self.length, 1)
31+
32+
33+
class Ball:
34+
SIZE, SPEED, AMOUNT, SOUND, BOUNCE_DYNAMIC_ANGLE, SIZE_REDUCTION_RATE = dp.height//5, 1, 1, 100, 1, 0.33
35+
36+
def __init__(self, menu_selection):
37+
self.menu_selection = menu_selection
38+
angle = random.uniform(math.pi/8, math.pi/4) * random.choice([-1, 1]) # to debug corner bounce in Solo mode: angle=math.pi/11.5, self.x=int(self.SIZE) + 1
39+
self.dx = self.SPEED * math.cos(angle)
40+
self.dy = self.SPEED * math.sin(angle)
41+
self.y = dp.height / 2.0
42+
self.x = int(self.SIZE) + 1 if menu_selection == 1 else int(random.uniform(int(self.SIZE) + 1, dp.width -self.SIZE - 1))
43+
44+
def update(self, bounce=0):
45+
def handle_bounce(bounce_type, axis, paddle):
46+
if axis == 'x':
47+
if self.BOUNCE_DYNAMIC_ANGLE and paddle:
48+
mid_ball, mid_pad = (self.y + self.SIZE/2), (paddle.y + paddle.length/2)
49+
relative_position = (mid_pad - mid_ball) / (paddle.length / 2 + self.SIZE / 2)
50+
new_angle = max(-1, min(1, relative_position)) * (math.pi / 2.25)
51+
self.dx = self.SPEED * (-1 if self.dx > 0 else 1)
52+
self.dy = self.SPEED * abs(math.sin(new_angle)) * (-1 if new_angle > 0 else 1)
53+
else:
54+
self.dx = -self.dx
55+
elif axis == 'y':
56+
self.dy = -self.dy
57+
58+
if bounce_type == 1: # paddle
59+
self.SIZE = max(1, self.SIZE - self.SIZE_REDUCTION_RATE)
60+
stats.paddle_bounces += 1
61+
elif bounce_type == 2: # wall
62+
stats.wall_bounces += 1
63+
else: # invalid bounce
64+
return 0
65+
tb.audio.play(freq=7458 if bounce_type == 1 else 7902, duration=self.SOUND)
66+
return bounce_type
67+
68+
self.x += self.dx
69+
self.y += self.dy
70+
if self.dx < 0 and self.x <= Paddle.WIDTH and self.y <= paddle1.y + paddle1.length and paddle1.y <= self.y + self.SIZE:
71+
bounce = handle_bounce(1, 'x', paddle1)
72+
elif self.dx < 0 and self.menu_selection == 0 and (Paddle.WIDTH <= self.x <= Paddle.WIDTH * 2) and self.y <= paddle2.y + paddle2.length and paddle2.y <= self.y + self.SIZE:
73+
bounce = handle_bounce(1, 'x', paddle2)
74+
elif self.dx > 0 and self.menu_selection == 1 and (self.x + self.SIZE >= dp.width - Paddle.WIDTH) and self.y <= paddle2.y + paddle2.length and paddle2.y <= self.y + self.SIZE:
75+
bounce = handle_bounce(1, 'x', paddle2)
76+
elif self.dx > 0 and wall.length > 0 and self.x >= dp.width - 1 - self.SIZE:
77+
bounce = handle_bounce(2, 'x', None)
78+
if (self.dy < 0 and self.y <= 1) or (self.dy > 0 and self.y >= dp.height - self.SIZE):
79+
bounce = handle_bounce(2, 'y', None)
80+
if bounce == 0 and (self.x + self.SIZE <= 0 or self.x >= dp.width):
81+
stats.winner = "Game over" if self.x <= 0 else "Player1 wins"
82+
self.dx = 0
83+
dp.drawFilledRectangle(int(self.x), int(self.y), int(self.SIZE), int(self.SIZE), 1)
84+
85+
86+
def draw_text_screen(lines, highlight_line=-1):
87+
dp.fill(0)
88+
for i, line in enumerate(lines):
89+
if i == highlight_line:
90+
dp.drawFilledRectangle(0, i * 8, len(line) * 6, 8, 1)
91+
dp.drawText(line, 0, i * 8, 1 if i != highlight_line else 0)
92+
dp.update()
93+
94+
95+
def handle_ingame_input(paddle1, paddle2):
96+
if tb.buttonU.pressed(): # paddle1
97+
paddle1.direction = -1
98+
elif tb.buttonD.pressed():
99+
paddle1.direction = 1
100+
else:
101+
paddle1.direction = 0
102+
if tb.buttonA.pressed(): # paddle2
103+
paddle2.direction = -1
104+
elif tb.buttonB.pressed():
105+
paddle2.direction = 1
106+
else:
107+
paddle2.direction = 0
108+
109+
110+
def update_and_draw(objects):
111+
dp.fill(0)
112+
dp.drawLine(0, 0, dp.width - 1, 0, 1) # horizontal walls
113+
dp.drawLine(0, dp.height -1, dp.width - 1, dp.height -1, 1)
114+
for o in objects:
115+
o.update()
116+
dp.update()
117+
118+
119+
def show_winner(stats, menu_selection):
120+
draw_text_screen([
121+
f"{stats.winner}!",
122+
f"{int((time.ticks_ms() - stats.start_time)/1000)} seconds",
123+
f"{stats.wall_bounces} wall bounces",
124+
f"{stats.paddle_bounces} paddle bounces",
125+
"Press Right ->"])
126+
while True:
127+
if (tb.buttonR.pressed()):
128+
return menu_selection
129+
elif (tb.buttonL.pressed()):
130+
return -1
131+
132+
133+
def show_menu(selected=0, options=["Coop", "Versus", "Solo", "Settings"]):
134+
while True:
135+
draw_text_screen(["2pddl 4 2ppl"] + options, highlight_line=selected + 1)
136+
if tb.buttonU.justPressed():
137+
selected = (selected - 1) % len(options)
138+
elif tb.buttonD.justPressed():
139+
selected = (selected + 1) % len(options)
140+
elif tb.buttonA.justPressed() or tb.buttonR.justPressed():
141+
return selected
142+
143+
144+
def restart_game(menu_selection):
145+
paddle1 = Paddle(0, dp.height // 2 - Paddle.HEIGHT // 2)
146+
paddle2 = Paddle(dp.width - Paddle.WIDTH, dp.height // 2 - Paddle.HEIGHT // 2)
147+
wall = Paddle(dp.width - Paddle.WIDTH, 0)
148+
if menu_selection == 0: # Coop
149+
paddle2.x = paddle1.x + 1
150+
paddle1.y = 0
151+
paddle2.y = dp.height - paddle2.length
152+
wall.length = dp.height
153+
elif menu_selection == 1: # Versus
154+
wall.length = 0
155+
elif menu_selection == 2: # Solo
156+
paddle2.length = 0
157+
wall.length = dp.height
158+
balls = [Ball(menu_selection) for _ in range(Ball.AMOUNT + (menu_selection == 0) * 2)]
159+
return paddle1, paddle2, balls, wall, Stats()
160+
161+
162+
def show_settings(selected=0, start_index=0):
163+
settings = [ # name, value, min_val, max_val, step
164+
["Paddle Height", Paddle.HEIGHT, int(2), 40, 2],
165+
["Paddle Speed", Paddle.SPEED, 0.2, 2.0, 0.2],
166+
["Ball Speed", Ball.SPEED, 0.2, 5.0, 0.2],
167+
["Ball Size", Ball.SIZE, 1, 50, 1],
168+
["Sound Duration", Ball.SOUND, int(0), 500, 50],
169+
["Ball Amount", Ball.AMOUNT, 1, 20, 1],
170+
["Bounce Angle", Ball.BOUNCE_DYNAMIC_ANGLE, 0, 1, 1],
171+
["Ball reduction rate", Ball.SIZE_REDUCTION_RATE, 0, 2, 0.1],
172+
]
173+
while True:
174+
lines = ["Settings"]
175+
for i in range(3): # screen can show max 5 lines of text
176+
index = start_index + i
177+
if index < len(settings):
178+
name, value, _, _, _ = settings[index]
179+
lines.append(f"{name}: {value:.1f}")
180+
draw_text_screen(lines + ["Back = Left"], highlight_line=selected - start_index + 1 if selected < len(settings) else 4)
181+
182+
if tb.buttonU.justPressed():
183+
if selected > 0:
184+
selected -= 1
185+
if selected < start_index:
186+
start_index = selected
187+
else:
188+
selected = len(settings)
189+
start_index = max(0, len(settings) - 3)
190+
elif tb.buttonD.justPressed():
191+
if selected < len(settings):
192+
selected += 1
193+
if selected >= start_index + 3:
194+
start_index = selected - 2
195+
else:
196+
selected = 0
197+
start_index = 0
198+
elif selected != len(settings) and (tb.buttonA.justPressed() or tb.buttonR.justPressed()):
199+
adjust_setting(settings[selected])
200+
elif tb.buttonL.justPressed() or tb.buttonA.justPressed() or tb.buttonR.justPressed():
201+
return settings
202+
203+
204+
def adjust_setting(setting):
205+
name, value, min_val, max_val, step = setting
206+
while True:
207+
draw_text_screen([name, f"Value: {value:.1f}", "Left = Back"])
208+
if tb.buttonD.justPressed():
209+
value = max(min_val, value - step)
210+
elif tb.buttonU.justPressed():
211+
value = min(max_val, value + step)
212+
elif tb.buttonA.justPressed() or tb.buttonR.justPressed() or tb.buttonL.justPressed():
213+
setting[1] = value
214+
return
215+
216+
217+
dp.setFPS(60)
218+
menu_selection = -1
219+
220+
while True:
221+
if menu_selection == -1:
222+
menu_selection = show_menu()
223+
paddle1, paddle2, balls, wall, stats = restart_game(menu_selection)
224+
stats.winner = "Good luck! "
225+
elif stats.winner:
226+
if menu_selection == 3: # Settings
227+
new_settings = show_settings()
228+
for i, attr in enumerate(['HEIGHT', 'SPEED', 'SPEED', 'SIZE', 'SOUND', 'AMOUNT', 'BOUNCE_DYNAMIC_ANGLE', 'SIZE_REDUCTION_RATE']):
229+
setattr(Paddle if i < 2 else Ball, attr, new_settings[i][1])
230+
menu_selection = -1
231+
menu_selection = show_winner(stats, menu_selection)
232+
paddle1, paddle2, balls, wall, stats = restart_game(menu_selection)
233+
else:
234+
handle_ingame_input(paddle1, paddle2)
235+
update_and_draw([paddle1, paddle2, wall] + balls)
1.59 MB
Binary file not shown.

2pddl42ppl/arcade_description.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2-players on a single Thumby! Use familiar Pong mechanics against each other in Versus mode. Choose Coop mode to achieve your best time and bouncing stats or even solo mode for a greater individual challenge!
2+
3+
Physics, paddle, balls... up to 8 parameters adjustable using ingame settings menu to make each experience unique. All this under 0x100 (255) lines of code. Check it out here or in github.com/isaacbernat/2pddl42ppl and get inspired to make your own games ;D
4+
5+
Author: isaacbernat
6+
Version: 1.0

0 commit comments

Comments
 (0)