Coverage for src/glomph/entities.py: 0%

75 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-06 00:21 +0800

1"""Game entities (player, ghosts, etc.).""" 

2 

3from enum import Enum 

4from typing import NamedTuple 

5 

6 

7class Position(NamedTuple): 

8 """2D position coordinates.""" 

9 x: int 

10 y: int 

11 

12 

13class Velocity(NamedTuple): 

14 """Movement velocity.""" 

15 dx: int 

16 dy: int 

17 

18 

19class Entity: 

20 """Base class for game entities.""" 

21 

22 def __init__(self, position: Position, char: str = "?", color: int = 0) -> None: 

23 """Initialize entity.""" 

24 self.position = position 

25 self.char = char 

26 self.color = color 

27 self.velocity = Velocity(0, 0) 

28 

29 def move(self, dx: int = 0, dy: int = 0) -> None: 

30 """Move entity by delta.""" 

31 self.position = Position( 

32 self.position.x + dx, 

33 self.position.y + dy 

34 ) 

35 

36 def set_velocity(self, dx: int, dy: int) -> None: 

37 """Set movement velocity.""" 

38 self.velocity = Velocity(dx, dy) 

39 

40 def update(self) -> None: 

41 """Update entity state.""" 

42 self.move(self.velocity.dx, self.velocity.dy) 

43 

44 

45class Player(Entity): 

46 """Player entity (Pac-Man).""" 

47 

48 def __init__(self, position: Position) -> None: 

49 """Initialize player.""" 

50 super().__init__(position, "@", 2) # Red @ symbol 

51 self.lives = 3 

52 self.score = 0 

53 self.direction = "left" # Current facing direction 

54 

55 def set_direction(self, direction: str) -> None: 

56 """Set player direction and update velocity.""" 

57 self.direction = direction 

58 if direction == "up": 

59 self.set_velocity(0, -1) 

60 elif direction == "down": 

61 self.set_velocity(0, 1) 

62 elif direction == "left": 

63 self.set_velocity(-1, 0) 

64 elif direction == "right": 

65 self.set_velocity(1, 0) 

66 else: 

67 self.set_velocity(0, 0) 

68 

69 def lose_life(self) -> None: 

70 """Player loses a life.""" 

71 self.lives -= 1 

72 

73 def add_score(self, points: int) -> None: 

74 """Add points to score.""" 

75 self.score += points 

76 

77 

78class GhostMode(Enum): 

79 """Ghost behavior modes.""" 

80 SCATTER = "scatter" 

81 CHASE = "chase" 

82 FRIGHTENED = "frightened" 

83 

84 

85class Ghost(Entity): 

86 """Ghost entity.""" 

87 

88 def __init__(self, position: Position, name: str, color: int) -> None: 

89 """Initialize ghost.""" 

90 super().__init__(position, "M", color) # M for ghost 

91 self.name = name 

92 self.mode = GhostMode.SCATTER 

93 self.target = position # Position to move towards 

94 self.home_corner = position # Scatter mode target corner 

95 

96 def set_mode(self, mode: GhostMode) -> None: 

97 """Set ghost behavior mode.""" 

98 self.mode = mode 

99 if mode == GhostMode.FRIGHTENED: 

100 self.char = "Ø" # Different appearance when frightened 

101 self.color = 5 # Magenta when frightened 

102 else: 

103 self.char = "M" 

104 # Reset to normal color (would be per-ghost) 

105 

106 def set_target(self, target: Position) -> None: 

107 """Set movement target position.""" 

108 self.target = target 

109 

110 def update_ai(self, player_pos: Position) -> None: 

111 """Update ghost AI based on current mode.""" 

112 if self.mode == GhostMode.SCATTER: 

113 self.set_target(self.home_corner) 

114 elif self.mode == GhostMode.CHASE: 

115 # Different chase behaviors per ghost (simplified for now) 

116 self.set_target(player_pos) 

117 elif self.mode == GhostMode.FRIGHTENED: 

118 # Random movement when frightened (simplified) 

119 pass 

120 

121 def calculate_direction(self) -> str: 

122 """Calculate best direction to move towards target.""" 

123 # Simple pathfinding: move towards target 

124 dx = self.target.x - self.position.x 

125 dy = self.target.y - self.position.y 

126 

127 # Prefer larger axis difference 

128 if abs(dx) > abs(dy): 

129 return "right" if dx > 0 else "left" 

130 else: 

131 return "down" if dy > 0 else "up" 

132 

133 

134def create_ghosts() -> list[Ghost]: 

135 """Create the four classic ghosts.""" 

136 return [ 

137 Ghost(Position(14, 11), "Blinky", 2), # Red, targets player directly 

138 Ghost(Position(14, 13), "Pinky", 5), # Pink, ambushes ahead of player 

139 Ghost(Position(12, 13), "Inky", 4), # Cyan, uses complex targeting 

140 Ghost(Position(16, 13), "Clyde", 3), # Orange, random when close 

141 ]