4
\$\begingroup\$

Kind of a noob at Python here, and this is one of my first "big"(big for a beginner like me) project that I undertook with Tkinter and Pynput. Basically, this code will simulate an enemy's movement pattern based on some conditions that I made(you will be able to see the different "phases" being printed out on the console). You can then control the player using the arrow keys.

I would like some advice on what I should improve on for future projects. Should I add more comments? Is the code structured well? etc.

import math
import tkinter as tk
from pynput import keyboard
class Application:
 def __init__(self, master, height = 800, width = 800, updatesPerSecond = 10, safeCircle = True):
 self.height = height
 self.width = width
 self.root = master
 self.updatesPerSecond = updatesPerSecond
 self.player = Player()
 self.enemy = Enemy()
 self.safeCircle = safeCircle
 self.canvas = tk.Canvas(self.root, height = self.height, width = self.width)
 self.canvas.pack()
 self.player_rectangle = self.canvas.create_rectangle(self.player.x-self.player.hLength, self.player.y-self.player.hLength, self.player.x+self.player.hLength, self.player.y+self.player.hLength)
 self.enemy_rectangle = self.canvas.create_rectangle(self.enemy.x-self.player.hLength, self.enemy.y-self.player.hLength, self.enemy.x+self.player.hLength, self.enemy.y+self.player.hLength)
 if self.safeCircle:
 self.safe_circle = self.canvas.create_oval(self.player.x-self.enemy.safe_distance, self.player.y-self.enemy.safe_distance, self.player.x+self.enemy.safe_distance, self.player.y+self.enemy.safe_distance)
 self.keypress_list = []
 self.listener = keyboard.Listener(on_press = self.on_press, on_release = self.on_release)
 self.listener.start()
 self.player_movement()
 self.enemy_movement()
 def player_movement(self):
 if "down" in self.keypress_list:
 self.player.update_y(self.player.speed)
 if "up" in self.keypress_list:
 self.player.update_y(-self.player.speed)
 if "left" in self.keypress_list:
 self.player.update_x(-self.player.speed)
 if "right" in self.keypress_list:
 self.player.update_x(self.player.speed)
 self.player.boundary_check(self.height, self.width)
 self.canvas.coords(self.player_rectangle, self.player.x-self.player.hLength, self.player.y-self.player.hLength, self.player.x+self.player.hLength, self.player.y+self.player.hLength)
 if self.safeCircle:
 self.canvas.coords(self.safe_circle, self.player.x-self.enemy.safe_distance, self.player.y-self.enemy.safe_distance, self.player.x+self.enemy.safe_distance, self.player.y+self.enemy.safe_distance)
 self.root.after(1000//self.updatesPerSecond, self.player_movement)
 def enemy_movement(self):
 self.enemy.update_pos(self.player)
 self.enemy.boundary_check(self.height, self.width)
 self.canvas.coords(self.enemy_rectangle, self.enemy.x-self.enemy.length/2, self.enemy.y-self.enemy.length/2, self.enemy.x+self.enemy.length/2, self.enemy.y+self.enemy.length/2)
 self.root.after(1000//self.updatesPerSecond, self.enemy_movement)
 def key_test(self, key):
 try:
 return key.name
 except:
 return
 def on_press(self, key):
 key = self.key_test(key)
 if not key in self.keypress_list:
 self.keypress_list.append(key)
 def on_release(self, key):
 key = self.key_test(key)
 self.keypress_list.remove(key)
class SimObject:
 def __init__(self, x, y, speed, length):
 self.x = x
 self.y = y
 self.speed = speed
 self.length = length
 self.hLength = self.length/2
 def boundary_check(self, height, width):
 if self.x - self.hLength < 0:
 self.x = self.hLength
 if self.y - self.hLength < 0:
 self.y = self.hLength
 if self.x + self.hLength > width:
 self.x = width - self.hLength
 if self.y + self.hLength > height:
 self.y = height - self.hLength
 def update_x(self, offset):
 self.x+=offset
 def update_y(self, offset):
 self.y+=offset
class Player(SimObject):
 def __init__(self, x = 400, y = 400, speed = 10, length = 20):
 super().__init__(x, y, speed, length)
class Enemy(SimObject):
 def __init__(self, x = 10, y = 10, speed = 5, length = 20, safe_distance = 100):
 super().__init__(x, y, speed, length)
 self.safe_distance = safe_distance
 self.last_phase = -1
 def update_phase(self, n):
 phase_list=[f"{i} Phase" for i in ["Orbit", "Rush", "Run"]]
 if self.last_phase!=n:
 print(phase_list[n])
 self.last_phase = n
 def update_pos(self, player):
 PI=math.pi
 dx=player.x-self.x
 dy=player.y-self.y
 g_to_p_ang=math.atan2(dy,dx)
 p_to_g_ang=PI+g_to_p_ang
 dist=math.sqrt(dx*dx+dy*dy)
 ang_increase=self.speed/self.safe_distance
 t=p_to_g_ang
 if abs(dist-self.safe_distance)<=self.speed:#near the orbit
 self.update_phase(0)
 t+=ang_increase
 self.x=self.safe_distance*math.cos(t)+player.x
 self.y=self.safe_distance*math.sin(t)+player.y
 elif dist>self.safe_distance:#far from orbit
 self.update_phase(1)
 self.update_x(self.speed*math.cos(g_to_p_ang))
 self.update_y(self.speed*math.sin(g_to_p_ang))
 elif dist<self.safe_distance:#far inside of orbit
 self.update_phase(2)
 self.update_x(self.speed*math.cos(p_to_g_ang))
 self.update_y(self.speed*math.sin(p_to_g_ang))
root = tk.Tk()
root.resizable(0,0)
root.title("Enemy Movement Test")
application = Application(root)
tk.mainloop()
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Jul 21, 2021 at 19:30
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$
  • Format your code according to PEP-8, there are automatic checker and even automatic formatters for that.

  • This is often considered as code smell:

 def key_test(self, key):
 try:
 return key.name
 except:
 return

See: https://stackoverflow.com/questions/10594113/bad-idea-to-catch-all-exceptions-in-python

  • Some methods are longer than I would consider readable and some of repetitive code. Try to extract some repeating code block as a methods and some repetitive expressions as well-named local variables to explain the process.
answered Jul 22, 2021 at 7:44
\$\endgroup\$
2
\$\begingroup\$

In addition to what Roman Pavelka suggests:

  • Represent your keypress_list as a keypresses set (note that it's not helpful to embed the type of a variable in its name; this is what type hints are for)
  • Factor out rectangle-with-margin calculation routines into your SimObject class
  • Do not 'start' anything in your constructor. 'start' in a separate routine or perhaps the entry method of a context manager, stopping in the corresponding exit routine.
  • Do not assign a boolean to self.safeCircle; it should be an Optional (i.e. an object or None)
  • Do not run separate timers for your enemy and player objects; just use one
  • key_test should be using getattr with a None default which will achieve the same effect in a more explicit and safe way
  • Add PEP484 type hints
  • Do not add the key name to the keypress set if the key name is None
  • Rephrase your boundary_check - which does not check at all (nothing is returned), so should be called something like enforce_bounds - as a series of min and max calls
  • Represent last_phase as an enumeration for better maintainability and legibility
  • No need to import pi if you just negate the coordinate deltas based on whether you are in the rush or run phase
  • In update_pos, your last else needs no condition; that's redundant
  • Move the logic at the bottom into a main guard

Suggested

import enum
import math
import tkinter as tk
from enum import Enum
from typing import Optional, Tuple
from pynput import keyboard
from pynput.keyboard import Key
class Application:
 def __init__(
 self, master: tk.Tk, height: int = 800, width: int = 800,
 updates_per_second: int = 10, safe_circle: bool = True,
 ):
 self.height = height
 self.width = width
 self.root = master
 self.updates_per_second = updates_per_second
 self.player = Player()
 self.enemy = Enemy()
 self.canvas = tk.Canvas(self.root, height=self.height, width=self.width)
 self.canvas.pack()
 self.player_rectangle = self.canvas.create_rectangle(*self.player.rect)
 self.enemy_rectangle = self.canvas.create_rectangle(*self.enemy.rect)
 if safe_circle:
 self.safe_circle = self.canvas.create_oval(
 *self.player.margin_rect(self.enemy.safe_distance)
 )
 else:
 self.safe_circle = None
 self.keypresses = set()
 self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
 def __enter__(self) -> 'Application':
 self.listener.start()
 self.movement()
 return self
 def __exit__(self, exc_type, exc_val, exc_tb) -> None:
 self.listener.stop()
 def movement(self) -> None:
 self.player_movement()
 self.enemy_movement()
 self.root.after(1000 // self.updates_per_second, self.movement)
 def player_movement(self) -> None:
 if "down" in self.keypresses:
 self.player.update_y(self.player.speed)
 if "up" in self.keypresses:
 self.player.update_y(-self.player.speed)
 if "left" in self.keypresses:
 self.player.update_x(-self.player.speed)
 if "right" in self.keypresses:
 self.player.update_x(self.player.speed)
 self.player.enforce_bounds(self.height, self.width)
 self.canvas.coords(self.player_rectangle, *self.player.rect)
 if self.safe_circle:
 self.canvas.coords(
 self.safe_circle,
 *self.player.margin_rect(self.enemy.safe_distance)
 )
 def enemy_movement(self) -> None:
 self.enemy.update_pos(self.player.x, self.player.y)
 self.enemy.enforce_bounds(self.height, self.width)
 self.canvas.coords(
 self.enemy_rectangle,
 *self.enemy.margin_rect(self.enemy.length / 2),
 )
 @staticmethod
 def key_test(key: Key) -> Optional[str]:
 return getattr(key, 'name', None)
 def on_press(self, key: Key) -> None:
 key = self.key_test(key)
 if key is not None:
 self.keypresses.add(key)
 def on_release(self, key: Key) -> None:
 key = self.key_test(key)
 self.keypresses.discard(key)
@enum.unique
class EnemyPhase(Enum):
 ORBIT = 'Orbit'
 RUSH = 'Rush'
 RUN = 'Run'
class SimObject:
 def __init__(self, x: int, y: int, speed: int, length: int):
 self.x = x
 self.y = y
 self.speed = speed
 self.length = length
 self.h_length = self.length / 2
 def enforce_bounds(self, height: int, width: int) -> None:
 self.x = max(self.h_length, min(width - self.h_length, self.x))
 self.y = max(self.h_length, min(height - self.h_length, self.y))
 def update_x(self, offset: float) -> None:
 self.x += offset
 def update_y(self, offset: float) -> None:
 self.y += offset
 def margin_rect(self, margin: float) -> Tuple[float, float, float, float]:
 return (
 self.x - margin, self.y - margin,
 self.x + margin, self.y + margin,
 )
 @property
 def rect(self) -> Tuple[float, float, float, float]:
 return self.margin_rect(self.h_length)
class Player(SimObject):
 def __init__(self, x: int = 400, y: int = 400, speed: int = 10, length: int = 20):
 super().__init__(x, y, speed, length)
class Enemy(SimObject):
 def __init__(
 self, x: int = 10, y: int = 10, speed: int = 5, length: int = 20,
 safe_distance: int = 100,
 ):
 super().__init__(x, y, speed, length)
 self.safe_distance = safe_distance
 self.last_phase = EnemyPhase.RUSH
 def update_phase(self, phase: EnemyPhase) -> None:
 if self.last_phase != phase:
 self.last_phase = phase
 print(f'{phase.value} Phase')
 def update_pos(self, player_x: float, player_y: float) -> None:
 dx = self.x - player_x
 dy = self.y - player_y
 p_to_g_ang = math.atan2(dy, dx)
 dist = math.sqrt(dx*dx + dy*dy)
 if abs(dist - self.safe_distance) <= self.speed: # near the orbit
 ang_increase = self.speed / self.safe_distance
 t = p_to_g_ang + ang_increase
 self.update_phase(EnemyPhase.ORBIT)
 self.x = self.safe_distance * math.cos(t) + player_x
 self.y = self.safe_distance * math.sin(t) + player_y
 else:
 sx = self.speed * math.cos(p_to_g_ang)
 sy = self.speed * math.sin(p_to_g_ang)
 if dist > self.safe_distance: # far from orbit
 self.update_phase(EnemyPhase.RUSH)
 self.update_x(-sx)
 self.update_y(-sy)
 else: # far inside of orbit
 self.update_phase(EnemyPhase.RUN)
 self.update_x(sx)
 self.update_y(sy)
def main():
 root = tk.Tk()
 root.resizable(0, 0)
 root.title("Enemy Movement Test")
 with Application(root):
 tk.mainloop()
if __name__ == '__main__':
 main()
answered Jul 22, 2021 at 21:40
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the review! As I did just start Python not long ago, I still have some trouble comprehending many parts of your suggested code, so I will need some time to be able to understand it. For now I just want to ask, do I need to install the enum and typing modules, and could you explain in more detail what those modules are doing? \$\endgroup\$ Commented Jul 22, 2021 at 22:28
  • \$\begingroup\$ Both modules are built-in; you have them already. typing adds non-runtime hints for programmers and static analysers to better understand your code. Enumerations are used to represent a variable that can take a constrained set of values. \$\endgroup\$ Commented Jul 23, 2021 at 2:01

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.