gh-space-shooter 0.0.4__tar.gz → 0.0.5__tar.gz

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.
Files changed (39) hide show
  1. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/.github/workflows/publish.yml +5 -4
  2. gh_space_shooter-0.0.5/CLAUDE.md +87 -0
  3. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/PKG-INFO +6 -5
  4. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/README.md +5 -4
  5. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/pyproject.toml +2 -2
  6. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/cli.py +25 -4
  7. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/animator.py +18 -8
  8. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/renderer.py +31 -5
  9. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/uv.lock +1 -1
  10. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/.github/dependabot.yml +0 -0
  11. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/.github/workflows/test.yml +0 -0
  12. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/.gitignore +0 -0
  13. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/.python-version +0 -0
  14. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/LICENSE +0 -0
  15. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/action.yml +0 -0
  16. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/example.gif +0 -0
  17. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/__init__.py +0 -0
  18. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/console_printer.py +0 -0
  19. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/constants.py +0 -0
  20. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/__init__.py +0 -0
  21. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/__init__.py +0 -0
  22. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/bullet.py +0 -0
  23. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/drawable.py +0 -0
  24. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/enemy.py +0 -0
  25. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/explosion.py +0 -0
  26. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/ship.py +0 -0
  27. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/drawables/starfield.py +0 -0
  28. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/game_state.py +0 -0
  29. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/render_context.py +0 -0
  30. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/strategies/__init__.py +0 -0
  31. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/strategies/base_strategy.py +0 -0
  32. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/strategies/column_strategy.py +0 -0
  33. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/strategies/random_strategy.py +0 -0
  34. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/game/strategies/row_strategy.py +0 -0
  35. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/github_client.py +0 -0
  36. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/src/gh_space_shooter/py.typed +0 -0
  37. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/tests/conftest.py +0 -0
  38. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/tests/test_bullet_collision.py +0 -0
  39. {gh_space_shooter-0.0.4 → gh_space_shooter-0.0.5}/tests/test_strategies.py +0 -0
@@ -45,11 +45,12 @@ jobs:
45
45
  - name: Publish
46
46
  run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
47
47
 
48
- - name: GitHub Release v${{ steps.bump.outputs.current-version }}
48
+ - name: GitHub Release PIP v${{ steps.bump.outputs.current-version }}
49
49
  uses: ncipollo/release-action@v1
50
50
  with:
51
- name: gh-space-shooter v${{ steps.bump.outputs.current-version }}
51
+ name: gh-space-shooter pypi v${{ steps.bump.outputs.current-version }}
52
52
  tag: v${{ steps.bump.outputs.current-version }}
