gh-space-shooter 0.0.2__tar.gz → 0.0.4__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 (41) hide show
  1. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/.github/workflows/publish.yml +1 -1
  2. gh_space_shooter-0.0.4/.github/workflows/test.yml +40 -0
  3. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/.gitignore +3 -0
  4. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/PKG-INFO +51 -14
  5. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/README.md +47 -12
  6. gh_space_shooter-0.0.4/action.yml +71 -0
  7. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/pyproject.toml +13 -4
  8. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/cli.py +16 -5
  9. gh_space_shooter-0.0.4/src/gh_space_shooter/constants.py +30 -0
  10. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/animator.py +10 -7
  11. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/bullet.py +11 -8
  12. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/drawable.py +6 -2
  13. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/enemy.py +1 -1
  14. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/explosion.py +22 -14
  15. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/ship.py +13 -8
  16. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/starfield.py +31 -15
  17. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/game_state.py +13 -9
  18. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/github_client.py +0 -2
  19. gh_space_shooter-0.0.4/tests/conftest.py +27 -0
  20. gh_space_shooter-0.0.4/tests/test_bullet_collision.py +124 -0
  21. gh_space_shooter-0.0.4/tests/test_strategies.py +276 -0
  22. gh_space_shooter-0.0.4/uv.lock +305 -0
  23. gh_space_shooter-0.0.2/czl9707_contributions.json +0 -2015
  24. gh_space_shooter-0.0.2/src/gh_space_shooter/constants.py +0 -11
  25. gh_space_shooter-0.0.2/uv.lock +0 -255
  26. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/.github/dependabot.yml +0 -0
  27. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/.python-version +0 -0
  28. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/LICENSE +0 -0
  29. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/example.gif +0 -0
  30. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/__init__.py +0 -0
  31. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/console_printer.py +0 -0
  32. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/__init__.py +0 -0
  33. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/drawables/__init__.py +0 -0
  34. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/render_context.py +0 -0
  35. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/renderer.py +0 -0
  36. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/strategies/__init__.py +0 -0
  37. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/strategies/base_strategy.py +0 -0
  38. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/strategies/column_strategy.py +0 -0
  39. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/strategies/random_strategy.py +0 -0
  40. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/game/strategies/row_strategy.py +0 -0
  41. {gh_space_shooter-0.0.2 → gh_space_shooter-0.0.4}/src/gh_space_shooter/py.typed +0 -0
@@ -48,7 +48,7 @@ jobs:
48
48
  - name: GitHub Release v${{ steps.bump.outputs.current-version }}
49
49
  uses: ncipollo/release-action@v1
50
50
  with:
51
- name: pytest-asyncio-concurrent v${{ steps.bump.outputs.current-version }}
51
+ name: gh-space-shooter v${{ steps.bump.outputs.current-version }}
52
52
  tag: v${{ steps.bump.outputs.current-version }}
53
53
  body: "Automated release of version v${{ steps.bump.outputs.current-version }}"
