Skip to main content

Player

game/player.py defines the Player class, a pygame.sprite.Sprite that handles movement input, screen-boundary clamping, motion trail rendering, and health state.
class Player(pygame.sprite.Sprite):
    def __init__(self, x: int, y: int) -> None
    def handle_input(self) -> None
    def update(self, screen_width: int, screen_height: int) -> None
    def draw(self, screen: pygame.Surface) -> None

Attributes

AttributeTypeDefaultDescription
imagepygame.Surface32×32 SRCALPHANeon-green rounded rectangle sprite. Outer border (0, 255, 150), inner fill (0, 100, 50).
rectpygame.RectCentred at (x, y)Collision and positioning rect derived from image.
pospygame.Vector2(x, y)Sub-pixel position used for all movement calculations.
trail_positionslist[tuple[float, float]][]Ring buffer of the last 10 positions, newest first.
speedint6Movement speed in pixels per frame (applied after normalisation).
healthint100Current health. Decremented by 15 on enemy contact.
max_healthint100Maximum health. Used by the HUD to calculate the health bar fill ratio.

__init__(x, y)

x
int
required
Horizontal spawn position in screen coordinates. Typically width // 2.
y
int
required
Vertical spawn position in screen coordinates. Typically height // 2.
Constructs the sprite surface, sets pos and rect, and initialises all attributes to their defaults. The sprite image is a 32×32 surface with pygame.SRCALPHA, drawn with two rounded rectangles to create a glowing terminal-cursor look.

handle_input()

Reads the current keyboard state and moves the player. Called internally by update().
def handle_input(self) -> None:
    keys = pygame.key.get_pressed()
    move = pygame.Vector2(0, 0)
    if keys[pygame.K_w] or keys[pygame.K_UP]:   move.y -= 1
    if keys[pygame.K_s] or keys[pygame.K_DOWN]:  move.y += 1
    if keys[pygame.K_a] or keys[pygame.K_LEFT]:  move.x -= 1
    if keys[pygame.K_d] or keys[pygame.K_RIGHT]: move.x += 1

    if move.length() > 0:
        move = move.normalize() * self.speed
        self.pos += move
        self.rect.center = self.pos

Supported keys

ActionKeys
Move upW,
Move downS,
Move leftA,
Move rightD,
The move vector is normalised before scaling, so diagonal movement is the same speed as cardinal movement (no speed boost when pressing two keys simultaneously).
The main game loop also reads key state independently to track last_direction for bullet firing. Both reads happen from the same pygame.key.get_pressed() snapshot.

update(screen_width, screen_height)

Called once per frame by the game loop via player.update(width, height).
screen_width
int
required
Current screen width in pixels. Used as the right boundary for clamping.
screen_height
int
required
Current screen height in pixels. Used as the bottom boundary for clamping.
Execution order:
1

Process input

Calls handle_input() to apply keyboard movement to self.pos.
2

Clamp to screen bounds

Ensures the player sprite centre never gets closer than 16 px to any screen edge:
self.pos.x = max(16, min(screen_width  - 16, self.pos.x))
self.pos.y = max(16, min(screen_height - 16, self.pos.y))
self.rect.center = self.pos
The 16 px margin equals half the sprite width/height, so the player is fully visible at all times.
3

Update trail buffer

Inserts the current position at the front of trail_positions and trims the list to 10 entries:
self.trail_positions.insert(0, tuple(self.pos))
if len(self.trail_positions) > 10:
    self.trail_positions.pop()
The oldest position is always at index 9.
Wall collision is handled in the main game loop after player.update() returns. If the player’s rect overlaps a wall rect, player.pos is reset to its pre-update value.

draw(screen)

Renders the motion trail and the player sprite.
screen
pygame.Surface
required
The active Pygame display surface.

Trail rendering

Iterates over trail_positions from newest (index 0) to oldest (index 9). Each ghost frame is a semi-transparent cyan rectangle that shrinks and fades with age:
for i, pos in enumerate(self.trail_positions):
    alpha = 150 - (i * 15)   # 150, 135, 120, ..., 15
    size  = 32  - (i * 2)    # 32,  30,  28,  ..., 14
    if size <= 0:
        continue
    s = pygame.Surface((size, size), pygame.SRCALPHA)
    pygame.draw.rect(s, (0, 200, 255, alpha), (0, 0, size, size), 2, border_radius=2)
    screen.blit(s, s.get_rect(center=pos))
Frame indexAlphaSize (px)
0 (newest)15032
113530
212028
9 (oldest)1514
Only the border of each ghost is drawn (stroke width 2), giving a wireframe ghost effect.

Sprite rendering

After the trail, the player’s own image is blitted at self.rect:
screen.blit(self.image, self.rect)
draw() is called directly by the main loop and does not use the shake offset, so the player sprite stays visually stable while the world shakes around it.

Build docs developers (and LLMs) love