gh-space-shooter 0.0.4__py3-none-any.whl → 1.0.0__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 +25 -4
- gh_space_shooter/game/animator.py +18 -8
- gh_space_shooter/game/renderer.py +31 -5
- gh_space_shooter/github_client.py +1 -1
- {gh_space_shooter-0.0.4.dist-info → gh_space_shooter-1.0.0.dist-info}/METADATA +11 -6
- {gh_space_shooter-0.0.4.dist-info → gh_space_shooter-1.0.0.dist-info}/RECORD +9 -9
- {gh_space_shooter-0.0.4.dist-info → gh_space_shooter-1.0.0.dist-info}/WHEEL +0 -0
- {gh_space_shooter-0.0.4.dist-info → gh_space_shooter-1.0.0.dist-info}/entry_points.txt +0 -0
- {gh_space_shooter-0.0.4.dist-info → gh_space_shooter-1.0.0.dist-info}/licenses/LICENSE +0 -0
gh_space_shooter/cli.py
CHANGED
|
@@ -60,6 +60,16 @@ def main(
|
|
|
60
60
|
"--fps",
|
|
61
61
|
help="Frames per second for the animation",
|
|
62
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
|
+
),
|
|
63
73
|
) -> None:
|
|
64
74
|
"""
|
|
65
75
|
Fetch or load GitHub contribution graph data and display it.
|
|
@@ -95,7 +105,7 @@ def main(
|
|
|
95
105
|
_save_data_to_file(data, raw_output)
|
|
96
106
|
|
|
97
107
|
# Generate GIF if requested
|
|
98
|
-
_generate_gif(data, out, strategy, fps)
|
|
108
|
+
_generate_gif(data, out, strategy, fps, watermark, maxFrame)
|
|
99
109
|
|
|
100
110
|
except CLIError as e:
|
|
101
111
|
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
@@ -151,7 +161,14 @@ def _save_data_to_file(data: ContributionData, file_path: str) -> None:
|
|
|
151
161
|
raise CLIError(f"Failed to save file '{file_path}': {e}")
|
|
152
162
|
|
|
153
163
|
|
|
154
|
-
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:
|
|
155
172
|
"""Generate animated GIF visualization."""
|
|
156
173
|
# GIF format limitation: delays below 20ms (>50 FPS) are clamped by most browsers
|
|
157
174
|
if fps > 50:
|
|
@@ -174,8 +191,12 @@ def _generate_gif(data: ContributionData, file_path: str, strategy_name: str, fp
|
|
|
174
191
|
|
|
175
192
|
# Create animator and generate GIF
|
|
176
193
|
try:
|
|
177
|
-
animator = Animator(data, strategy, fps=fps)
|
|
178
|
-
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
|
+
|
|
179
200
|
console.print(f"[green]✓[/green] GIF saved to {file_path}")
|
|
180
201
|
except Exception as e:
|
|
181
202
|
raise CLIError(f"Failed to generate GIF: {e}")
|
|
@@ -1,10 +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
8
|
from ..github_client import ContributionData
|
|
9
9
|
from .game_state import GameState
|
|
10
10
|
from .renderer import Renderer
|
|
@@ -20,6 +20,7 @@ class Animator:
|
|
|
20
20
|
contribution_data: ContributionData,
|
|
21
21
|
strategy: BaseStrategy,
|
|
22
22
|
fps: int,
|
|
23
|
+
watermark: bool = False,
|
|
23
24
|
):
|
|
24
25
|
"""
|
|
25
26
|
Initialize animator.
|
|
@@ -28,16 +29,18 @@ class Animator:
|
|
|
28
29
|
contribution_data: The GitHub contribution data
|
|
29
30
|
strategy: The strategy to use for clearing enemies
|
|
30
31
|
fps: Frames per second for the animation
|
|
32
|
+
watermark: Whether to add watermark to the GIF
|
|
31
33
|
"""
|
|
32
34
|
self.contribution_data = contribution_data
|
|
33
35
|
self.strategy = strategy
|
|
34
36
|
self.fps = fps
|
|
37
|
+
self.watermark = watermark
|
|
35
38
|
self.frame_duration = 1000 // fps
|
|
36
39
|
# Delta time in seconds per frame
|
|
37
40
|
# Used to scale all speeds (cells/second) to per-frame movement
|
|
38
41
|
self.delta_time = 1.0 / fps
|
|
39
42
|
|
|
40
|
-
def generate_gif(self,
|
|
43
|
+
def generate_gif(self, maxFrame: int | None) -> BytesIO:
|
|
41
44
|
"""
|
|
42
45
|
Generate animated GIF and save to file.
|
|
43
46
|
|
|
@@ -46,20 +49,27 @@ class Animator:
|
|
|
46
49
|
"""
|
|
47
50
|
# Initialize game state
|
|
48
51
|
game_state = GameState(self.contribution_data)
|
|
49
|
-
renderer = Renderer(game_state, RenderContext.darkmode())
|
|
52
|
+
renderer = Renderer(game_state, RenderContext.darkmode(), watermark=self.watermark)
|
|
50
53
|
|
|
51
|
-
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
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
gif_buffer = BytesIO()
|
|
54
61
|
if frames:
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
frames[0].save(
|
|
63
|
+
gif_buffer,
|
|
64
|
+
format="gif",
|
|
57
65
|
save_all=True,
|
|
58
|
-
append_images=
|
|
66
|
+
append_images=frames[1:],
|
|
59
67
|
duration=self.frame_duration,
|
|
60
68
|
loop=0, # Loop forever
|
|
61
69
|
optimize=False,
|
|
62
70
|
)
|
|
71
|
+
|
|
72
|
+
return gif_buffer
|
|
63
73
|
|
|
64
74
|
def _generate_frames(
|
|
65
75
|
self, game_state: GameState, renderer: Renderer
|
|
@@ -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)
|
|
@@ -122,7 +122,7 @@ class GitHubClient:
|
|
|
122
122
|
if "errors" in data:
|
|
123
123
|
errors = data["errors"]
|
|
124
124
|
error_messages = [error.get("message", str(error)) for error in errors]
|
|
125
|
-
raise GitHubAPIError(
|
|
125
|
+
raise GitHubAPIError(', '.join(error_messages))
|
|
126
126
|
|
|
127
127
|
# Check if user exists
|
|
128
128
|
if not data.get("data", {}).get("user"):
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gh-space-shooter
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 1.0.0
|
|
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
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: pillow>=10.1.0
|
|
10
10
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -22,6 +22,10 @@ Transform your GitHub contribution graph into an epic space shooter game!
|
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
+
### Onetime Generation
|
|
26
|
+
|
|
27
|
+
A [web interface](https://gh-space-shooter.kiyo-n-zane.com) is available for on-demand GIF generation without installing anything locally.
|
|
28
|
+
|
|
25
29
|
### GitHub Action
|
|
26
30
|
|
|
27
31
|
Automatically update your game GIF daily using GitHub Actions! Add this workflow to your repository at `.github/workflows/update-game.yml`:
|
|
@@ -122,14 +126,15 @@ gh-space-shooter torvalds --output my-epic-game.gif
|
|
|
122
126
|
gh-space-shooter torvalds -o my-game.gif
|
|
123
127
|
|
|
124
128
|
# Choose enemy attack strategy
|
|
125
|
-
gh-space-shooter torvalds --strategy column # Enemies attack in columns
|
|
126
129
|
gh-space-shooter torvalds --strategy row # Enemies attack in rows
|
|
127
130
|
gh-space-shooter torvalds -s random # Random chaos (default)
|
|
128
131
|
|
|
129
132
|
# Adjust animation frame rate
|
|
130
|
-
gh-space-shooter torvalds --fps 25 #
|
|
131
|
-
gh-space-shooter torvalds --fps 40 # Default
|
|
132
|
-
|
|
133
|
+
gh-space-shooter torvalds --fps 25 # Lower Frame rate, Smaller file size
|
|
134
|
+
gh-space-shooter torvalds --fps 40 # Default Frame rate, Larger file size
|
|
135
|
+
|
|
136
|
+
# Stop the animation earlier
|
|
137
|
+
gh-space-shooter torvalds --max-frame 200 # Stop after 200 frames
|
|
133
138
|
```
|
|
134
139
|
|
|
135
140
|
This creates an animated GIF showing:
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
gh_space_shooter/__init__.py,sha256=jBFfHY3YC8-3m-eVPIo3CWoQLsy5Yvd0vcbHVbUDTWY,337
|
|
2
|
-
gh_space_shooter/cli.py,sha256=
|
|
2
|
+
gh_space_shooter/cli.py,sha256=jmMwTGA_Xq80vBdWOurv6wcksmke6mtrHNBm9LQUpRQ,6340
|
|
3
3
|
gh_space_shooter/console_printer.py,sha256=opT5vITkPJ_9BCPNMDays6EtXez9m86YXY9pK5_Hdh8,2345
|
|
4
4
|
gh_space_shooter/constants.py,sha256=jMEaDAO1xdo9Bu4SN0vcOZfoESl3IAfF8r8OS8CSuwM,1399
|
|
5
|
-
gh_space_shooter/github_client.py,sha256=
|
|
5
|
+
gh_space_shooter/github_client.py,sha256=tR2yeDCvq2i5XE1_QvVXcd1S9qzo2AuK8HHJ9gfK6Mw,4676
|
|
6
6
|
gh_space_shooter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
gh_space_shooter/game/__init__.py,sha256=bIc-S0hOiYqYBtdmd9_CedqYY07Il6XiV__TqcbtJNA,707
|
|
8
|
-
gh_space_shooter/game/animator.py,sha256=
|
|
8
|
+
gh_space_shooter/game/animator.py,sha256=k0BTcWFlKq3fUi9hVn2f0btMakbgn8CZEMGvs8un9tg,3540
|
|
9
9
|
gh_space_shooter/game/game_state.py,sha256=s-aQ-XvL4ybeu7q1Dhmd5tngC6fIMVbEBGrDtp6ELME,2949
|
|
10
10
|
gh_space_shooter/game/render_context.py,sha256=TPJw9RmRB0_786W-O7nfB61UoDzDT_SlEVdaSlW750c,1805
|
|
11
|
-
gh_space_shooter/game/renderer.py,sha256=
|
|
11
|
+
gh_space_shooter/game/renderer.py,sha256=JQLTDlI5GLeXCBQUeQdBwOh3xfGAr6kfmVwbdZvEEdk,2509
|
|
12
12
|
gh_space_shooter/game/drawables/__init__.py,sha256=lzo3O5cxahrYTyOQVrz7rQ3Prkaj8Z_dpzlt0UdurOo,306
|
|
13
13
|
gh_space_shooter/game/drawables/bullet.py,sha256=GTWDD0YqJ6cVlXBVFGNB_TPf0FV_J5Drmw23Fx_VpdE,3100
|
|
14
14
|
gh_space_shooter/game/drawables/drawable.py,sha256=JjPnzpTnaRvKHYiQVCLOiq0zHjR8rOo7jqf6bKu5cKo,847
|
|
@@ -21,8 +21,8 @@ gh_space_shooter/game/strategies/base_strategy.py,sha256=IwPdthCWkgsFdUsMjQgifpS
|
|
|
21
21
|
gh_space_shooter/game/strategies/column_strategy.py,sha256=AQHXVTRe5BEjc4QHRL9QLtkkelTzGUGf2531POGtkG0,1728
|
|
22
22
|
gh_space_shooter/game/strategies/random_strategy.py,sha256=l0GKMkGJa_QEvNrN57R_KgWDBafGebP926fZJqpdghc,2215
|
|
23
23
|
gh_space_shooter/game/strategies/row_strategy.py,sha256=8izcioSGrVYGdQ__GrdfAQxnJt1BLhwNdumeuQiDhpg,1426
|
|
24
|
-
gh_space_shooter-0.0.
|
|
25
|
-
gh_space_shooter-0.0.
|
|
26
|
-
gh_space_shooter-0.0.
|
|
27
|
-
gh_space_shooter-0.0.
|
|
28
|
-
gh_space_shooter-0.0.
|
|
24
|
+
gh_space_shooter-1.0.0.dist-info/METADATA,sha256=6OKZ5hO1uLH3hkzz0aK9l0Fc-6b4ffDSlZYoqIzije4,4462
|
|
25
|
+
gh_space_shooter-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
gh_space_shooter-1.0.0.dist-info/entry_points.txt,sha256=SmK2ET5vz62eaMC4mhxmLJ1f_H9qSTXOvFOHNo-qwCk,62
|
|
27
|
+
gh_space_shooter-1.0.0.dist-info/licenses/LICENSE,sha256=teCrgzzcmjYCQ-RqXkDmICcHMN1AfaabrjZsW6O3KEk,1075
|
|
28
|
+
gh_space_shooter-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|