gh-space-shooter 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ """Player ship object."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from PIL import ImageDraw
6
+
7
+ from ...constants import SHIP_POSITION_Y, SHIP_SPEED
8
+ from .drawable import Drawable
9
+
10
+ if TYPE_CHECKING:
11
+ from ..game_state import GameState
12
+ from ..render_context import RenderContext
13
+
14
+
15
+ class Ship(Drawable):
16
+ """Represents the player's ship."""
17
+
18
+ def __init__(self, game_state: "GameState"):
19
+ """Initialize the ship at starting position."""
20
+ self.x: float = 25 # Start middle of screen
21
+ self.target_x = self.x
22
+ self.shoot_cooldown = 0 # Frames until ship can shoot again
23
+ self.game_state = game_state
24
+
25
+ def move_to(self, x: int):
26
+ """
27
+ Move ship to a new x position.
28
+
29
+ Args:
30
+ x: Target x position
31
+ """
32
+ self.target_x = x
33
+
34
+ def is_moving(self) -> bool:
35
+ """Check if ship is moving to a new position."""
36
+ return self.x != self.target_x
37
+
38
+ def can_shoot(self) -> bool:
39
+ """Check if ship can shoot (cooldown has finished)."""
40
+ return self.shoot_cooldown == 0
41
+
42
+ def animate(self) -> None:
43
+ """Update ship position, moving toward target at constant speed."""
44
+ if self.x < self.target_x:
45
+ self.x = min(self.x + SHIP_SPEED, self.target_x)
46
+ elif self.x > self.target_x:
47
+ self.x = max(self.x - SHIP_SPEED, self.target_x)
48
+
49
+ # Decrement shoot cooldown
50
+ if self.shoot_cooldown > 0:
51
+ self.shoot_cooldown -= 1
52
+
53
+ def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
54
+ """Draw a simple Galaga-style ship."""
55
+ x, y = context.get_cell_position(self.x, SHIP_POSITION_Y)
56
+
57
+ # Calculate ship dimensions
58
+ center_x = x + context.cell_size // 2
59
+ height = context.cell_size
60
+ wing_width = 8
61
+
62
+ # Draw left wing
63
+ draw.polygon(
64
+ [
65
+ (center_x - 2, y + height * 0.5),
66
+ (center_x - wing_width, y + height * 0.8),
67
+ (center_x - wing_width, y + height * 1),
68
+ (center_x - 2, y + height * 0.7),
69
+ ],
70
+ fill=(*context.ship_color, 128)
71
+ )
72
+ draw.rectangle(
73
+ [
74
+ center_x - wing_width - 1, y + height * 0.5,
75
+ center_x - wing_width, y + height * 1,
76
+ ],
77
+ fill=context.ship_color
78
+ )
79
+
80
+ # Draw right wing
81
+ draw.polygon(
82
+ [
83
+ (center_x + 2, y + height * 0.5),
84
+ (center_x + wing_width, y + height * 0.8),
85
+ (center_x + wing_width, y + height * 1),
86
+ (center_x + 2, y + height * 0.7),
87
+ ],
88
+ fill=(*context.ship_color, 128)
89
+ )
90
+ draw.rectangle(
91
+ [
92
+ center_x + wing_width, y + height * 0.5,
93
+ center_x + wing_width + 1, y + height * 1
94
+ ],
95
+ fill=context.ship_color
96
+ )
97
+
98
+
99
+ draw.polygon(
100
+ [
101
+ (center_x, y),
102
+ (center_x - 3, y + height * 0.7),
103
+ (center_x, y + height),
104
+ (center_x + 3, y + height * 0.7),
105
+ ],
106
+ fill=context.ship_color
107
+ )
@@ -0,0 +1,65 @@
1
+ """Animated starfield background."""
2
+
3
+ import random
4
+ from typing import TYPE_CHECKING
5
+
6
+ from PIL import ImageDraw
7
+
8
+ from ...constants import NUM_WEEKS, SHIP_POSITION_Y
9
+ from .drawable import Drawable
10
+
11
+ if TYPE_CHECKING:
12
+ from ..render_context import RenderContext
13
+
14
+
15
+ class Starfield(Drawable):
16
+ """Animated starfield background with slowly moving stars."""
17
+
18
+ def __init__(self):
19
+ """Initialize the starfield with random stars."""
20
+ self.stars = []
21
+ # Generate about 100 stars across the play area
22
+ for _ in range(100):
23
+ # Random position across the entire grid area
24
+ x = random.uniform(-2, NUM_WEEKS + 2)
25
+ y = random.uniform(-2, SHIP_POSITION_Y + 4)
26
+ # Brightness: 0.2 to 1.0 (dimmer stars for depth)
27
+ brightness = random.uniform(0.2, 1.0)
28
+ # Size: 1-2 pixels
29
+ size = random.choice([1, 1, 1, 2]) # More 1-pixel stars
30
+ # Speed: slower for dimmer (farther) stars
31
+ speed = 0.02 + (brightness * 0.03) # 0.02-0.05 cells per frame
32
+ self.stars.append([x, y, brightness, size, speed])
33
+
34
+ def animate(self) -> None:
35
+ """Move stars downward, wrapping around when they go off screen."""
36
+ for star in self.stars:
37
+ # star[1] is the y position, star[4] is the speed
38
+ star[1] += star[4]
39
+
40
+ # Wrap around: if star goes below the screen, move it back to the top
41
+ if star[1] > SHIP_POSITION_Y + 4:
42
+ star[1] = -2
43
+ # Randomize x position when wrapping for variety
44
+ star[0] = random.uniform(-2, NUM_WEEKS + 2)
45
+
46
+ def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
47
+ """Draw all stars at their current positions."""
48
+ for star_x, star_y, brightness, size, _ in self.stars:
49
+ # Convert grid position to pixel position
50
+ x, y = context.get_cell_position(star_x, star_y)
51
+
52
+ # Calculate star color (white with varying brightness)
53
+ star_brightness = int(255 * brightness)
54
+ star_color = (star_brightness, star_brightness, star_brightness, 255)
55
+
56
+ # Draw star as a small rectangle or point
57
+ if size == 1:
58
+ # Single pixel star
59
+ draw.point([(x, y)], fill=star_color)
60
+ else:
61
+ # Slightly larger star (2x2)
62
+ draw.rectangle(
63
+ [x, y, x + size - 1, y + size - 1],
64
+ fill=star_color
65
+ )
@@ -0,0 +1,80 @@
1
+ """Game state management for tracking enemies, ship, and bullets."""
2
+
3
+ from typing import TYPE_CHECKING, List
4
+
5
+ from PIL import ImageDraw
6
+
7
+ from ..constants import SHIP_SHOOT_COOLDOWN_FRAMES
8
+ from ..github_client import ContributionData
9
+ from .drawables import Bullet, Drawable, Enemy, Explosion, Ship, Starfield
10
+
11
+ if TYPE_CHECKING:
12
+ from .render_context import RenderContext
13
+
14
+
15
+ class GameState(Drawable):
16
+ """Manages the current state of the game."""
17
+
18
+ def __init__(self, contribution_data: ContributionData):
19
+ """
20
+ Initialize game state from contribution data.
21
+
22
+ Args:
23
+ contribution_data: The GitHub contribution data
24
+ """
25
+ self.starfield = Starfield()
26
+ self.ship = Ship(self)
27
+ self.enemies: List[Enemy] = []
28
+ self.bullets: List[Bullet] = []
29
+ self.explosions: List[Explosion] = []
30
+
31
+ self._initialize_enemies(contribution_data)
32
+
33
+ def _initialize_enemies(self, contribution_data: ContributionData):
34
+ """Create enemies based on contribution levels."""
35
+ weeks = contribution_data["weeks"]
36
+ for week_idx, week in enumerate(weeks):
37
+ for day_idx, day in enumerate(week["days"]):
38
+ level = day["level"]
39
+ if level <= 0:
40
+ continue
41
+ enemy = Enemy(x=week_idx, y=day_idx, health=level, game_state=self)
42
+ self.enemies.append(enemy)
43
+
44
+ def shoot(self) -> None:
45
+ """
46
+ Ship shoots a bullet and starts cooldown timer.
47
+ """
48
+ bullet = Bullet(int(self.ship.x), game_state=self)
49
+ self.bullets.append(bullet)
50
+ self.ship.shoot_cooldown = SHIP_SHOOT_COOLDOWN_FRAMES
51
+
52
+ def is_complete(self) -> bool:
53
+ """Check if game is complete (all enemies destroyed)."""
54
+ return len(self.enemies) == 0
55
+
56
+ def can_take_action(self) -> bool:
57
+ """Check if ship can take an action (not moving and can shoot)."""
58
+ return not self.ship.is_moving() and self.ship.can_shoot()
59
+
60
+ def animate(self) -> None:
61
+ """Update all game objects for next frame."""
62
+ self.starfield.animate()
63
+ self.ship.animate()
64
+ for enemy in self.enemies:
65
+ enemy.animate()
66
+ for bullet in self.bullets:
67
+ bullet.animate()
68
+ for explosion in self.explosions:
69
+ explosion.animate()
70
+
71
+ def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
72
+ """Draw all game objects including the grid."""
73
+ self.starfield.draw(draw, context)
74
+ for enemy in self.enemies:
75
+ enemy.draw(draw, context)
76
+ for explosion in self.explosions:
77
+ explosion.draw(draw, context)
78
+ for bullet in self.bullets:
79
+ bullet.draw(draw, context)
80
+ self.ship.draw(draw, context)
@@ -0,0 +1,56 @@
1
+ """Rendering context for drawable objects."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Tuple
5
+
6
+
7
+ @dataclass
8
+ class RenderContext:
9
+ """
10
+ Context providing rendering helpers and constants to drawable objects.
11
+
12
+ This encapsulates all the information needed to render game objects,
13
+ including colors, sizes, and helper functions. Can be extended for theming.
14
+ """
15
+
16
+ # Size constants
17
+ cell_size: int
18
+ cell_spacing: int
19
+ padding: int
20
+
21
+ # Colors - can be customized for different themes
22
+ background_color: Tuple[int, int, int]
23
+ grid_color: Tuple[int, int, int]
24
+ ship_color: Tuple[int, int, int]
25
+ bullet_color: Tuple[int, int, int]
26
+ enemy_colors: dict[int, Tuple[int, int, int]] # Maps health level to color
27
+
28
+ def get_cell_position(self, x: float, y: float) -> tuple[float, float]:
29
+ """
30
+ Get the pixel position (x, y) for a grid coordinate.
31
+
32
+ Args:
33
+ week: Week position (0-51, can be fractional for smooth animation)
34
+ day: Day position (0-6, can be fractional for smooth animation)
35
+
36
+ Returns:
37
+ Tuple of (x, y) pixel coordinates
38
+ """
39
+ return (
40
+ self.padding + x * (self.cell_size + self.cell_spacing),
41
+ self.padding + y * (self.cell_size + self.cell_spacing),
42
+ )
43
+
44
+ @staticmethod
45
+ def darkmode() -> "RenderContext":
46
+ """Predefined dark mode rendering context."""
47
+ return RenderContext(
48
+ cell_size=12,
49
+ cell_spacing=3,
50
+ padding=40,
51
+ background_color=(13, 17, 23),
52
+ grid_color=(22, 27, 34),
53
+ enemy_colors={1: (0, 109, 50), 2: (38, 166, 65), 3: (57, 211, 83), 4: (87, 242, 135)},
54
+ ship_color=(68, 147, 248),
55
+ bullet_color=(255, 223, 0),
56
+ )
@@ -0,0 +1,44 @@
1
+ """Renderer for drawing game frames using Pillow."""
2
+
3
+ from PIL import Image, ImageDraw
4
+
5
+ from ..constants import NUM_WEEKS, SHIP_POSITION_Y
6
+ from .game_state import GameState
7
+ from .render_context import RenderContext
8
+
9
+ class Renderer:
10
+ """Renders game state as PIL Images."""
11
+ def __init__(self, game_state: GameState, render_context: RenderContext):
12
+ """
13
+ Initialize renderer.
14
+
15
+ Args:
16
+ game_state: The game state to render
17
+ render_context: Rendering configuration and theming
18
+ """
19
+ self.game_state = game_state
20
+
21
+ self.context = render_context
22
+
23
+ self.grid_width = NUM_WEEKS * (self.context.cell_size + self.context.cell_spacing)
24
+ self.grid_height = SHIP_POSITION_Y * (self.context.cell_size + self.context.cell_spacing)
25
+ self.width = self.grid_width + 2 * self.context.padding
26
+ self.height = self.grid_height + 2 * self.context.padding
27
+
28
+ def render_frame(self) -> Image.Image:
29
+ """
30
+ Render the current game state as an image.
31
+
32
+ Returns:
33
+ PIL Image of the current frame
34
+ """
35
+ # Create image with background color
36
+ img = Image.new("RGB", (self.width, self.height), self.context.background_color)
37
+
38
+ # Draw game state
39
+ overlay = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0))
40
+ draw = ImageDraw.Draw(overlay, "RGBA")
41
+ self.game_state.draw(draw, self.context)
42
+ combined = Image.alpha_composite(img.convert("RGBA"), overlay)
43
+
44
+ return combined.convert("RGB").convert("P", palette=Image.Palette.ADAPTIVE)
@@ -0,0 +1,14 @@
1
+ """Strategy implementations for enemy clearing."""
2
+
3
+ from .base_strategy import Action, BaseStrategy
4
+ from .column_strategy import ColumnStrategy
5
+ from .random_strategy import RandomStrategy
6
+ from .row_strategy import RowStrategy
7
+
8
+ __all__ = [
9
+ "BaseStrategy",
10
+ "Action",
11
+ "ColumnStrategy",
12
+ "RowStrategy",
13
+ "RandomStrategy",
14
+ ]
@@ -0,0 +1,43 @@
1
+ """Base strategy interface for enemy clearing strategies."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Iterator
5
+
6
+ if TYPE_CHECKING:
7
+ from ..game_state import GameState
8
+
9
+
10
+ class Action:
11
+ """Represents a single action in the game."""
12
+
13
+ def __init__(self, x: int, shoot: bool = False):
14
+ """
15
+ Initialize an action.
16
+
17
+ Args:
18
+ x: Week position (0-51) where ship should move
19
+ shoot: Whether to shoot at this position
20
+ """
21
+ self.x = x
22
+ self.shoot = shoot
23
+
24
+ def __repr__(self) -> str:
25
+ action_type = "SHOOT" if self.shoot else "MOVE"
26
+ return f"Action({action_type} x={self.x})"
27
+
28
+
29
+ class BaseStrategy(ABC):
30
+ """Abstract base class for enemy clearing strategies."""
31
+
32
+ @abstractmethod
33
+ def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
34
+ """
35
+ Generate sequence of actions for the ship to clear enemies.
36
+
37
+ Args:
38
+ game_state: The current game state with enemies, ship, and bullets
39
+
40
+ Yields:
41
+ Action objects representing ship movements and shots
42
+ """
43
+ pass
@@ -0,0 +1,46 @@
1
+ """Column-by-column strategy: Ship moves week by week (left to right)."""
2
+
3
+ from typing import TYPE_CHECKING, Iterator
4
+
5
+ from ...constants import NUM_WEEKS
6
+ from .base_strategy import Action, BaseStrategy
7
+
8
+ if TYPE_CHECKING:
9
+ from ..game_state import GameState
10
+
11
+
12
+ class ColumnStrategy(BaseStrategy):
13
+ """
14
+ Ship moves column by column (week by week) from left to right.
15
+
16
+ For each week, the ship shoots at all enemies in that column until destroyed,
17
+ reacting to the actual game state rather than planning ahead.
18
+ """
19
+
20
+ def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
21
+ """
22
+ Generate actions moving week by week, reacting to living enemies.
23
+
24
+ The ship moves through each week (column), shooting at enemies
25
+ until all enemies in that week are destroyed, then moves to the next week.
26
+ Uses actual game state to determine which enemies are still alive.
27
+
28
+ Args:
29
+ game_state: The current game state with living enemies
30
+
31
+ Yields:
32
+ Action objects representing ship movements and shots
33
+ """
34
+ # Process each week (column) from left to right
35
+ for week_idx in range(NUM_WEEKS):
36
+ # Keep shooting at enemies in this week until none remain
37
+ while True:
38
+ # Find all living enemies in this week
39
+ enemies_in_week = [e for e in game_state.enemies if e.x == week_idx]
40
+ total_health = sum(e.health for e in enemies_in_week)
41
+ flying_bullets = len([b for b in game_state.bullets if int(b.x) == week_idx])
42
+
43
+ if flying_bullets >= total_health:
44
+ break
45
+
46
+ yield Action(x=week_idx, shoot=True)
@@ -0,0 +1,61 @@
1
+ """Random strategy: Pick random columns and shoot from bottom up."""
2
+
3
+ import random
4
+ from typing import TYPE_CHECKING, Iterator
5
+
6
+ from .base_strategy import Action, BaseStrategy
7
+
8
+ if TYPE_CHECKING:
9
+ from ..game_state import GameState
10
+
11
+
12
+ class RandomStrategy(BaseStrategy):
13
+ """
14
+ Ship uses weighted random selection to pick columns based on distance.
15
+
16
+ Takes the 8 closest columns and applies distance-based weights for selection,
17
+ creating a balanced mix of efficiency and unpredictability.
18
+ """
19
+
20
+ def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
21
+ """
22
+ Generate actions using weighted random selection based on distance.
23
+
24
+ Sorts columns by distance, takes the 8 closest, and applies weights:
25
+ - Distance 0 (same position): weight 3
26
+ - Distance 1-3: weight 5 (highest priority)
27
+ - Distance 4+: weight 1 (lowest priority)
28
+
29
+ Args:
30
+ game_state: The current game state with living enemies
31
+
32
+ Yields:
33
+ Action objects representing ship movements and shots
34
+ """
35
+ while game_state.enemies:
36
+ columns_with_enemies = list(set(e.x for e in game_state.enemies))
37
+ ship_x = game_state.ship.x
38
+
39
+ # Take the first 8 closest columns
40
+ columns_by_distance = sorted(columns_with_enemies, key=lambda col: abs(col - ship_x))
41
+ candidate_columns = columns_by_distance[:8]
42
+
43
+ # Assign weights based on distance
44
+ weights = []
45
+ for col in candidate_columns:
46
+ distance = abs(col - ship_x)
47
+ if distance == 0:
48
+ weights.append(10)
49
+ elif 1 <= distance <= 3:
50
+ weights.append(100)
51
+ else: # distance >= 4
52
+ weights.append(1)
53
+
54
+ # Choose randomly with weights
55
+ target_column = random.choices(candidate_columns, weights=weights)[0]
56
+
57
+ enemies_in_column = [e for e in game_state.enemies if e.x == target_column]
58
+ lowest_enemy = max(enemies_in_column, key=lambda e: e.y)
59
+
60
+ for _ in range(lowest_enemy.health):
61
+ yield Action(x=target_column, shoot=True)
@@ -0,0 +1,41 @@
1
+ """Row-by-row strategy: Process enemies row by row (day by day)."""
2
+
3
+ from typing import TYPE_CHECKING, Iterator
4
+
5
+ from ...constants import NUM_DAYS
6
+ from .base_strategy import Action, BaseStrategy
7
+
8
+ if TYPE_CHECKING:
9
+ from ..game_state import GameState
10
+
11
+
12
+ class RowStrategy(BaseStrategy):
13
+ """
14
+ Ship processes enemies row by row (day by day) from top to bottom.
15
+
16
+ For each row (day), the ship moves to each enemy position in that row
17
+ and shoots until all enemies in the row are destroyed.
18
+ """
19
+
20
+ def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
21
+ """
22
+ Generate actions processing enemies row by row.
23
+
24
+ The ship processes each day (row) from Sunday (0) to Saturday (6),
25
+ moving horizontally to shoot at all enemies in that row before
26
+ proceeding to the next row.
27
+
28
+ Args:
29
+ game_state: The current game state with living enemies
30
+
31
+ Yields:
32
+ Action objects representing ship movements and shots
33
+ """
34
+ # Process each day (row) from top to bottom
35
+ for day_idx in range(NUM_DAYS - 1, -1, -1):
36
+ enemies_in_row = [e for e in game_state.enemies if e.y == day_idx]
37
+ enemies_in_row.sort(key=lambda e: e.x * (day_idx % 2 * 2 - 1)) # zig-zag
38
+
39
+ for enemy in enemies_in_row:
40
+ for _ in range(enemy.health):
41
+ yield Action(x=enemy.x, shoot=True)