53
- body: "Automated release of version v${{ steps.bump.outputs.current-version }}"
53
+ body: "Automated release of pypi version v${{ steps.bump.outputs.current-version }}"
54
54
  artifacts: dist/*
55
- token: ${{ secrets.GH_TOKEN }}
55
+ token: ${{ secrets.GH_TOKEN }}
56
+ generateReleaseNotes: true
@@ -0,0 +1,87 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build and Development Commands
6
+
7
+ ```bash
8
+ # Install dependencies (uses uv package manager)
9
+ uv sync
10
+
11
+ # Install with dev dependencies
12
+ uv sync --extra dev
13
+
14
+ # Run the CLI
15
+ uv run gh-space-shooter <username>
16
+
17
+ # Run tests
18
+ uv run pytest tests/ -v
19
+
20
+ # Run a single test file
21
+ uv run pytest tests/test_strategies.py -v
22
+
23
+ # Run a specific test
24
+ uv run pytest tests/test_strategies.py::test_column_strategy -v
25
+ ```
26
+
27
+ ## Environment Setup
28
+
29
+ Requires a GitHub Personal Access Token with `read:user` scope:
30
+ ```bash
31
+ export GH_TOKEN=your_token_here
32
+ # Or create a .env file with GH_TOKEN=your_token
33
+ ```
34
+
35
+ ## Project Context
36
+
37
+ **Current main usage**: GitHub Action that automatically updates a game GIF in user repositories daily (see `.github/workflows/` for the action definition).
38
+
39
+ **Planned**: Wrap this into a Python webapp for on-demand GIF generation.
40
+
41
+ ## Architecture Overview
42
+
43
+ This is a CLI tool that transforms GitHub contribution graphs into animated space shooter GIFs using Pillow.
44
+
45
+ ### Core Flow
46
+
47
+ 1. **CLI (`cli.py`)** - Typer-based entry point that orchestrates the pipeline
48
+ 2. **GitHubClient (`github_client.py`)** - Fetches contribution data via GitHub GraphQL API, returns typed `ContributionData` dict
49
+ 3. **Animator (`game/animator.py`)** - Main game loop that coordinates strategy execution and frame generation
50
+ 4. **GameState (`game/game_state.py`)** - Central state container holding ship, enemies, bullets, explosions
51
+ 5. **Renderer (`game/renderer.py`)** - Converts GameState to PIL Images each frame
52
+
53
+ ### Strategy Pattern
54
+
55
+ Strategies (`game/strategies/`) define how the ship clears enemies:
56
+ - `BaseStrategy` - Abstract base defining `generate_actions(game_state) -> Iterator[Action]`
57
+ - `ColumnStrategy` - Clears enemies column by column (left to right)
58
+ - `RowStrategy` - Clears enemies row by row (top to bottom)
59
+ - `RandomStrategy` - Targets enemies in random order
60
+
61
+ Strategies yield `Action(x, shoot)` objects. The Animator processes these: moving the ship to position `x`, waiting for movement/cooldown to complete, then shooting if `shoot=True`.
62
+
63
+ ### Drawable System
64
+
65
+ All game objects inherit from `Drawable` (`game/drawables/drawable.py`):
66
+ - `animate(delta_time)` - Update state (position, cooldowns, particles)
67
+ - `draw(draw, context)` - Render to PIL ImageDraw
68
+
69
+ Drawables: `Ship`, `Enemy`, `Bullet`, `Explosion`, `Starfield`
70
+
71
+ The `RenderContext` (`game/render_context.py`) holds theming (colors, cell sizes, padding) and coordinate conversion helpers.
72
+
73
+ ### Animation Loop
74
+
75
+ In `Animator._generate_frames()`:
76
+ 1. Strategy yields next action
77
+ 2. Ship moves to target x position (animate frames until arrived)
78
+ 3. Ship shoots if action.shoot (animate frames for bullet travel + explosions)
79
+ 4. Repeat until all enemies destroyed
80
+
81
+ Frame rate is configurable (default 40 FPS). All speeds use delta_time for frame-rate independence.
82
+
83
+ ### Key Constants (`constants.py`)
84
+
85
+ - `NUM_WEEKS = 52` - Contribution graph width
86
+ - `NUM_DAYS = 7` - Contribution graph height
87
+ - Speeds are in cells/second, durations in seconds
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gh-space-shooter
3
- Version: 0.0.4
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
@@ -122,14 +122,15 @@ gh-space-shooter torvalds --output my-epic-game.gif
122
122
  gh-space-shooter torvalds -o my-game.gif
123
123
 
124
124
  # Choose enemy attack strategy
125
- gh-space-shooter torvalds --strategy column # Enemies attack in columns
126
125
  gh-space-shooter torvalds --strategy row # Enemies attack in rows
127
126
  gh-space-shooter torvalds -s random # Random chaos (default)
128
127
 
129
128
  # 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
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
133
134
  ```
134
135
 
135
136
  This creates an animated GIF showing:
@@ -106,14 +106,15 @@ gh-space-shooter torvalds --output my-epic-game.gif
106
106
  gh-space-shooter torvalds -o my-game.gif
107
107
 
108
108
  # Choose enemy attack strategy
109
- gh-space-shooter torvalds --strategy column # Enemies attack in columns
110
109
  gh-space-shooter torvalds --strategy row # Enemies attack in rows
111
110
  gh-space-shooter torvalds -s random # Random chaos (default)
112
111
 
113
112
  # Adjust animation frame rate
114
- gh-space-shooter torvalds --fps 25 # Slower, smaller file size
115
- gh-space-shooter torvalds --fps 40 # Default frame rate
116
- gh-space-shooter torvalds --fps 50 # Smoother animation
113
+ gh-space-shooter torvalds --fps 25 # Lower Frame rate, Smaller file size
114
+ gh-space-shooter torvalds --fps 40 # Default Frame rate, Larger file size
115
+
116
+ # Stop the animation earlier
117
+ gh-space-shooter torvalds --max-frame 200 # Stop after 200 frames
117
118
  ```
118
119
 
119
120
  This creates an animated GIF showing:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gh-space-shooter"
3
- version = "0.0.4"
3
+ version = "0.0.5"
4
4
  description = "A CLI tool that visualizes GitHub contribution graphs as gamified GIFs"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -29,7 +29,7 @@ build-backend = "hatchling.build"
29
29
 
30
30
 
31
31
  [tool.bumpversion]
32
- current_version = "0.0.4"
32
+ current_version = "0.0.5"
33
33
  parse = """(?x)
34
34
  (?P<major>0|[1-9]\\d*)\\.
35
35
  (?P<minor>0|[1-9]\\d*)\\.
@@ -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)
@@ -46,7 +46,7 @@ wheels = [
46
46
 
47
47
  [[package]]
48
48
  name = "gh-space-shooter"
49
- version = "0.0.3"
49
+ version = "0.0.4"
50
50
  source = { editable = "." }
51
51
  dependencies = [
52
52
  { name = "httpx" },