gh-space-shooter 0.0.3__py3-none-any.whl → 0.0.5__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.
- gh_space_shooter/cli.py +38 -6
- gh_space_shooter/constants.py +17 -6
- gh_space_shooter/game/animator.py +29 -16
- gh_space_shooter/game/drawables/bullet.py +9 -5
- gh_space_shooter/game/drawables/drawable.py +6 -2
- gh_space_shooter/game/drawables/enemy.py +1 -1
- gh_space_shooter/game/drawables/explosion.py +13 -9
- gh_space_shooter/game/drawables/ship.py +13 -8
- gh_space_shooter/game/drawables/starfield.py +31 -15
- gh_space_shooter/game/game_state.py +13 -9
- gh_space_shooter/game/renderer.py +31 -5
- {gh_space_shooter-0.0.3.dist-info → gh_space_shooter-0.0.5.dist-info}/METADATA +9 -2
- gh_space_shooter-0.0.5.dist-info/RECORD +28 -0
- gh_space_shooter-0.0.3.dist-info/RECORD +0 -28
- {gh_space_shooter-0.0.3.dist-info → gh_space_shooter-0.0.5.dist-info}/WHEEL +0 -0
- {gh_space_shooter-0.0.3.dist-info → gh_space_shooter-0.0.5.dist-info}/entry_points.txt +0 -0
- {gh_space_shooter-0.0.3.dist-info → gh_space_shooter-0.0.5.dist-info}/licenses/LICENSE +0 -0
gh_space_shooter/cli.py
CHANGED
|
@@ -8,8 +8,8 @@ import typer
|
|
|
8
8
|
from dotenv import load_dotenv
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
|
|
11
|
+
from .constants import DEFAULT_FPS
|
|
12
|
+
from .game.strategies.base_strategy import BaseStrategy
|
|
13
13
|
from .console_printer import ContributionConsolePrinter
|
|
14
14
|
from .game import Animator, ColumnStrategy, RandomStrategy, RowStrategy
|
|
15
15
|
from .github_client import ContributionData, GitHubAPIError, GitHubClient
|
|
@@ -55,6 +55,21 @@ def main(
|
|
|
55
55
|
"-s",
|
|
56
56
|
help="Strategy for clearing enemies (column, row, random)",
|
|
57
57
|
),
|
|
58
|
+
fps: int = typer.Option(
|
|
59
|
+
DEFAULT_FPS,
|
|
60
|
+
"--fps",
|
|
61
|
+
help="Frames per second for the animation",
|
|
62
|
+
),
|
|
63
|
+
maxFrame: int | None = typer.Option(
|
|
64
|
+
None,
|
|
65
|
+
"--max-frame",
|
|
66
|
+
help="Maximum number of frames to generate",
|
|
67
|
+
),
|
|
68
|
+
watermark: bool = typer.Option(
|
|
69
|
+
False,
|
|
70
|
+
"--watermark",
|
|
71
|
+
help="Add watermark to the GIF",
|
|
72
|
+
),
|
|
58
73
|
) -> None:
|
|
59
74
|
"""
|
|
60
75
|
Fetch or load GitHub contribution graph data and display it.
|
|
@@ -90,7 +105,7 @@ def main(
|
|
|
90
105
|
_save_data_to_file(data, raw_output)
|
|
91
106
|
|
|
92
107
|
# Generate GIF if requested
|
|
93
|
-
_generate_gif(data, out, strategy)
|
|
108
|
+
_generate_gif(data, out, strategy, fps, watermark, maxFrame)
|
|
94
109
|
|
|
95
110
|
except CLIError as e:
|
|
96
111
|
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
@@ -146,8 +161,21 @@ def _save_data_to_file(data: ContributionData, file_path: str) -> None:
|
|
|
146
161
|
raise CLIError(f"Failed to save file '{file_path}': {e}")
|
|
147
162
|
|
|
148
163
|
|
|
149
|
-
def _generate_gif(
|
|
164
|
+
def _generate_gif(
|
|
165
|
+
data: ContributionData,
|
|
166
|
+
file_path: str,
|
|
167
|
+
strategy_name: str,
|
|
168
|
+
fps: int,
|
|
169
|
+
watermark: bool,
|
|
170
|
+
maxFrame: int | None
|
|
171
|
+
) -> None:
|
|
150
172
|
"""Generate animated GIF visualization."""
|
|
173
|
+
# GIF format limitation: delays below 20ms (>50 FPS) are clamped by most browsers
|
|
174
|
+
if fps > 50:
|
|
175
|
+
console.print(
|
|
176
|
+
f"[yellow]Warning:[/yellow] FPS > 50 may not display correctly in browsers "
|
|
177
|
+
f"(GIF delay will be {1000 // fps}ms, but browsers clamp delays < 20ms to ~100ms)"
|
|
178
|
+
)
|
|
151
179
|
console.print("\n[bold blue]Generating GIF animation...[/bold blue]")
|
|
152
180
|
|
|
153
181
|
if strategy_name == "column":
|
|
@@ -163,8 +191,12 @@ def _generate_gif(data: ContributionData, file_path: str, strategy_name: str) ->
|
|
|
163
191
|
|
|
164
192
|
# Create animator and generate GIF
|
|
165
193
|
try:
|
|
166
|
-
animator = Animator(data, strategy)
|
|
167
|
-
animator.generate_gif(
|
|
194
|
+
animator = Animator(data, strategy, fps=fps, watermark=watermark)
|
|
195
|
+
buffer = animator.generate_gif(maxFrame=maxFrame)
|
|
196
|
+
console.print("[bold blue]Saving GIF animation...[/bold blue]")
|
|
197
|
+
with open(file_path, "wb") as f:
|
|
198
|
+
f.write(buffer.getvalue())
|
|
199
|
+
|
|
168
200
|
console.print(f"[green]✓[/green] GIF saved to {file_path}")
|
|
169
201
|
except Exception as e:
|
|
170
202
|
raise CLIError(f"Failed to generate GIF: {e}")
|
gh_space_shooter/constants.py
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
"""Global constants for the application."""
|
|
2
2
|
|
|
3
|
+
# Animation settings
|
|
4
|
+
DEFAULT_FPS = 40 # Default frames per second for animation
|
|
5
|
+
|
|
3
6
|
# GitHub contribution graph dimensions
|
|
4
7
|
NUM_WEEKS = 52 # Number of weeks in contribution graph
|
|
5
8
|
NUM_DAYS = 7 # Number of days in a week (Sun-Sat)
|
|
6
9
|
SHIP_POSITION_Y = NUM_DAYS + 3 # Ship is positioned just below the grid
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
# Speeds in cells per second (frame-rate independent)
|
|
12
|
+
SHIP_SPEED = 12.5 # Cells per second the ship moves
|
|
13
|
+
BULLET_SPEED = 7.5 # Cells per second the bullet moves
|
|
10
14
|
BULLET_TRAILING_LENGTH = 3 # Number of trailing segments for bullets
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
BULLET_TRAIL_SPACING = 0.15 # Spacing between trail segments in cells
|
|
16
|
+
|
|
17
|
+
# Durations in seconds (frame-rate independent)
|
|
18
|
+
SHIP_SHOOT_COOLDOWN = 0.2 # Seconds between ship shots
|
|
13
19
|
|
|
20
|
+
# Explosion settings
|
|
14
21
|
EXPLOSION_PARTICLE_COUNT_LARGE = 8 # Number of particles in a large explosion
|
|
15
22
|
EXPLOSION_PARTICLE_COUNT_SMALL = 4 # Number of particles in a small explosion
|
|
16
23
|
EXPLOSION_MAX_RADIUS_LARGE = 20 # Max radius for large explosions
|
|
17
24
|
EXPLOSION_MAX_RADIUS_SMALL = 10 # Max radius for small explosions
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
EXPLOSION_DURATION_LARGE = 0.4 # Seconds for large explosion animation
|
|
26
|
+
EXPLOSION_DURATION_SMALL = 0.12 # Seconds for small explosion animation
|
|
27
|
+
|
|
28
|
+
# Starfield settings (speeds in cells per second)
|
|
29
|
+
STAR_SPEED_MIN = 1.0 # Minimum star speed (dimmer/farther stars)
|
|
30
|
+
STAR_SPEED_MAX = 2.5 # Maximum star speed (brighter/closer stars)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Animator for generating GIF animations from game strategies."""
|
|
2
2
|
|
|
3
|
+
from io import BytesIO
|
|
3
4
|
from typing import Iterator
|
|
4
5
|
|
|
5
6
|
from PIL import Image
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
from ..constants import FRAME_DURATION_MS
|
|
9
8
|
from ..github_client import ContributionData
|
|
10
9
|
from .game_state import GameState
|
|
11
10
|
from .renderer import Renderer
|
|
@@ -20,7 +19,8 @@ class Animator:
|
|
|
20
19
|
self,
|
|
21
20
|
contribution_data: ContributionData,
|
|
22
21
|
strategy: BaseStrategy,
|
|
23
|
-
|
|
22
|
+
fps: int,
|
|
23
|
+
watermark: bool = False,
|
|
24
24
|
):
|
|
25
25
|
"""
|
|
26
26
|
Initialize animator.
|
|
@@ -28,13 +28,19 @@ class Animator:
|
|
|
28
28
|
Args:
|
|
29
29
|
contribution_data: The GitHub contribution data
|
|
30
30
|
strategy: The strategy to use for clearing enemies
|
|
31
|
-
|
|
31
|
+
fps: Frames per second for the animation
|
|
32
|
+
watermark: Whether to add watermark to the GIF
|
|
32
33
|
"""
|
|
33
34
|
self.contribution_data = contribution_data
|
|
34
35
|
self.strategy = strategy
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
self.fps = fps
|
|
37
|
+
self.watermark = watermark
|
|
38
|
+
self.frame_duration = 1000 // fps
|
|
39
|
+
# Delta time in seconds per frame
|
|
40
|
+
# Used to scale all speeds (cells/second) to per-frame movement
|
|
41
|
+
self.delta_time = 1.0 / fps
|
|
42
|
+
|
|
43
|
+
def generate_gif(self, maxFrame: int | None) -> BytesIO:
|
|
38
44
|
"""
|
|
39
45
|
Generate animated GIF and save to file.
|
|
40
46
|
|
|
@@ -43,20 +49,27 @@ class Animator:
|
|
|
43
49
|
"""
|
|
44
50
|
# Initialize game state
|
|
45
51
|
game_state = GameState(self.contribution_data)
|
|
46
|
-
renderer = Renderer(game_state, RenderContext.darkmode())
|
|
52
|
+
renderer = Renderer(game_state, RenderContext.darkmode(), watermark=self.watermark)
|
|
47
53
|
|
|
48
|
-
frames =
|
|
54
|
+
frames: list[Image.Image] = []
|
|
55
|
+
for frame in self._generate_frames(game_state, renderer):
|
|
56
|
+
frames.append(frame)
|
|
57
|
+
if maxFrame is not None and len(frames) >= maxFrame:
|
|
58
|
+
break
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
gif_buffer = BytesIO()
|
|
51
61
|
if frames:
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
frames[0].save(
|
|
63
|
+
gif_buffer,
|
|
64
|
+
format="gif",
|
|
54
65
|
save_all=True,
|
|
55
|
-
append_images=
|
|
66
|
+
append_images=frames[1:],
|
|
56
67
|
duration=self.frame_duration,
|
|
57
68
|
loop=0, # Loop forever
|
|
58
69
|
optimize=False,
|
|
59
70
|
)
|
|
71
|
+
|
|
72
|
+
return gif_buffer
|
|
60
73
|
|
|
61
74
|
def _generate_frames(
|
|
62
75
|
self, game_state: GameState, renderer: Renderer
|
|
@@ -79,18 +92,18 @@ class Animator:
|
|
|
79
92
|
for action in self.strategy.generate_actions(game_state):
|
|
80
93
|
game_state.ship.move_to(action.x)
|
|
81
94
|
while game_state.can_take_action() is False:
|
|
82
|
-
game_state.animate()
|
|
95
|
+
game_state.animate(self.delta_time)
|
|
83
96
|
yield renderer.render_frame()
|
|
84
97
|
|
|
85
98
|
if action.shoot:
|
|
86
99
|
game_state.shoot()
|
|
87
|
-
game_state.animate()
|
|
100
|
+
game_state.animate(self.delta_time)
|
|
88
101
|
yield renderer.render_frame()
|
|
89
102
|
|
|
90
103
|
force_kill_countdown = 100
|
|
91
104
|
# Add final frames showing completion
|
|
92
105
|
while not game_state.is_complete():
|
|
93
|
-
game_state.animate()
|
|
106
|
+
game_state.animate(self.delta_time)
|
|
94
107
|
yield renderer.render_frame()
|
|
95
108
|
|
|
96
109
|
force_kill_countdown -= 1
|
|
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from PIL import ImageDraw
|
|
6
6
|
|
|
7
|
-
from ...constants import BULLET_SPEED, BULLET_TRAILING_LENGTH, SHIP_POSITION_Y
|
|
7
|
+
from ...constants import BULLET_SPEED, BULLET_TRAILING_LENGTH, BULLET_TRAIL_SPACING, SHIP_POSITION_Y
|
|
8
8
|
from .drawable import Drawable
|
|
9
9
|
from .explosion import Explosion
|
|
10
10
|
|
|
@@ -37,9 +37,13 @@ class Bullet(Drawable):
|
|
|
37
37
|
return enemy
|
|
38
38
|
return None
|
|
39
39
|
|
|
40
|
-
def animate(self) -> None:
|
|
41
|
-
"""Update bullet position, check for collisions, and remove on hit.
|
|
42
|
-
|
|
40
|
+
def animate(self, delta_time: float) -> None:
|
|
41
|
+
"""Update bullet position, check for collisions, and remove on hit.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
delta_time: Time elapsed since last frame in seconds.
|
|
45
|
+
"""
|
|
46
|
+
self.y -= BULLET_SPEED * delta_time
|
|
43
47
|
hit_enemy = self._check_collision()
|
|
44
48
|
if hit_enemy:
|
|
45
49
|
explosion = Explosion(self.x, self.y, "small", self.game_state)
|
|
@@ -53,7 +57,7 @@ class Bullet(Drawable):
|
|
|
53
57
|
"""Draw the bullet with trailing tail effect."""
|
|
54
58
|
|
|
55
59
|
for i in range(BULLET_TRAILING_LENGTH):
|
|
56
|
-
trail_y = self.y + (i + 1) *
|
|
60
|
+
trail_y = self.y + (i + 1) * BULLET_TRAIL_SPACING
|
|
57
61
|
fade_factor = (i + 1) / BULLET_TRAILING_LENGTH / 2
|
|
58
62
|
self._draw_bullet(draw, context, (self.x, trail_y), fade_factor=fade_factor)
|
|
59
63
|
|
|
@@ -13,8 +13,12 @@ class Drawable(ABC):
|
|
|
13
13
|
"""Interface for objects that can be animated and drawn."""
|
|
14
14
|
|
|
15
15
|
@abstractmethod
|
|
16
|
-
def animate(self) -> None:
|
|
17
|
-
"""Update the object's state for the next animation frame.
|
|
16
|
+
def animate(self, delta_time: float) -> None:
|
|
17
|
+
"""Update the object's state for the next animation frame.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
delta_time: Time elapsed since last frame in seconds.
|
|
21
|
+
"""
|
|
18
22
|
pass
|
|
19
23
|
|
|
20
24
|
@abstractmethod
|
|
@@ -42,7 +42,7 @@ class Enemy(Drawable):
|
|
|
42
42
|
self.game_state.explosions.append(explosion)
|
|
43
43
|
self.game_state.enemies.remove(self)
|
|
44
44
|
|
|
45
|
-
def animate(self) -> None:
|
|
45
|
+
def animate(self, delta_time: float) -> None:
|
|
46
46
|
"""Update enemy state for next frame (enemies don't animate currently)."""
|
|
47
47
|
pass
|
|
48
48
|
|
|
@@ -7,8 +7,8 @@ from typing import TYPE_CHECKING, Literal
|
|
|
7
7
|
from PIL import ImageDraw
|
|
8
8
|
|
|
9
9
|
from ...constants import (
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
EXPLOSION_DURATION_LARGE,
|
|
11
|
+
EXPLOSION_DURATION_SMALL,
|
|
12
12
|
EXPLOSION_MAX_RADIUS_LARGE,
|
|
13
13
|
EXPLOSION_MAX_RADIUS_SMALL,
|
|
14
14
|
EXPLOSION_PARTICLE_COUNT_LARGE,
|
|
@@ -37,22 +37,26 @@ class Explosion(Drawable):
|
|
|
37
37
|
self.x = x
|
|
38
38
|
self.y = y
|
|
39
39
|
self.game_state = game_state
|
|
40
|
-
self.
|
|
41
|
-
self.
|
|
40
|
+
self.elapsed_time = 0.0 # Seconds elapsed since explosion started
|
|
41
|
+
self.duration = EXPLOSION_DURATION_SMALL if size == "small" else EXPLOSION_DURATION_LARGE
|
|
42
42
|
self.max_radius = EXPLOSION_MAX_RADIUS_SMALL if size == "small" else EXPLOSION_MAX_RADIUS_LARGE
|
|
43
43
|
self.particle_count = EXPLOSION_PARTICLE_COUNT_SMALL if size == "small" else EXPLOSION_PARTICLE_COUNT_LARGE
|
|
44
44
|
self.particle_angles = [random.uniform(0, 2 * math.pi) for _ in range(self.particle_count)]
|
|
45
45
|
|
|
46
|
-
def animate(self) -> None:
|
|
47
|
-
"""Progress the explosion animation and remove when complete.
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
def animate(self, delta_time: float) -> None:
|
|
47
|
+
"""Progress the explosion animation and remove when complete.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
delta_time: Time elapsed since last frame in seconds.
|
|
51
|
+
"""
|
|
52
|
+
self.elapsed_time += delta_time
|
|
53
|
+
if self.elapsed_time >= self.duration:
|
|
50
54
|
self.game_state.explosions.remove(self)
|
|
51
55
|
|
|
52
56
|
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
53
57
|
"""Draw expanding particle explosion with fade effect."""
|
|
54
58
|
|
|
55
|
-
progress = self.
|
|
59
|
+
progress = self.elapsed_time / self.duration
|
|
56
60
|
fade = 1 - progress # Fade out as animation progresses
|
|
57
61
|
|
|
58
62
|
center_x, center_y = context.get_cell_position(self.x, self.y)
|
|
@@ -19,7 +19,7 @@ class Ship(Drawable):
|
|
|
19
19
|
"""Initialize the ship at starting position."""
|
|
20
20
|
self.x: float = 25 # Start middle of screen
|
|
21
21
|
self.target_x = self.x
|
|
22
|
-
self.shoot_cooldown = 0 #
|
|
22
|
+
self.shoot_cooldown = 0.0 # Seconds until ship can shoot again
|
|
23
23
|
self.game_state = game_state
|
|
24
24
|
|
|
25
25
|
def move_to(self, x: int):
|
|
@@ -37,18 +37,23 @@ class Ship(Drawable):
|
|
|
37
37
|
|
|
38
38
|
def can_shoot(self) -> bool:
|
|
39
39
|
"""Check if ship can shoot (cooldown has finished)."""
|
|
40
|
-
return self.shoot_cooldown
|
|
40
|
+
return self.shoot_cooldown <= 0
|
|
41
41
|
|
|
42
|
-
def animate(self) -> None:
|
|
43
|
-
"""Update ship position, moving toward target at constant speed.
|
|
42
|
+
def animate(self, delta_time: float) -> None:
|
|
43
|
+
"""Update ship position, moving toward target at constant speed.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
delta_time: Time elapsed since last frame in seconds.
|
|
47
|
+
"""
|
|
48
|
+
delta_x = SHIP_SPEED * delta_time
|
|
44
49
|
if self.x < self.target_x:
|
|
45
|
-
self.x = min(self.x +
|
|
50
|
+
self.x = min(self.x + delta_x, self.target_x)
|
|
46
51
|
elif self.x > self.target_x:
|
|
47
|
-
self.x = max(self.x -
|
|
52
|
+
self.x = max(self.x - delta_x, self.target_x)
|
|
48
53
|
|
|
49
|
-
# Decrement shoot cooldown
|
|
54
|
+
# Decrement shoot cooldown (scaled by delta_time)
|
|
50
55
|
if self.shoot_cooldown > 0:
|
|
51
|
-
self.shoot_cooldown -=
|
|
56
|
+
self.shoot_cooldown -= delta_time
|
|
52
57
|
|
|
53
58
|
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
54
59
|
"""Draw a simple Galaga-style ship."""
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
"""Animated starfield background."""
|
|
2
2
|
|
|
3
3
|
import random
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
5
5
|
|
|
6
6
|
from PIL import ImageDraw
|
|
7
7
|
|
|
8
|
-
from ...constants import NUM_WEEKS, SHIP_POSITION_Y
|
|
8
|
+
from ...constants import NUM_WEEKS, SHIP_POSITION_Y, STAR_SPEED_MIN, STAR_SPEED_MAX
|
|
9
9
|
from .drawable import Drawable
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from ..render_context import RenderContext
|
|
13
13
|
|
|
14
|
+
class Star(TypedDict):
|
|
15
|
+
x: float
|
|
16
|
+
y: float
|
|
17
|
+
brightness: float
|
|
18
|
+
size: int
|
|
19
|
+
speed: float
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
class Starfield(Drawable):
|
|
16
23
|
"""Animated starfield background with slowly moving stars."""
|
|
17
24
|
|
|
18
|
-
def __init__(self):
|
|
25
|
+
def __init__(self) -> None:
|
|
19
26
|
"""Initialize the starfield with random stars."""
|
|
20
|
-
self.stars = []
|
|
27
|
+
self.stars: list[Star] = []
|
|
21
28
|
# Generate about 100 stars across the play area
|
|
22
29
|
for _ in range(100):
|
|
23
30
|
# Random position across the entire grid area
|
|
@@ -27,25 +34,34 @@ class Starfield(Drawable):
|
|
|
27
34
|
brightness = random.uniform(0.2, 1.0)
|
|
28
35
|
# Size: 1-2 pixels
|
|
29
36
|
size = random.choice([1, 1, 1, 2]) # More 1-pixel stars
|
|
30
|
-
# Speed: slower for dimmer (farther) stars
|
|
31
|
-
speed =
|
|
32
|
-
self.stars.append(
|
|
37
|
+
# Speed: slower for dimmer (farther) stars (in cells per second)
|
|
38
|
+
speed = STAR_SPEED_MIN + (brightness * (STAR_SPEED_MAX - STAR_SPEED_MIN))
|
|
39
|
+
self.stars.append(
|
|
40
|
+
{"x": x, "y": y, "brightness": brightness, "size": size, "speed": speed}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def animate(self, delta_time: float) -> None:
|
|
44
|
+
"""Move stars downward, wrapping around when they go off screen.
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
Args:
|
|
47
|
+
delta_time: Time elapsed since last frame in seconds.
|
|
48
|
+
"""
|
|
36
49
|
for star in self.stars:
|
|
37
|
-
|
|
38
|
-
star[1] += star[4]
|
|
50
|
+
star["y"] += star["speed"] * delta_time
|
|
39
51
|
|
|
40
52
|
# Wrap around: if star goes below the screen, move it back to the top
|
|
41
|
-
if star[
|
|
42
|
-
star[
|
|
53
|
+
if star["y"] > SHIP_POSITION_Y + 4:
|
|
54
|
+
star["y"] = -2
|
|
43
55
|
# Randomize x position when wrapping for variety
|
|
44
|
-
star[
|
|
56
|
+
star["x"] = random.uniform(-2, NUM_WEEKS + 2)
|
|
45
57
|
|
|
46
58
|
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
47
59
|
"""Draw all stars at their current positions."""
|
|
48
|
-
for
|
|
60
|
+
for star in self.stars:
|
|
61
|
+
star_x = star["x"]
|
|
62
|
+
star_y = star["y"]
|
|
63
|
+
brightness = star["brightness"]
|
|
64
|
+
size = star["size"]
|
|
49
65
|
# Convert grid position to pixel position
|
|
50
66
|
x, y = context.get_cell_position(star_x, star_y)
|
|
51
67
|
|
|
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, List
|
|
|
4
4
|
|
|
5
5
|
from PIL import ImageDraw
|
|
6
6
|
|
|
7
|
-
from ..constants import
|
|
7
|
+
from ..constants import SHIP_SHOOT_COOLDOWN
|
|
8
8
|
from ..github_client import ContributionData
|
|
9
9
|
from .drawables import Bullet, Drawable, Enemy, Explosion, Ship, Starfield
|
|
10
10
|
|
|
@@ -47,7 +47,7 @@ class GameState(Drawable):
|
|
|
47
47
|
"""
|
|
48
48
|
bullet = Bullet(int(self.ship.x), game_state=self)
|
|
49
49
|
self.bullets.append(bullet)
|
|
50
|
-
self.ship.shoot_cooldown =
|
|
50
|
+
self.ship.shoot_cooldown = SHIP_SHOOT_COOLDOWN
|
|
51
51
|
|
|
52
52
|
def is_complete(self) -> bool:
|
|
53
53
|
"""Check if game is complete (all enemies destroyed)."""
|
|
@@ -57,16 +57,20 @@ class GameState(Drawable):
|
|
|
57
57
|
"""Check if ship can take an action (not moving and can shoot)."""
|
|
58
58
|
return not self.ship.is_moving() and self.ship.can_shoot()
|
|
59
59
|
|
|
60
|
-
def animate(self) -> None:
|
|
61
|
-
"""Update all game objects for next frame.
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
def animate(self, delta_time: float) -> None:
|
|
61
|
+
"""Update all game objects for next frame.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
delta_time: Time elapsed since last frame in seconds.
|
|
65
|
+
"""
|
|
66
|
+
self.starfield.animate(delta_time)
|
|
67
|
+
self.ship.animate(delta_time)
|
|
64
68
|
for enemy in self.enemies:
|
|
65
|
-
enemy.animate()
|
|
69
|
+
enemy.animate(delta_time)
|
|
66
70
|
for bullet in self.bullets:
|
|
67
|
-
bullet.animate()
|
|
71
|
+
bullet.animate(delta_time)
|
|
68
72
|
for explosion in self.explosions:
|
|
69
|
-
explosion.animate()
|
|
73
|
+
explosion.animate(delta_time)
|
|
70
74
|
|
|
71
75
|
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
72
76
|
"""Draw all game objects including the grid."""
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
"""Renderer for drawing game frames using Pillow."""
|
|
2
2
|
|
|
3
|
-
from PIL import Image, ImageDraw
|
|
3
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
4
4
|
|
|
5
5
|
from ..constants import NUM_WEEKS, SHIP_POSITION_Y
|
|
6
6
|
from .game_state import GameState
|
|
7
7
|
from .render_context import RenderContext
|
|
8
8
|
|
|
9
|
+
WATERMARK_TEXT = "by czl9707/gh-space-shooter"
|
|
10
|
+
|
|
11
|
+
|
|
9
12
|
class Renderer:
|
|
10
13
|
"""Renders game state as PIL Images."""
|
|
11
|
-
def __init__(self, game_state: GameState, render_context: RenderContext):
|
|
14
|
+
def __init__(self, game_state: GameState, render_context: RenderContext, watermark: bool = False):
|
|
12
15
|
"""
|
|
13
16
|
Initialize renderer.
|
|
14
17
|
|
|
15
18
|
Args:
|
|
16
19
|
game_state: The game state to render
|
|
17
20
|
render_context: Rendering configuration and theming
|
|
21
|
+
watermark: Whether to add watermark to frames
|
|
18
22
|
"""
|
|
19
23
|
self.game_state = game_state
|
|
20
|
-
|
|
21
24
|
self.context = render_context
|
|
25
|
+
self.watermark = watermark
|
|
22
26
|
|
|
23
27
|
self.grid_width = NUM_WEEKS * (self.context.cell_size + self.context.cell_spacing)
|
|
24
28
|
self.grid_height = SHIP_POSITION_Y * (self.context.cell_size + self.context.cell_spacing)
|
|
@@ -34,11 +38,33 @@ class Renderer:
|
|
|
34
38
|
"""
|
|
35
39
|
# Create image with background color
|
|
36
40
|
img = Image.new("RGB", (self.width, self.height), self.context.background_color)
|
|
37
|
-
|
|
41
|
+
|
|
38
42
|
# Draw game state
|
|
39
43
|
overlay = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0))
|
|
40
44
|
draw = ImageDraw.Draw(overlay, "RGBA")
|
|
41
45
|
self.game_state.draw(draw, self.context)
|
|
46
|
+
|
|
47
|
+
# Draw watermark if enabled
|
|
48
|
+
if self.watermark:
|
|
49
|
+
self._draw_watermark(draw)
|
|
50
|
+
|
|
42
51
|
combined = Image.alpha_composite(img.convert("RGBA"), overlay)
|
|
43
|
-
|
|
52
|
+
|
|
44
53
|
return combined.convert("RGB").convert("P", palette=Image.Palette.ADAPTIVE)
|
|
54
|
+
|
|
55
|
+
def _draw_watermark(self, draw: ImageDraw.ImageDraw) -> None:
|
|
56
|
+
"""Draw watermark text in the bottom-right corner."""
|
|
57
|
+
font = ImageFont.load_default()
|
|
58
|
+
color = (100, 100, 100, 128) # Semi-transparent gray
|
|
59
|
+
margin = 5
|
|
60
|
+
|
|
61
|
+
# Get text bounding box
|
|
62
|
+
bbox = draw.textbbox((0, 0), WATERMARK_TEXT, font=font)
|
|
63
|
+
text_width = bbox[2] - bbox[0]
|
|
64
|
+
text_height = bbox[3] - bbox[1]
|
|
65
|
+
|
|
66
|
+
# Position in bottom-right corner
|
|
67
|
+
x = self.width - text_width - margin
|
|
68
|
+
y = self.height - text_height - margin
|
|
69
|
+
|
|
70
|
+
draw.text((x, y), WATERMARK_TEXT, font=font, fill=color)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gh-space-shooter
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: A CLI tool that visualizes GitHub contribution graphs as gamified GIFs
|
|
5
5
|
Author-email: zane <czl970721@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -60,6 +60,7 @@ Then display it in your README:
|
|
|
60
60
|
- `username` (optional): Username to generate game for (defaults to repo owner)
|
|
61
61
|
- `output-path` (optional): Where to save the GIF (default: `gh-space-shooter.gif`)
|
|
62
62
|
- `strategy` (optional): Attack pattern - `column`, `row`, or `random` (default: `random`)
|
|
63
|
+
- `fps` (optional): Frames per second for the animation (default: `40`)
|
|
63
64
|
- `commit-message` (optional): Commit message for the update
|
|
64
65
|
|
|
65
66
|
### From PyPI
|
|
@@ -121,9 +122,15 @@ gh-space-shooter torvalds --output my-epic-game.gif
|
|
|
121
122
|
gh-space-shooter torvalds -o my-game.gif
|
|
122
123
|
|
|
123
124
|
# Choose enemy attack strategy
|
|
124
|
-
gh-space-shooter torvalds --strategy column # Enemies attack in columns
|
|
125
125
|
gh-space-shooter torvalds --strategy row # Enemies attack in rows
|
|
126
126
|
gh-space-shooter torvalds -s random # Random chaos (default)
|
|
127
|
+
|
|
128
|
+
# Adjust animation frame rate
|
|
129
|
+
gh-space-shooter torvalds --fps 25 # Lower Frame rate, Smaller file size
|
|
130
|
+
gh-space-shooter torvalds --fps 40 # Default Frame rate, Larger file size
|
|
131
|
+
|
|
132
|
+
# Stop the animation earlier
|
|
133
|
+
gh-space-shooter torvalds --max-frame 200 # Stop after 200 frames
|
|
127
134
|
```
|
|
128
135
|
|
|
129
136
|
This creates an animated GIF showing:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
gh_space_shooter/__init__.py,sha256=jBFfHY3YC8-3m-eVPIo3CWoQLsy5Yvd0vcbHVbUDTWY,337
|
|
2
|
+
gh_space_shooter/cli.py,sha256=jmMwTGA_Xq80vBdWOurv6wcksmke6mtrHNBm9LQUpRQ,6340
|
|
3
|
+
gh_space_shooter/console_printer.py,sha256=opT5vITkPJ_9BCPNMDays6EtXez9m86YXY9pK5_Hdh8,2345
|
|
4
|
+
gh_space_shooter/constants.py,sha256=jMEaDAO1xdo9Bu4SN0vcOZfoESl3IAfF8r8OS8CSuwM,1399
|
|
5
|
+
gh_space_shooter/github_client.py,sha256=PkXGWjR7TpzJvCPg37RM4kPq1phg_MlyBpHNtofaNXQ,4697
|
|
6
|
+
gh_space_shooter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
gh_space_shooter/game/__init__.py,sha256=bIc-S0hOiYqYBtdmd9_CedqYY07Il6XiV__TqcbtJNA,707
|
|
8
|
+
gh_space_shooter/game/animator.py,sha256=k0BTcWFlKq3fUi9hVn2f0btMakbgn8CZEMGvs8un9tg,3540
|
|
9
|
+
gh_space_shooter/game/game_state.py,sha256=s-aQ-XvL4ybeu7q1Dhmd5tngC6fIMVbEBGrDtp6ELME,2949
|
|
10
|
+
gh_space_shooter/game/render_context.py,sha256=TPJw9RmRB0_786W-O7nfB61UoDzDT_SlEVdaSlW750c,1805
|
|
11
|
+
gh_space_shooter/game/renderer.py,sha256=JQLTDlI5GLeXCBQUeQdBwOh3xfGAr6kfmVwbdZvEEdk,2509
|
|
12
|
+
gh_space_shooter/game/drawables/__init__.py,sha256=lzo3O5cxahrYTyOQVrz7rQ3Prkaj8Z_dpzlt0UdurOo,306
|
|
13
|
+
gh_space_shooter/game/drawables/bullet.py,sha256=GTWDD0YqJ6cVlXBVFGNB_TPf0FV_J5Drmw23Fx_VpdE,3100
|
|
14
|
+
gh_space_shooter/game/drawables/drawable.py,sha256=JjPnzpTnaRvKHYiQVCLOiq0zHjR8rOo7jqf6bKu5cKo,847
|
|
15
|
+
gh_space_shooter/game/drawables/enemy.py,sha256=clBEpp4TqVPLD_d6jTiuXipI3BbLJv_ks8eqvDr0vmc,1940
|
|
16
|
+
gh_space_shooter/game/drawables/explosion.py,sha256=XfIbecMwlx8WwFV4HqFA_0hpLc3_eDRektvdvqB0dJo,2944
|
|
17
|
+
gh_space_shooter/game/drawables/ship.py,sha256=_DIX5JbQkcLoGpUGf9M87g_5QB5bMYi1sSLsfhEDad4,3396
|
|
18
|
+
gh_space_shooter/game/drawables/starfield.py,sha256=VEUj7gX-R_GtBklr6JDekGdphsHc-ZkvRcA-m1LFGCs,2980
|
|
19
|
+
gh_space_shooter/game/strategies/__init__.py,sha256=a34i4FlNv-aQ-oEkT1ha7oJDBt7Cd7EFpbMWsYNHpW0,338
|
|
20
|
+
gh_space_shooter/game/strategies/base_strategy.py,sha256=IwPdthCWkgsFdUsMjQgifpSJv1bPRCCuUMF0o00y6pk,1152
|
|
21
|
+
gh_space_shooter/game/strategies/column_strategy.py,sha256=AQHXVTRe5BEjc4QHRL9QLtkkelTzGUGf2531POGtkG0,1728
|
|
22
|
+
gh_space_shooter/game/strategies/random_strategy.py,sha256=l0GKMkGJa_QEvNrN57R_KgWDBafGebP926fZJqpdghc,2215
|
|
23
|
+
gh_space_shooter/game/strategies/row_strategy.py,sha256=8izcioSGrVYGdQ__GrdfAQxnJt1BLhwNdumeuQiDhpg,1426
|
|
24
|
+
gh_space_shooter-0.0.5.dist-info/METADATA,sha256=3gohUvLrz02WEX-2l8muePrupWID-vb1pXcyyJbtPp4,4297
|
|
25
|
+
gh_space_shooter-0.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
gh_space_shooter-0.0.5.dist-info/entry_points.txt,sha256=SmK2ET5vz62eaMC4mhxmLJ1f_H9qSTXOvFOHNo-qwCk,62
|
|
27
|
+
gh_space_shooter-0.0.5.dist-info/licenses/LICENSE,sha256=teCrgzzcmjYCQ-RqXkDmICcHMN1AfaabrjZsW6O3KEk,1075
|
|
28
|
+
gh_space_shooter-0.0.5.dist-info/RECORD,,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
gh_space_shooter/__init__.py,sha256=jBFfHY3YC8-3m-eVPIo3CWoQLsy5Yvd0vcbHVbUDTWY,337
|
|
2
|
-
gh_space_shooter/cli.py,sha256=AHONWh6Kv9Y8D7OWsnOH6kOjN73aSCXF8Ats9jKJwkY,5310
|
|
3
|
-
gh_space_shooter/console_printer.py,sha256=opT5vITkPJ_9BCPNMDays6EtXez9m86YXY9pK5_Hdh8,2345
|
|
4
|
-
gh_space_shooter/constants.py,sha256=zMJn4wtCJf11X0pEJeK2q9zfawifB8O-PH1NEOpB0k0,1004
|
|
5
|
-
gh_space_shooter/github_client.py,sha256=PkXGWjR7TpzJvCPg37RM4kPq1phg_MlyBpHNtofaNXQ,4697
|
|
6
|
-
gh_space_shooter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
gh_space_shooter/game/__init__.py,sha256=bIc-S0hOiYqYBtdmd9_CedqYY07Il6XiV__TqcbtJNA,707
|
|
8
|
-
gh_space_shooter/game/animator.py,sha256=5nCY0xfRl6tifwixUlUlVXCQfDjhsR6HbQBKHHmYHH8,2998
|
|
9
|
-
gh_space_shooter/game/game_state.py,sha256=pvKc-FAapVXuFNb2hCGoOhJI1P8kWXkyCf_lruc-j50,2804
|
|
10
|
-
gh_space_shooter/game/render_context.py,sha256=TPJw9RmRB0_786W-O7nfB61UoDzDT_SlEVdaSlW750c,1805
|
|
11
|
-
gh_space_shooter/game/renderer.py,sha256=J13eUOWHkImtiNWGyvRqORM7ngg1Q7LBUgs4cWFl26g,1617
|
|
12
|
-
gh_space_shooter/game/drawables/__init__.py,sha256=lzo3O5cxahrYTyOQVrz7rQ3Prkaj8Z_dpzlt0UdurOo,306
|
|
13
|
-
gh_space_shooter/game/drawables/bullet.py,sha256=tNhdvMw8HPbxyZuE-xTJpo0UGJvb9H8Hj5rA_nZjSUk,2948
|
|
14
|
-
gh_space_shooter/game/drawables/drawable.py,sha256=xIJWI5m-kZCbvCfgYF4jiqTvPFelViJ1gLilz7adJoM,738
|
|
15
|
-
gh_space_shooter/game/drawables/enemy.py,sha256=RCIE4Vn2tis0iJmJO_JUIxnomSgDSsfFhrcPPWmFLhE,1921
|
|
16
|
-
gh_space_shooter/game/drawables/explosion.py,sha256=XJM7NUnK-lkD-AOw8AusUD2erfojyW3rKAi-vqY8eEw,2767
|
|
17
|
-
gh_space_shooter/game/drawables/ship.py,sha256=j389lXOUWvPoFZUNvVN-OEjhTtIlEkAVeEYqF7gDBIE,3216
|
|
18
|
-
gh_space_shooter/game/drawables/starfield.py,sha256=ON_cQ6Qfg30ocLHQEp1dceqputHmxJ7sf61o4uNF4Ng,2530
|
|
19
|
-
gh_space_shooter/game/strategies/__init__.py,sha256=a34i4FlNv-aQ-oEkT1ha7oJDBt7Cd7EFpbMWsYNHpW0,338
|
|
20
|
-
gh_space_shooter/game/strategies/base_strategy.py,sha256=IwPdthCWkgsFdUsMjQgifpSJv1bPRCCuUMF0o00y6pk,1152
|
|
21
|
-
gh_space_shooter/game/strategies/column_strategy.py,sha256=AQHXVTRe5BEjc4QHRL9QLtkkelTzGUGf2531POGtkG0,1728
|
|
22
|
-
gh_space_shooter/game/strategies/random_strategy.py,sha256=l0GKMkGJa_QEvNrN57R_KgWDBafGebP926fZJqpdghc,2215
|
|
23
|
-
gh_space_shooter/game/strategies/row_strategy.py,sha256=8izcioSGrVYGdQ__GrdfAQxnJt1BLhwNdumeuQiDhpg,1426
|
|
24
|
-
gh_space_shooter-0.0.3.dist-info/METADATA,sha256=oSw8RS0yzv1ZX9oLO3vs_MWgpD38yn8fdeSxG3FxADQ,3999
|
|
25
|
-
gh_space_shooter-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
-
gh_space_shooter-0.0.3.dist-info/entry_points.txt,sha256=SmK2ET5vz62eaMC4mhxmLJ1f_H9qSTXOvFOHNo-qwCk,62
|
|
27
|
-
gh_space_shooter-0.0.3.dist-info/licenses/LICENSE,sha256=teCrgzzcmjYCQ-RqXkDmICcHMN1AfaabrjZsW6O3KEk,1075
|
|
28
|
-
gh_space_shooter-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|