54
54
  artifacts: dist/*
@@ -0,0 +1,40 @@
1
+ name: Run Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ types: [opened, synchronize, reopened]
8
+
9
+ env:
10
+ PYTHON_LATEST: 3.13
11
+
12
+ jobs:
13
+ test:
14
+ name: Test Python ${{ matrix.python-version }}
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ matrix:
18
+ python-version: ["3.13"]
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v7
26
+
27
+ - name: Install Python ${{ matrix.python-version }}
28
+ run: uv python install ${{ matrix.python-version }}
29
+
30
+ - name: Install dependencies
31
+ run: uv sync --extra dev
32
+
33
+ - name: Run tests
34
+ run: uv run pytest tests/ -v
35
+
36
+ - name: Test Summary
37
+ if: always()
38
+ run: |
39
+ echo "## Test Results" >> $GITHUB_STEP_SUMMARY
40
+ echo "Tests completed for Python ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY
@@ -17,3 +17,6 @@ wheels/
17
17
  # Environment variables (secrets)
18
18
  .env
19
19
  .env.*
20
+
21
+ # Test output
22
+ my-contributions.json
@@ -1,15 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gh-space-shooter
3
- Version: 0.0.2
3
+ Version: 0.0.4
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
7
  Requires-Python: >=3.13
8
8
  Requires-Dist: httpx>=0.27.0
9
- Requires-Dist: pillow>=10.0.0
9
+ Requires-Dist: pillow>=10.1.0
10
10
  Requires-Dist: python-dotenv>=1.0.0
11
11
  Requires-Dist: rich>=13.0.0
12
12
  Requires-Dist: typer>=0.12.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
13
15
  Description-Content-Type: text/markdown
14
16
 
15
17
  # gh-space-shooter 🚀
@@ -18,19 +20,50 @@ Transform your GitHub contribution graph into an epic space shooter game!
18
20
 
19
21
  ![Example Game](example.gif)
20
22
 
21
- ## Features
23
+ ## Usage
24
+
25
+ ### GitHub Action
26
+
27
+ Automatically update your game GIF daily using GitHub Actions! Add this workflow to your repository at `.github/workflows/update-game.yml`:
22
28
 
23
- - 🚀 **Galaga-style space shooter** - Classic arcade gameplay with your contribution data
24
- - 📊 **GitHub integration** - Fetches your last 52 weeks of contributions automatically
25
- - 🎮 **Smart enemy AI** - Multiple attack strategies (columns, rows, random patterns)
26
- - 💥 **Particle effects** - Explosions with randomized particles and smooth animations
27
- - 🎨 **Polished graphics** - Rounded enemies, smooth ship design, starfield background
28
- - 📈 **Contribution stats** - View your coding activity statistics
29
- - 💾 **Export options** - Save both the GIF and raw JSON data
29
+ ```yaml
30
+ name: Update Space Shooter Game
30
31
 
31
- ## Installation
32
+ on:
33
+ schedule:
34
+ - cron: '0 0 * * *' # Daily at midnight UTC
35
+ workflow_dispatch: # Allow manual trigger
32
36
 
33
- ### From PyPI (Recommended)
37
+ permissions:
38
+ contents: write
39
+
40
+ jobs:
41
+ update-game:
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - uses: czl9707/gh-space-shooter@v1
47
+ with:
48
+ github-token: ${{ secrets.GITHUB_TOKEN }}
49
+ output-path: 'game.gif'
50
+ strategy: 'random'
51
+ ```
52
+
53
+ Then display it in your README:
54
+ ```markdown
55
+ ![My GitHub Game](game.gif)
56
+ ```
57
+
58
+ **Action Inputs:**
59
+ - `github-token` (required): GitHub token for fetching contributions
60
+ - `username` (optional): Username to generate game for (defaults to repo owner)
61
+ - `output-path` (optional): Where to save the GIF (default: `gh-space-shooter.gif`)
62
+ - `strategy` (optional): Attack pattern - `column`, `row`, or `random` (default: `random`)
63
+ - `fps` (optional): Frames per second for the animation (default: `40`)
64
+ - `commit-message` (optional): Commit message for the update
65
+
66
+ ### From PyPI
34
67
 
35
68
  ```bash
36
69
  pip install gh-space-shooter
@@ -92,6 +125,11 @@ gh-space-shooter torvalds -o my-game.gif
92
125
  gh-space-shooter torvalds --strategy column # Enemies attack in columns
93
126
  gh-space-shooter torvalds --strategy row # Enemies attack in rows
94
127
  gh-space-shooter torvalds -s random # Random chaos (default)
128
+
129
+ # 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
95
133
  ```
96
134
 
97
135
  This creates an animated GIF showing:
@@ -131,8 +169,7 @@ When saved to JSON, the data includes:
131
169
  }
132
170
  ]
133
171
  }
134
- ],
135
- "fetched_at": "2024-12-30T12:00:00"
172
+ ]
136
173
  }
137
174
  ```
138
175
 
@@ -4,19 +4,50 @@ Transform your GitHub contribution graph into an epic space shooter game!
4
4
 
5
5
  ![Example Game](example.gif)
6
6
 
7
- ## Features
7
+ ## Usage
8
+
9
+ ### GitHub Action
10
+
11
+ Automatically update your game GIF daily using GitHub Actions! Add this workflow to your repository at `.github/workflows/update-game.yml`:
8
12
 
9
- - 🚀 **Galaga-style space shooter** - Classic arcade gameplay with your contribution data
10
- - 📊 **GitHub integration** - Fetches your last 52 weeks of contributions automatically
11
- - 🎮 **Smart enemy AI** - Multiple attack strategies (columns, rows, random patterns)
12
- - 💥 **Particle effects** - Explosions with randomized particles and smooth animations
13
- - 🎨 **Polished graphics** - Rounded enemies, smooth ship design, starfield background
14
- - 📈 **Contribution stats** - View your coding activity statistics
15
- - 💾 **Export options** - Save both the GIF and raw JSON data
13
+ ```yaml
14
+ name: Update Space Shooter Game
16
15
 
17
- ## Installation
16
+ on:
17
+ schedule:
18
+ - cron: '0 0 * * *' # Daily at midnight UTC
19
+ workflow_dispatch: # Allow manual trigger
18
20
 
19
- ### From PyPI (Recommended)
21
+ permissions:
22
+ contents: write
23
+
24
+ jobs:
25
+ update-game:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - uses: czl9707/gh-space-shooter@v1
31
+ with:
32
+ github-token: ${{ secrets.GITHUB_TOKEN }}
33
+ output-path: 'game.gif'
34
+ strategy: 'random'
35
+ ```
36
+
37
+ Then display it in your README:
38
+ ```markdown
39
+ ![My GitHub Game](game.gif)
40
+ ```
41
+
42
+ **Action Inputs:**
43
+ - `github-token` (required): GitHub token for fetching contributions
44
+ - `username` (optional): Username to generate game for (defaults to repo owner)
45
+ - `output-path` (optional): Where to save the GIF (default: `gh-space-shooter.gif`)
46
+ - `strategy` (optional): Attack pattern - `column`, `row`, or `random` (default: `random`)
47
+ - `fps` (optional): Frames per second for the animation (default: `40`)
48
+ - `commit-message` (optional): Commit message for the update
49
+
50
+ ### From PyPI
20
51
 
21
52
  ```bash
22
53
  pip install gh-space-shooter
@@ -78,6 +109,11 @@ gh-space-shooter torvalds -o my-game.gif
78
109
  gh-space-shooter torvalds --strategy column # Enemies attack in columns
79
110
  gh-space-shooter torvalds --strategy row # Enemies attack in rows
80
111
  gh-space-shooter torvalds -s random # Random chaos (default)
112
+
113
+ # 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
81
117
  ```
82
118
 
83
119
  This creates an animated GIF showing:
@@ -117,8 +153,7 @@ When saved to JSON, the data includes:
117
153
  }
118
154
  ]
119
155
  }
120
- ],
121
- "fetched_at": "2024-12-30T12:00:00"
156
+ ]
122
157
  }
123
158
  ```
124
159
 
@@ -0,0 +1,71 @@
1
+ name: 'GitHub Space Shooter'
2
+ description: 'Transform your GitHub contribution graph into an space shooter game GIF'
3
+ author: 'zane'
4
+
5
+ branding:
6
+ icon: 'navigation-2'
7
+ color: 'black'
8
+
9
+ inputs:
10
+ github-token:
11
+ description: 'GitHub token for fetching contribution data (usually secrets.GITHUB_TOKEN)'
12
+ required: true
13
+ username:
14
+ description: 'GitHub username to generate game for (defaults to repository owner)'
15
+ required: false
16
+ default: ${{ github.repository_owner }}
17
+ output-path:
18
+ description: 'Path where the GIF should be saved'
19
+ required: false
20
+ default: 'gh-space-shooter.gif'
21
+ strategy:
22
+ description: 'Enemy attack strategy (column, row, or random)'
23
+ required: false
24
+ default: 'random'
25
+ fps:
26
+ description: 'Frames per second for the animation (default: 40)'
27
+ required: false
28
+ default: '40'
29
+ commit-message:
30
+ description: 'Commit message for the GIF update'
31
+ required: false
32
+ default: 'Update space shooter game GIF'
33
+
34
+ outputs:
35
+ gif-path:
36
+ description: 'Path to the generated GIF file'
37
+ value: ${{ steps.generate.outputs.gif-path }}
38
+
39
+ runs:
40
+ using: 'composite'
41
+ steps:
42
+ - name: Set up Python
43
+ uses: actions/setup-python@v5
44
+ with:
45
+ python-version: '3.13'
46
+
47
+ - name: Install gh-space-shooter
48
+ shell: bash
49
+ run: |
50
+ python -m pip install --upgrade pip
51
+ pip install gh-space-shooter
52
+
53
+ - name: Generate game GIF
54
+ id: generate
55
+ shell: bash
56
+ env:
57
+ GH_TOKEN: ${{ inputs.github-token }}
58
+ run: |
59
+ gh-space-shooter ${{ inputs.username }} \
60
+ --output ${{ inputs.output-path }} \
61
+ --strategy ${{ inputs.strategy }} \
62
+ --fps ${{ inputs.fps }}
63
+
64
+ - name: Commit and push GIF
65
+ shell: bash
66
+ run: |
67
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
68
+ git config --local user.name "github-actions[bot]"
69
+ git add ${{ inputs.output-path }}
70
+ git diff --staged --quiet || git commit -m "${{ inputs.commit-message }}"
71
+ git push
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gh-space-shooter"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  description = "A CLI tool that visualizes GitHub contribution graphs as gamified GIFs"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -12,7 +12,12 @@ dependencies = [
12
12
  "httpx>=0.27.0",
13
13
  "python-dotenv>=1.0.0",
14
14
  "rich>=13.0.0",
15
- "pillow>=10.0.0",
15
+ "pillow>=10.1.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.0.0",
16
21
  ]
17
22
 
18
23
  [project.scripts]
@@ -24,7 +29,7 @@ build-backend = "hatchling.build"
24
29
 
25
30
 
26
31
  [tool.bumpversion]
27
- current_version = "0.0.2"
32
+ current_version = "0.0.4"
28
33
  parse = """(?x)
29
34
  (?P<major>0|[1-9]\\d*)\\.
30
35
  (?P<minor>0|[1-9]\\d*)\\.
@@ -47,4 +52,8 @@ commit_args = ""
47
52
  [[tool.bumpversion.files]]
48
53
  filename = "pyproject.toml"
49
54
  search = "version = \"{current_version}\""
50
- replace = "version = \"{new_version}\""
55
+ replace = "version = \"{new_version}\""
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
59
+ pythonpath = ["src"]
@@ -8,8 +8,8 @@ import typer
8
8
  from dotenv import load_dotenv
9
9
  from rich.console import Console
10
10
 
11
- from gh_space_shooter.game.strategies.base_strategy import BaseStrategy
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,11 @@ 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
+ ),
58
63
  ) -> None:
59
64
  """
60
65
  Fetch or load GitHub contribution graph data and display it.
@@ -90,7 +95,7 @@ def main(
90
95
  _save_data_to_file(data, raw_output)
91
96
 
92
97
  # Generate GIF if requested
93
- _generate_gif(data, out, strategy)
98
+ _generate_gif(data, out, strategy, fps)
94
99
 
95
100
  except CLIError as e:
96
101
  err_console.print(f"[bold red]Error:[/bold red] {e}")
@@ -146,8 +151,14 @@ def _save_data_to_file(data: ContributionData, file_path: str) -> None:
146
151
  raise CLIError(f"Failed to save file '{file_path}': {e}")
147
152
 
148
153
 
149
- def _generate_gif(data: ContributionData, file_path: str, strategy_name: str) -> None:
154
+ def _generate_gif(data: ContributionData, file_path: str, strategy_name: str, fps: int) -> None:
150
155
  """Generate animated GIF visualization."""
156
+ # GIF format limitation: delays below 20ms (>50 FPS) are clamped by most browsers
157
+ if fps > 50:
158
+ console.print(
159
+ f"[yellow]Warning:[/yellow] FPS > 50 may not display correctly in browsers "
160
+ f"(GIF delay will be {1000 // fps}ms, but browsers clamp delays < 20ms to ~100ms)"
161
+ )
151
162
  console.print("\n[bold blue]Generating GIF animation...[/bold blue]")
152
163
 
153
164
  if strategy_name == "column":
@@ -163,7 +174,7 @@ def _generate_gif(data: ContributionData, file_path: str, strategy_name: str) ->
163
174
 
164
175
  # Create animator and generate GIF
165
176
  try:
166
- animator = Animator(data, strategy)
177
+ animator = Animator(data, strategy, fps=fps)
167
178
  animator.generate_gif(file_path)
168
179
  console.print(f"[green]✓[/green] GIF saved to {file_path}")
169
180
  except Exception as e:
@@ -0,0 +1,30 @@
1
+ """Global constants for the application."""
2
+
3
+ # Animation settings
4
+ DEFAULT_FPS = 40 # Default frames per second for animation
5
+
6
+ # GitHub contribution graph dimensions
7
+ NUM_WEEKS = 52 # Number of weeks in contribution graph
8
+ NUM_DAYS = 7 # Number of days in a week (Sun-Sat)
9
+ SHIP_POSITION_Y = NUM_DAYS + 3 # Ship is positioned just below the grid
10
+
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
14
+ BULLET_TRAILING_LENGTH = 3 # Number of trailing segments for bullets
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
19
+
20
+ # Explosion settings
21
+ EXPLOSION_PARTICLE_COUNT_LARGE = 8 # Number of particles in a large explosion
22
+ EXPLOSION_PARTICLE_COUNT_SMALL = 4 # Number of particles in a small explosion
23
+ EXPLOSION_MAX_RADIUS_LARGE = 20 # Max radius for large explosions
24
+ EXPLOSION_MAX_RADIUS_SMALL = 10 # Max radius for small explosions
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)
@@ -5,7 +5,6 @@ from typing import Iterator
5
5
  from PIL import Image
6
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,7 @@ class Animator:
20
19
  self,
21
20
  contribution_data: ContributionData,
22
21
  strategy: BaseStrategy,
23
- frame_duration: int = FRAME_DURATION_MS,
22
+ fps: int,
24
23
  ):
25
24
  """
26
25
  Initialize animator.
@@ -28,11 +27,15 @@ class Animator:
28
27
  Args:
29
28
  contribution_data: The GitHub contribution data
30
29
  strategy: The strategy to use for clearing enemies
31
- frame_duration: Duration of each frame in milliseconds
30
+ fps: Frames per second for the animation
32
31
  """
33
32
  self.contribution_data = contribution_data
34
33
  self.strategy = strategy
35
- self.frame_duration = frame_duration
34
+ self.fps = fps
35
+ self.frame_duration = 1000 // fps
36
+ # Delta time in seconds per frame
37
+ # Used to scale all speeds (cells/second) to per-frame movement
38
+ self.delta_time = 1.0 / fps
36
39
 
37
40
  def generate_gif(self, output_path: str) -> None:
38
41
  """
@@ -79,18 +82,18 @@ class Animator:
79
82
  for action in self.strategy.generate_actions(game_state):
80
83
  game_state.ship.move_to(action.x)
81
84
  while game_state.can_take_action() is False:
82
- game_state.animate()
85
+ game_state.animate(self.delta_time)
83
86
  yield renderer.render_frame()
84
87
 
85
88
  if action.shoot:
86
89
  game_state.shoot()
87
- game_state.animate()
90
+ game_state.animate(self.delta_time)
88
91
  yield renderer.render_frame()
89
92
 
90
93
  force_kill_countdown = 100
91
94
  # Add final frames showing completion
92
95
  while not game_state.is_complete():
93
- game_state.animate()
96
+ game_state.animate(self.delta_time)
94
97
  yield renderer.render_frame()
95
98
 
96
99
  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, 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
- self.y -= BULLET_SPEED
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)
@@ -52,10 +56,9 @@ class Bullet(Drawable):
52
56
  def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
53
57
  """Draw the bullet with trailing tail effect."""
54
58
 
55
- trail_num = 3
56
- for i in range(trail_num):
57
- trail_y = self.y + (i + 1) * BULLET_SPEED
58
- fade_factor = (i + 1) / trail_num / 2
59
+ for i in range(BULLET_TRAILING_LENGTH):
60
+ trail_y = self.y + (i + 1) * BULLET_TRAIL_SPACING
61
+ fade_factor = (i + 1) / BULLET_TRAILING_LENGTH / 2
59
62
  self._draw_bullet(draw, context, (self.x, trail_y), fade_factor=fade_factor)
60
63
 
61
64
  self._draw_bullet(draw, context, (self.x, self.y), fade_factor=0.3, offset=.6)
@@ -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
 
@@ -6,6 +6,14 @@ from typing import TYPE_CHECKING, Literal
6
6
 
7
7
  from PIL import ImageDraw
8
8
 
9
+ from ...constants import (
10
+ EXPLOSION_DURATION_LARGE,
11
+ EXPLOSION_DURATION_SMALL,
12
+ EXPLOSION_MAX_RADIUS_LARGE,
13
+ EXPLOSION_MAX_RADIUS_SMALL,
14
+ EXPLOSION_PARTICLE_COUNT_LARGE,
15
+ EXPLOSION_PARTICLE_COUNT_SMALL,
16
+ )
9
17
  from .drawable import Drawable
10
18
 
11
19
  if TYPE_CHECKING:
@@ -29,36 +37,36 @@ class Explosion(Drawable):
29
37
  self.x = x
30
38
  self.y = y
31
39
  self.game_state = game_state
32
- self.frame = 0
33
- self.max_frames = 6 if size == "small" else 20
34
- self.max_radius = 10 if size == "small" else 20
35
- self.particle_count = 4 if size == "small" else 8
36
- # Generate random angles for each particle
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
+ self.max_radius = EXPLOSION_MAX_RADIUS_SMALL if size == "small" else EXPLOSION_MAX_RADIUS_LARGE
43
+ self.particle_count = EXPLOSION_PARTICLE_COUNT_SMALL if size == "small" else EXPLOSION_PARTICLE_COUNT_LARGE
37
44
  self.particle_angles = [random.uniform(0, 2 * math.pi) for _ in range(self.particle_count)]
38
45
 
39
- def animate(self) -> None:
40
- """Progress the explosion animation and remove when complete."""
41
- self.frame += 1
42
- if self.frame >= self.max_frames:
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:
43
54
  self.game_state.explosions.remove(self)
44
55
 
45
56
  def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
46
57
  """Draw expanding particle explosion with fade effect."""
47
- # Calculate animation progress (0 to 1)
48
- progress = self.frame / self.max_frames
58
+
59
+ progress = self.elapsed_time / self.duration
49
60
  fade = 1 - progress # Fade out as animation progresses
50
61
 
51
- # Get center position
52
62
  center_x, center_y = context.get_cell_position(self.x, self.y)
53
63
  center_x += context.cell_size // 2
54
64
  center_y += context.cell_size // 2
55
65
 
56
- # Draw expanding particles in random directions
57
66
  for i in range(self.particle_count):
58
67
  distance = progress * self.max_radius
59
68
  angle = self.particle_angles[i]
60
69
 
61
- # Particle position using random angle
62
70
  px = int(center_x + distance * math.cos(angle))
63
71
  py = int(center_y + distance * math.sin(angle))
64
72
 
@@ -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 # Frames until ship can shoot again
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 == 0
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 + SHIP_SPEED, self.target_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 - SHIP_SPEED, self.target_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 -= 1
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."""