-
Notifications
You must be signed in to change notification settings - Fork 0
/
acid_rain.py
257 lines (246 loc) · 10.7 KB
/
acid_rain.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
from math import sqrt, exp
from microqiskit import QuantumCircuit, simulate
import pew
pew.init()
screen = pew.Pix()
qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)
def new_raindrop():
# _Quantumly_ returns a random number between 0 and 7 (inclusive of both).
# The PewPew's screen is an 8x8 grid so the number returned will be the
# raindrop's x coordinate.
return int(''.join(simulate(qc, shots=3, get='memory')), 2)
def flip_a_quantum_coin():
# _Quantumly_ returns a random number between 0 and 1 (inclusive of both).
return int(*simulate(qc, shots=1, get='memory'))
__REDUCED_PLANCK_CONSTANT = 1.054571817e-34
def sinh(x):
# Hyperbolic sine function.
e_x = exp(x)
return (e_x*e_x - 1) / (2 * e_x)
__TITLE_SCREEN = pew.Pix.from_text('AcidRain')
__GAME_OVER_SCREEN = pew.Pix.from_text('Game Over!')
__VICTORY_SCREEN = pew.Pix.from_text("Congratulations! You've won! ^-^")
class Victory(Exception):
pass
# Game settings
class GameSetings:
def __init__(self,
starting_speed=2, player_starting_x=3,
rain_colour=1, player_colour=255, erasing_colour=0,
game_speed_factor=1.012, energy_barrier=20,
wrap_around=True, simulating_real_physics=True,
mass=1e-18, barrier_width=1e-9, epsilon=0.0001):
self.starting_speed = starting_speed
self.player_starting_x = player_starting_x # Player is always on the ground so we only need the x component
self.rain_colour = rain_colour
self.player_colour = player_colour
self.erasing_colour = erasing_colour
self.game_speed_factor = game_speed_factor
self.energy_barrier = energy_barrier
self.wrap_around = wrap_around
self.simulating_real_physics = simulating_real_physics
self.mass = mass
self.barrier_width = barrier_width
self.epsilon = epsilon
# Game logic variables
player = None
raindrops_evaded = 0
raindrops = []
game_speed = None
# Game functions
def debounce(game_speed):
# When the player (irl) presses any button on the PewPew v10.2, it appropriately
# modifies the value returned by pew.keys() to reflect this.
#
# However the design of the circuitry at play (two metal plates coming into contact)
# when subject to the speed of and manner in which the player depresses and releases
# the button often registers more than one press, i.e. more than one state transition.
# The player operates at a speed much slower than the microcontroller's circuitry for
# one, and may also accidentally double-press the button.
#
# Visually speaking, if we were to graph the electrical signal generated by the button,
# there would be multiple transitions, or 'bounces', bunched up together within a
# short amount of time.
#
# To remedy this, we implement some sort of 'debouncing' using either hardware or
# software.
#
# One way to do so using software is to ignore all subsequent presses within a certain
# amount of time. We'd need to choose this window wisely, since all player input is
# ignored during that time.
#
# In our case, we could have that window decrease as the game goes faster so that the
# player feels less punished at high speeds.
if game_speed < 2.2:
t = 40 # total delay = 100 / 40 = 2.5 ms
elif game_speed < 2.5:
t = 50 # total delay = 100 / 50 = 2.0 ms
else:
t = 75 # total delay = 100 / 75 = 1.3 ms
for _ in range(100):
pew.tick(1/t)
if not pew.keys(): # if there are no key presses, we're done debouncing
return
def reset_game_logic(player_starting_x, starting_speed):
# Resets game logic variables to their starting values.
global player, raindrops_evaded, raindrops, game_speed
player = player_starting_x
raindrops_evaded = 0
del raindrops[:] # deletes contents of raindrops; doesn't replace raindrops with new empty list
game_speed = starting_speed
def clear_screen():
# Clears the screen by placing a black box over it.
screen.box(0, x=0, y=0, width=8, height=8)
def title_screen():
# Displays the title screen in a loop. Breaks if either X or O buttons are pressed.
activity = False
while True:
for dx in range(-8, __TITLE_SCREEN.width):
screen.blit(__TITLE_SCREEN, -dx, 1)
pew.show(screen)
pew.tick(1/12)
activity = pew.keys() & (pew.K_O | pew.K_X)
if activity:
break
if activity:
break
clear_screen()
def handle_quantum_tunnelling(simulating_real_physics, mass, energy_barrier,
barrier_width, starting_speed, epsilon,
erasing_colour):
# Representing the mass of the player by MASS, and noticing that when E >= ENERGY_BARRIER
# we end the game, we compute the wave number K_1:
# K_1 = sqrt((2*MASS*(ENERGY_BARRIER - E)) / __REDUCED_PLANCK_CONSTANT)
#
# Since __REDUCED_PLANCK_CONSTANT is pretty small, sqrt(1/__REDUCED_PLANCK_CONSTANT) will
# be large, so to bring K_1 into a relatively 'normal' range, we'll choose MASS and BARRIER_WIDTH
# (the width of the raindrop) to be relatively smaller.
#
# The probability of transmission through the raindrop when E < ENERGY_BARRIER is given by:
# 1 / (1 + (ENERGY_BARRIER**2 * sinh(K_1 * BARRIER_WIDTH)**2) / (4 * E * (ENERGY_BARRIER - E)))
# We'll accept the probability as being convincing if it is larger than 0.5.
#
# We note that this value is undefined for E == 0, which occurs at the very start when:
# E = game_speed - STARTING_SPEED
# To prevent raising an error, we redefine E to be:
# E = game_speed - STARTING_SPEED + EPSILON,
# for some small positive EPSILON.
global raindrops_evaded, raindrops
if any(r[0] == player and r[1] == 6 for r in raindrops):
# check if there is a raindrop directly above the raindrop directly
# above the player, because quantum tunnelling is only for one raindrop
if any(r[0] == player and r[1] == 5 for r in raindrops):
raise pew.GameOver
if simulating_real_physics:
E = game_speed - starting_speed + epsilon
k_1 = sqrt(2 * mass * (energy_barrier - E) / __REDUCED_PLANCK_CONSTANT)
t = 1 / (1 + energy_barrier*energy_barrier * sinh(k_1 * barrier_width)**2 / (4 * E * (energy_barrier - E)))
if t < 0.5:
screen.pixel(player, 6, color=erasing_colour)
raindrops.remove([player, 6])
raindrops_evaded += 1
else:
print('t=', t)
raise pew.GameOver
else:
if flip_a_quantum_coin() < 1:
screen.pixel(player, 6, color=erasing_colour)
raindrops.remove([player, 6])
raindrops_evaded += 1
else:
raise pew.GameOver
def handle_player_input(wrap_around, erasing_colour, player_colour,
simulating_real_physics, mass, energy_barrier,
barrier_width, starting_speed, epsilon):
# Checks for player input and interprets it as best as it can.
global player
keys = pew.keys()
if keys & pew.K_UP:
handle_quantum_tunnelling(
simulating_real_physics, mass, energy_barrier, barrier_width,
starting_speed, epsilon, erasing_colour
)
else:
if keys & pew.K_LEFT:
dx = -1
elif keys & pew.K_RIGHT:
dx = 1
else:
return
if player + dx > 7:
if not wrap_around:
return
dx = -7
if player + dx < 0:
if not wrap_around:
return
dx = 7
screen.pixel(player, 7, color=erasing_colour)
player += dx
screen.pixel(player, 7, color=player_colour)
def __run(gs):
# Function that handles one run of the game.
global player, raindrops, raindrops_evaded, game_speed
title_screen()
screen.pixel(player, 7, color=gs.player_colour)
try:
while True:
handle_player_input(
gs.wrap_around, gs.erasing_colour, gs.player_colour,
gs.simulating_real_physics, gs.mass, gs.energy_barrier,
gs.barrier_width, gs.starting_speed, gs.epsilon
)
for i in range(len(raindrops)-1, -1, -1):
# remove fallen raindrops
if raindrops[i][1] == 7:
screen.pixel(*raindrops[i], color=gs.erasing_colour)
del raindrops[i]
raindrops_evaded += 1
# fairly certain redrawing the player here fixed a visual bug
screen.pixel(player, 7, color=gs.player_colour)
# update raindrops
else:
screen.pixel(*raindrops[i], color=gs.erasing_colour)
raindrops[i][1] += 1
screen.pixel(*raindrops[i], color=gs.rain_colour)
# check for player collision
if raindrops[i][0] == player:
if raindrops[i][1] == 7:
raise pew.GameOver
# generate new raindrop
raindrops.append([new_raindrop(), -1])
if game_speed >= gs.energy_barrier:
raise Victory
pew.show(screen)
pew.tick(1/game_speed)
game_speed *= gs.game_speed_factor
except (pew.GameOver, Victory) as e:
clear_screen()
score = pew.Pix.from_text('Score: ' + str(raindrops_evaded))
p = __GAME_OVER_SCREEN if isinstance(e, pew.GameOver) else __VICTORY_SCREEN
for dx in range(-8, p.width):
screen.blit(p, -dx, 1)
pew.show(screen)
pew.tick(1/17)
for dx in range(-8, score.width):
screen.blit(score, -dx, 1)
pew.show(screen)
pew.tick(1/13)
debounce(game_speed) # helpful
reset_game_logic(gs.player_starting_x, gs.starting_speed)
def play(gs, forever=False):
# Function that calls the game on loop. Use this instead of __run.
assert isinstance(gs, GameSetings), 'need GameSettings object, not {}'.format(type(gs))
global player, game_speed
if player is None:
player = gs.player_starting_x
if game_speed is None:
game_speed = gs.starting_speed
try:
__run(gs)
while forever:
__run(gs)
except KeyboardInterrupt:
return