I am making a river crossing puzzle app using Pygame. Did I implement the conditions optimally?
The Person
class arguments for the conditions as to when or not a person will cross a river are these:
hate=[]
: If any of the names in the list are the names of any people inboat.aboard
, don't cross.like=[]
: If none of the names in the list are the names of any people inboat.aboard
, don't cross.alone=True
: If there is anyone inboat.aboard
besides himself in the boat, don't cross.alone=False
: If there is no one inboat.aboard
besides himself in the boat, don't cross.
Here is where I implemented the conditions:
def go(self):
hated = [name for person in self.aboard for name in person.hate]
liked = [name for person in self.aboard for name in person.like]
no = False
if any(person.alone == True for person in self.aboard) and len(self.aboard) > 1:
no = True
elif any(person.alone == False for person in self.aboard) and len(self.aboard) == 1:
no = True
elif any(person.name == name for person in self.aboard for name in hated):
no = True
elif liked:
if any(person.name == name for person in self.aboard for name in liked):
no = False
else:
no = True
else:
no = False
if not no:
if self.side == 'UP':
self.y += 300
self.side = 'DOWN'
else:
self.y -= 300
self.side = 'UP'
self.obj.y = self.y
I want to know if I implemented the conditions above optimally, or even correctly.
Here is when I run the entire code, and the conditions seem fine:
And here is my entire code:
import pygame
import random
pygame.init()
wn = pygame.display.set_mode((600, 600))
font = pygame.font.SysFont('Arial', 25)
people = []
class Person():
def __init__(self, name, x, y, color, side='UP', w=20, h=20, hate=[], like=[], alone=None):
self.name = name
self.color = color
self.x = x
self.y = y
self.w = w
self.h = h
self.alone = alone
self.hate = hate
self.like = like
self.side = side
self.obj = pygame.Rect(x, y, w, h)
people.append(self)
conditions = []
if alone == False:
conditions.append("won't go alone")
elif alone == True:
conditions.append("will only go alone")
if len(hate) > 1:
conditions.append(f"won't go with any of: {', '.join(hate)}")
elif len(hate) == 1:
conditions.append(f"won't go with {hate[0]}")
if len(like) > 1:
conditions.append(f"will only go if any of: {', '.join(like)} are going")
elif len(like) == 1:
conditions.append(f"will only go if {like[0]} is going")
self.conditions = conditions
def draw(self):
self.obj = pygame.Rect(self.x, self.y, self.w, self.h)
pygame.draw.rect(wn, self.color, self.obj)
def get_on_boat(self, boat):
self.x = random.randint(boat.x, boat.x+boat.w-self.w)
self.y = random.randint(boat.y, boat.y+boat.h-self.h)
self.side = boat.side
def write_conditions(self, x, y):
text = font.render(f"{self.name} {'; '.join(self.conditions)}", True, (0, 0, 0))
wn.blit(text, (x, y))
def get_off_boat(self, boat):
self.x += random.randint(50, 200)
def on_boat(self, boat):
return boat.x+boat.w-self.w >= self.x >= boat.x and boat.y+boat.h-self.h >= self.y >= boat.y
class Boat():
def __init__(self, room, x=280, y=200, color=(140, 140, 140), w=40, h=60, side='UP'):
self.room = room
self.x = x
self.y = y
self.w = w
self.h = h
self.trips = 0
self.color = color
self.obj = pygame.Rect(x, y, w, h)
self.side = side
self.aboard = []
def go(self):
hated = [name for person in boat.aboard for name in person.hate]
liked = [name for person in boat.aboard for name in person.like]
no = False
if any(person.alone == True for person in self.aboard) and len(self.aboard) > 1:
no = True
elif any(person.alone == False for person in self.aboard) and len(self.aboard) == 1:
no = True
elif any(person.name == name for person in self.aboard for name in hated):
no = True
elif liked:
if any(person.name == name for person in self.aboard for name in liked):
no = False
else:
no = True
else:
no = False
if not no:
if self.side == 'UP':
self.y += 300
self.side = 'DOWN'
else:
self.y -= 300
self.side = 'UP'
self.obj.y = self.y
def draw(self):
pygame.draw.rect(wn, self.color, self.obj)
boat = Boat(2)
Yel = Person('Yel', boat.x+100, boat.y, (255, 255, 0), hate=['Red']) # yellow
Red = Person('Red', boat.x-100, boat.y, (255, 0, 0), alone=True) # red
Pin = Person('Pin', boat.x+200, boat.y, (255, 0, 255), like=['Yel']) # pink
Blu = Person('Blu', boat.x+250, boat.y, (0, 0, 255), like=['Yel'], hate=['Gre']) # blue
Gre = Person('Gre', boat.x+280, boat.y, (0, 255, 0)) # green
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
elif event.type == pygame.MOUSEBUTTONDOWN:
for person in people:
if person.obj.collidepoint(pygame.mouse.get_pos()):
if person in boat.aboard:
person.get_off_boat(boat)
boat.aboard.remove(person)
boat.room += 1
else:
if boat.room and person.side == boat.side:
person.get_on_boat(boat)
boat.aboard.append(person)
boat.room -= 1
if not any(person.obj.collidepoint(pygame.mouse.get_pos()) for person in people):
if boat.obj.collidepoint(pygame.mouse.get_pos()):
if boat.aboard and boat.room >= 0:
boat.go()
for person in boat.aboard:
person.get_on_boat(boat)
boat.trips += 1
wn.fill((255, 255, 255))
pygame.draw.rect(wn, (100, 200, 255), (0, 290, 600, 180))
boat.draw()
for i, person in enumerate(people):
person.draw()
person.write_conditions(0, i*25)
pygame.display.update()
Also, I want some suggestions to clean up my messy code.
2 Answers 2
negative identifier
This is clunky:
if not no:
Perhaps rename to rejected
.
More generally, including negation in a variable name
is often a mistake, in the sense that it makes it more
difficult for a maintenance engineer to reason about your code
and about the
contract
it offers.
So when your English vocabulary offers a synonymous word
lacking negation ("rejected"), go with that.
Otherwise consider inverting the logic and adopting a
word like accepted
or compatible
.
main guard
pygame.init()
Please rephrase this import-time side effect so it is
if __name__ == "__main__":
pygame.init()
Why?
This makes it easier for an automated
unit test
module to instantiate a Person
or a Boat
and then exercise it.
In the same way, please bury the while True:
loop
within def main():
or similar function.
UX
The GUI looks good. I think it would be better if all the colors were spelled out: "Pink" instead of "Pin", "Green" instead of "Gre",etc.
The lone "Gre" on the last line is confusing.
It would be nice to show some simple instructions in the GUI because it is not clear what the user is expected to do. For example:
Click on colored square to add a person to the gray boat.
Click on the boat to cross the river.
It would be nice to explicitly state the goal of the game. For example, if the goal of the game is to have all people cross the river, show that in the GUI.
Documentation
The PEP 8 style guide recommends
adding docstrings for classes and functions. For example, summarize
what a Person
and a Boat
are and what they can do.
Simpler
In the __init__
function, this code:
if alone == False:
conditions.append("won't go alone")
elif alone == True:
conditions.append("will only go alone")
Is simpler as:
if alone:
conditions.append("will only go alone")
else:
conditions.append("won't go alone")
This assumes that the alone
variable is a boolean.
However, you do initialize it to None
:
def __init__(self, name, x, y, color, side='UP', w=20, h=20, hate=[], like=[], alone=None):
I suggest initializing it to either True
or False
(whichever makes more sense
in your code).
This would also simplify this line:
if any(person.alone == True for person in self.aboard) and len(self.aboard) > 1:
as:
if any(person.alone for person in self.aboard) and len(self.aboard) > 1:
Since the trips
variable isn't really used, it can be deleted.
Naming
It is common to pluralize the names of array variables.
hate
would be hates
.
like
would be likes
.
As I mentioned before, I think the color names would be better spelled out.
For example, the Yel
variable name would be Yellow
. Then you can delete the obvious
comment # yellow
.
In the go
function, the variable named no
is not very descriptive.
It could benefit from a more meaningful name.
DRY
In the go
function, this expression is repeated:
len(self.aboard)
You could set it to a variable to avoid repeatedly executing len
:
number_aboard = len(self.aboard)
Enum
The side
variable could be an Enum
since it is limited to a
specific set of values.
Person
class, I would. move thoseif/elif
statements out into either a separate method (maybe an@staticmethod
), or to a standalone function (if you don't like static methods). This might also be a good case for usingdataclasses
. \$\endgroup\$