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 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(data: ContributionData, file_path: str, strategy_name: str, fps: int) -> None:
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(file_path)
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, output_path: str) -> None:
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 = self._generate_frames(game_state, renderer)
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
- # Save as GIF
60
+ gif_buffer = BytesIO()
54
61
  if frames:
55
- next(frames).save(
56
- output_path,
62
+ frames[0].save(
63
+ gif_buffer,
64
+ format="gif",
57
65
  save_all=True,
58
- append_images=list(frames),
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(f"GraphQL errors: {', '.join(error_messages)}")
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.4
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.13
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 # Slower, smaller file size
131
- gh-space-shooter torvalds --fps 40 # Default frame rate
132
- gh-space-shooter torvalds --fps 50 # Smoother animation
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=mCo7PH0XGCeGG9V6lCPlZMCmDIapeJVCfJenQgEW9o8,5798
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=PkXGWjR7TpzJvCPg37RM4kPq1phg_MlyBpHNtofaNXQ,4697
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=G9PGcIQkdPgxByIKC1vlFnVHNdvTNefinXYj1_TEIMo,3126
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=J13eUOWHkImtiNWGyvRqORM7ngg1Q7LBUgs4cWFl26g,1617
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.4.dist-info/METADATA,sha256=DcUrmwjeM1VUOkm1th-R164K3FUYrxYIaj4wwuD8r-0,4310
25
- gh_space_shooter-0.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
- gh_space_shooter-0.0.4.dist-info/entry_points.txt,sha256=SmK2ET5vz62eaMC4mhxmLJ1f_H9qSTXOvFOHNo-qwCk,62
27
- gh_space_shooter-0.0.4.dist-info/licenses/LICENSE,sha256=teCrgzzcmjYCQ-RqXkDmICcHMN1AfaabrjZsW6O3KEk,1075
28
- gh_space_shooter-0.0.4.dist-info/RECORD,,
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,,