Refactor for clarity
Some things you're doing are complicated, and get repeated a lot! Let's handle "wait for a letter" and "move" separately, so our event loop looks like:
wait_for("a")
wait_for("d")
move()
This should be pretty simple:
def wait_for(ch):
"""Wait forever until the keyboard hears `ch`"""
while True:
if msvcrt.getch().decode('ASCII') == ch:
return
def move():
global distance # SEE NOTE BELOW
distance += 1
if distance % 10 == 0:
print("*", end="")
if distance == 50:
print(" You're halfway there!")
else:
print() # just a newline
Note that I'm using a global
now! "But Adam," I hear you cry, "I've heard that globals are evil, second only to Beelzebub and GOTO in being considered harmful!"
This is true! But we also want to be able to modify this variable with a function. Hmmm, maybe this could benefit from encapsulation! Classes to the rescue!
Encapsulate
Wrapping your game in a class
will let you give it attributes that its methods can modify! This will let us do things like factor out mutating functions into methods of the class Game.
class Game(object):
def __init__(self):
distance = 0
self.highscore = tuple() # start with no high score
# high scores will be a tuple of form
# (name, time)
@staticmethod
def wait_for(ch):
while True:
if msvcrt.getch().decode("ASCII") == ch:
return
def move(self):
self.distance += 1
if self.distance % 10 == 0:
print("*", end="")
if distance == 50:
print(" You're halfway there!")
else:
print()
Now your move
method modifies its instance's attribute named distance
, instead of some unknowable global variable. This is much better. Let's go further:
# inside class Game:
@staticmethod
def print_header():
print("\n--------------------------------------------------------------")
print('\n\nWelcome to the 100m sprint, tap a and d rapidly to move!')
print('* = 10m')
if self.highscore:
# use implicit string concatenation here to break at a logical point
print("\n**Current record: " + str(self.highscore[1]) + "s "
"by: " + self.highscore[0])
print('\nPress enter to start')
input()
def run(self):
"""Handle event loop"""
start = datetime.datetime.now() # SEE NOTE BELOW
while self.distance < 100:
self.wait_for("a")
self.wait_for("d")
self.move()
end = datetime.datetime.now()
d_time = (end - start).total_seconds()
if not self.highscore or d_time < self.highscore[1]:
# if the high score isn't set, or if this score beats it:
name = input("...")
self.highscore = (name, d_time) # round as desired
def start(self):
"""Starts one run of the game"""
self.distance = 0 # reset to zero
self.print_header()
self.run()
There's a lot to go through here, but most of it makes sense if you think of things as responsible for ONE thing.
wait_for
=> Halts execution until its character gets detected.
move
=> Handles advancing self.distance
and displaying feedback to user.
print_header
=> Prints the greeting screen. Really could be rolled into start
.
run
=> Handles all the "during game" logic. Commonly called the event loop.
start
=> Begins ONE run of the game (prints headers and calls event loop)
I did want to point out that I changed the time.time()
call to datetime.datetime.now()
. time.time
will be very confused if you start playing a game at 23:59:57 and it takes you more than 3 seconds to finish. datetime.timedelta
can handle that elegantly. The difference between two datetime
objects is a timedelta
, which has the method total_seconds
. We use that here to get the race time.
Let's run this puppy
Now that we're using a class, we need to instantiate it and make some calls so it will work. Luckily this is easy and idiomatic with an if __name__ == "__main__"
clause.
# at the bottom of the file, under your class definition
if __name__ == "__main__":
game = Game() # instantiate the class
while True:
game.start() # run the game once
if input("Play again? (y/n) ").lower() == "y":
continue # play again, including printing headers with new high score!
else:
break # quit
Since game
is now an instance of the class Game
, its state persists between game starts. In this case the only mutable state should be the high score.
As pointed out by jpmc26, having distance
be an instance variable is silly since it's only involved in the actual run of a game and has no meaning between runs. That actually simplifies the code a bit, because it lets us move wait_for
and move
inside run
as nested functions.
class Game(object):
def __init__(self):
self.highscore = tuple()
def run(self):
def wait_for(ch):
# copy the whole function from above
def move():
distance += 1
if distance % 10 == 0:
# etc of this function as above, just dropping the "self"s
distance = 0
start = datetime.datetime.now()
while distance < 100:
wait_for("a")
wait_for("d")
move()
# ... etc as above
In fact, in reading this again, it's probably easier to drop the distance
variable completely and simply run the wait loop 100 times.
class Game(object):
DISTANCE = 100
def run(self):
def wait_for(ch):
# as above
# no more move()
start = datetime.datetime.now()
for _ in range(self.DISTANCE):
wait_for("a")
wait_for("d")
# do your print stuff in here
# etc as above
distance = int(0)
? It's no different todistance = 0
. \$\endgroup\$