gh-space-shooter 0.0.1__tar.gz → 0.0.3__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 (40) hide show
  1. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/.github/workflows/publish.yml +3 -3
  2. gh_space_shooter-0.0.3/.github/workflows/test.yml +40 -0
  3. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/.gitignore +3 -0
  4. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/PKG-INFO +45 -14
  5. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/README.md +41 -12
  6. gh_space_shooter-0.0.3/action.yml +66 -0
  7. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/pyproject.toml +13 -4
  8. gh_space_shooter-0.0.3/src/gh_space_shooter/constants.py +19 -0
  9. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/bullet.py +3 -4
  10. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/explosion.py +12 -8
  11. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/github_client.py +0 -2
  12. gh_space_shooter-0.0.3/tests/conftest.py +27 -0
  13. gh_space_shooter-0.0.3/tests/test_bullet_collision.py +121 -0
  14. gh_space_shooter-0.0.3/tests/test_strategies.py +276 -0
  15. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/uv.lock +52 -2
  16. gh_space_shooter-0.0.1/czl9707_contributions.json +0 -2015
  17. gh_space_shooter-0.0.1/src/gh_space_shooter/constants.py +0 -11
  18. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/.github/dependabot.yml +0 -0
  19. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/.python-version +0 -0
  20. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/LICENSE +0 -0
  21. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/example.gif +0 -0
  22. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/__init__.py +0 -0
  23. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/cli.py +0 -0
  24. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/console_printer.py +0 -0
  25. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/__init__.py +0 -0
  26. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/animator.py +0 -0
  27. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/__init__.py +0 -0
  28. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/drawable.py +0 -0
  29. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/enemy.py +0 -0
  30. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/ship.py +0 -0
  31. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/drawables/starfield.py +0 -0
  32. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/game_state.py +0 -0
  33. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/render_context.py +0 -0
  34. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/renderer.py +0 -0
  35. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/strategies/__init__.py +0 -0
  36. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/strategies/base_strategy.py +0 -0
  37. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/strategies/column_strategy.py +0 -0
  38. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/strategies/random_strategy.py +0 -0
  39. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/game/strategies/row_strategy.py +0 -0
  40. {gh_space_shooter-0.0.1 → gh_space_shooter-0.0.3}/src/gh_space_shooter/py.typed +0 -0
@@ -30,9 +30,6 @@ jobs:
30
30
  - name: Install Python ${{ env.PYTHON_LATEST }}
31
31
  run: uv python install ${{ env.PYTHON_LATEST }}
32
32
 
33
- - name: Build
34
- run: uv build
35
-
36
33
  - name: Bump version
37
34
  id: bump
38
35
  uses: callowayproject/bump-my-version@master
@@ -41,6 +38,9 @@ jobs:
41
38
  with:
42
39
  args: ${{ inputs.release_type }}
43
40
  github-token: ${{ secrets.GH_TOKEN }}
41
+
42
+ - name: Build
43
+ run: uv build
44
44
 
45
45
  - name: Publish
46
46
  run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
@@ -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.1
3
+ Version: 0.0.3
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,49 @@ 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`:
28
+
29
+ ```yaml
30
+ name: Update Space Shooter Game
31
+
32
+ on:
33
+ schedule:
34
+ - cron: '0 0 * * *' # Daily at midnight UTC
35
+ workflow_dispatch: # Allow manual trigger
22
36
 
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
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
+ ```
30
57
 
31
- ## Installation
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
+ - `commit-message` (optional): Commit message for the update
32
64
 
33
- ### From PyPI (Recommended)
65
+ ### From PyPI
34
66
 
35
67
  ```bash
36
68
  pip install gh-space-shooter
@@ -131,8 +163,7 @@ When saved to JSON, the data includes:
131
163
  }
132
164
  ]
133
165
  }
134
- ],
135
- "fetched_at": "2024-12-30T12:00:00"
166
+ ]
136
167
  }
137
168
  ```
138
169
 
@@ -4,19 +4,49 @@ 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`:
12
+
13
+ ```yaml
14
+ name: Update Space Shooter Game
15
+
16
+ on:
17
+ schedule:
18
+ - cron: '0 0 * * *' # Daily at midnight UTC
19
+ workflow_dispatch: # Allow manual trigger
8
20
 
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
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
+ ```
16
41
 
17
- ## Installation
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
+ - `commit-message` (optional): Commit message for the update
18
48
 
19
- ### From PyPI (Recommended)
49
+ ### From PyPI
20
50
 
21
51
  ```bash
22
52
  pip install gh-space-shooter
@@ -117,8 +147,7 @@ When saved to JSON, the data includes:
117
147
  }
118
148
  ]
119
149
  }
120
- ],
121
- "fetched_at": "2024-12-30T12:00:00"
150
+ ]
122
151
  }
123
152
  ```
124
153
 
@@ -0,0 +1,66 @@
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
+ commit-message:
26
+ description: 'Commit message for the GIF update'
27
+ required: false
28
+ default: 'Update space shooter game GIF'
29
+
30
+ outputs:
31
+ gif-path:
32
+ description: 'Path to the generated GIF file'
33
+ value: ${{ steps.generate.outputs.gif-path }}
34
+
35
+ runs:
36
+ using: 'composite'
37
+ steps:
38
+ - name: Set up Python
39
+ uses: actions/setup-python@v5
40
+ with:
41
+ python-version: '3.13'
42
+
43
+ - name: Install gh-space-shooter
44
+ shell: bash
45
+ run: |
46
+ python -m pip install --upgrade pip
47
+ pip install gh-space-shooter
48
+
49
+ - name: Generate game GIF
50
+ id: generate
51
+ shell: bash
52
+ env:
53
+ GH_TOKEN: ${{ inputs.github-token }}
54
+ run: |
55
+ gh-space-shooter ${{ inputs.username }} \
56
+ --output ${{ inputs.output-path }} \
57
+ --strategy ${{ inputs.strategy }}
58
+
59
+ - name: Commit and push GIF
60
+ shell: bash
61
+ run: |
62
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
63
+ git config --local user.name "github-actions[bot]"
64
+ git add ${{ inputs.output-path }}
65
+ git diff --staged --quiet || git commit -m "${{ inputs.commit-message }}"
66
+ git push
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gh-space-shooter"
3
- version = "0.0.1"
3
+ version = "0.0.3"
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.1"
32
+ current_version = "0.0.3"
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"]
@@ -0,0 +1,19 @@
1
+ """Global constants for the application."""
2
+
3
+ # GitHub contribution graph dimensions
4
+ NUM_WEEKS = 52 # Number of weeks in contribution graph
5
+ NUM_DAYS = 7 # Number of days in a week (Sun-Sat)
6
+ SHIP_POSITION_Y = NUM_DAYS + 3 # Ship is positioned just below the grid
7
+
8
+ SHIP_SPEED = 0.25 # Cells per frame the ship moves
9
+ BULLET_SPEED = 0.15 # Cells per frame the bullet moves
10
+ BULLET_TRAILING_LENGTH = 3 # Number of trailing segments for bullets
11
+ FRAME_DURATION_MS = 20 # Duration of each frame in milliseconds
12
+ SHIP_SHOOT_COOLDOWN_FRAMES = 10 # Frames between ship shots
13
+
14
+ EXPLOSION_PARTICLE_COUNT_LARGE = 8 # Number of particles in a large explosion
15
+ EXPLOSION_PARTICLE_COUNT_SMALL = 4 # Number of particles in a small explosion
16
+ EXPLOSION_MAX_RADIUS_LARGE = 20 # Max radius for large explosions
17
+ EXPLOSION_MAX_RADIUS_SMALL = 10 # Max radius for small explosions
18
+ EXPLOSION_MAX_FRAMES_LARGE = 20 # Frames for large explosion animation
19
+ EXPLOSION_MAX_FRAMES_SMALL = 6 # Frames for small explosion animation
@@ -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, SHIP_POSITION_Y
8
8
  from .drawable import Drawable
9
9
  from .explosion import Explosion
10
10
 
@@ -52,10 +52,9 @@ class Bullet(Drawable):
52
52
  def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
53
53
  """Draw the bullet with trailing tail effect."""
54
54
 
55
- trail_num = 3
56
- for i in range(trail_num):
55
+ for i in range(BULLET_TRAILING_LENGTH):
57
56
  trail_y = self.y + (i + 1) * BULLET_SPEED
58
- fade_factor = (i + 1) / trail_num / 2
57
+ fade_factor = (i + 1) / BULLET_TRAILING_LENGTH / 2
59
58
  self._draw_bullet(draw, context, (self.x, trail_y), fade_factor=fade_factor)
60
59
 
61
60
  self._draw_bullet(draw, context, (self.x, self.y), fade_factor=0.3, offset=.6)
@@ -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_MAX_FRAMES_LARGE,
11
+ EXPLOSION_MAX_FRAMES_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:
@@ -30,10 +38,9 @@ class Explosion(Drawable):
30
38
  self.y = y
31
39
  self.game_state = game_state
32
40
  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
41
+ self.max_frames = EXPLOSION_MAX_FRAMES_SMALL if size == "small" else EXPLOSION_MAX_FRAMES_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
46
  def animate(self) -> None:
@@ -44,21 +51,18 @@ class Explosion(Drawable):
44
51
 
45
52
  def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
46
53
  """Draw expanding particle explosion with fade effect."""
47
- # Calculate animation progress (0 to 1)
54
+
48
55
  progress = self.frame / self.max_frames
49
56
  fade = 1 - progress # Fade out as animation progresses
50
57
 
51
- # Get center position
52
58
  center_x, center_y = context.get_cell_position(self.x, self.y)
53
59
  center_x += context.cell_size // 2
54
60
  center_y += context.cell_size // 2
55
61
 
56
- # Draw expanding particles in random directions
57
62
  for i in range(self.particle_count):
58
63
  distance = progress * self.max_radius
59
64
  angle = self.particle_angles[i]
60
65
 
61
- # Particle position using random angle
62
66
  px = int(center_x + distance * math.cos(angle))
63
67
  py = int(center_y + distance * math.sin(angle))
64
68
 
@@ -32,7 +32,6 @@ class ContributionData(TypedDict):
32
32
  username: str
33
33
  total_contributions: int
34
34
  weeks: list[ContributionWeek]
35
- fetched_at: str
36
35
 
37
36
 
38
37
  class GitHubAPIError(Exception):
@@ -157,7 +156,6 @@ class GitHubClient:
157
156
  "username": username,
158
157
  "total_contributions": calendar["totalContributions"],
159
158
  "weeks": weeks,
160
- "fetched_at": datetime.now().isoformat(),
161
159
  }
162
160
 
163
161
  LEVEL_MAP = {
@@ -0,0 +1,27 @@
1
+ """Shared test fixtures for gh-space-shooter tests."""
2
+
3
+ import pytest
4
+ from gh_space_shooter.game.game_state import GameState
5
+ from gh_space_shooter.github_client import ContributionData
6
+
7
+
8
+ @pytest.fixture
9
+ def empty_contribution_data() -> ContributionData:
10
+ """Create contribution data with no contributions."""
11
+ return {
12
+ "weeks": [
13
+ {
14
+ "days": [
15
+ {"level": 0, "date": "", "count": 0} for _ in range(7)
16
+ ]
17
+ }
18
+ for _ in range(52)
19
+ ],
20
+ "total_contributions": 0,
21
+ "username": "test_user",
22
+ }
23
+
24
+ @pytest.fixture
25
+ def default_game_state(empty_contribution_data):
26
+ """Create a game state with no enemies."""
27
+ return GameState(empty_contribution_data)
@@ -0,0 +1,121 @@
1
+ """Tests for bullet collision detection logic."""
2
+
3
+ from gh_space_shooter.game.game_state import GameState
4
+ from gh_space_shooter.game.drawables import Bullet, Enemy
5
+ from gh_space_shooter.constants import EXPLOSION_MAX_FRAMES_SMALL, EXPLOSION_MAX_FRAMES_LARGE
6
+
7
+
8
+ class TestBulletCollision:
9
+ """Tests for bullet collision detection and behavior."""
10
+
11
+ def test_collision_detection_same_x_position(self, default_game_state: GameState) -> None:
12
+ """Test that bullet detects collision when at same x position as enemy."""
13
+
14
+ enemy = Enemy(x=5, y=3, health=2, game_state=default_game_state)
15
+ default_game_state.enemies.append(enemy)
16
+ bullet = Bullet(x=5, game_state=default_game_state)
17
+ bullet.y = 2.0
18
+ default_game_state.bullets.append(bullet)
19
+
20
+ hit_enemy = bullet._check_collision()
21
+ assert hit_enemy is enemy
22
+
23
+ bullet.animate()
24
+ assert bullet not in default_game_state.bullets
25
+
26
+ def test_collision_detection_enemy_above_bullet(self, default_game_state: GameState) -> None:
27
+ """Test that collision is detected when enemy.y >= bullet.y."""
28
+
29
+ enemy = Enemy(x=5, y=3, health=2, game_state=default_game_state)
30
+ default_game_state.enemies.append(enemy)
31
+ bullet = Bullet(x=5, game_state=default_game_state)
32
+ bullet.y = 2.5
33
+ default_game_state.bullets.append(bullet)
34
+
35
+ hit_enemy = bullet._check_collision()
36
+ assert hit_enemy is enemy
37
+
38
+ bullet.animate()
39
+ assert bullet not in default_game_state.bullets
40
+
41
+ def test_no_collision_different_x_position(self, default_game_state: GameState) -> None:
42
+ """Test that bullet doesn't detect collision at different x positions."""
43
+
44
+ enemy = Enemy(x=5, y=3, health=2, game_state=default_game_state)
45
+ default_game_state.enemies.append(enemy)
46
+
47
+ bullet = Bullet(x=6, game_state=default_game_state)
48
+ bullet.y = 3.0
49
+
50
+ hit_enemy = bullet._check_collision()
51
+ assert hit_enemy is None
52
+
53
+ def test_bullet_damages_enemy_on_collision(self, default_game_state: GameState) -> None:
54
+ """Test that bullet damages enemy on collision."""
55
+
56
+ enemy = Enemy(x=5, y=3, health=3, game_state=default_game_state)
57
+ default_game_state.enemies.append(enemy)
58
+
59
+ bullet = Bullet(x=5, game_state=default_game_state)
60
+ bullet.y = 2.0
61
+ default_game_state.bullets.append(bullet)
62
+
63
+ bullet.animate()
64
+ assert enemy.health == 2
65
+
66
+ def test_enemy_destroyed_when_health_zero(self, default_game_state: GameState) -> None:
67
+ """Test that enemy is removed when health reaches zero."""
68
+
69
+ enemy = Enemy(x=5, y=3, health=1, game_state=default_game_state)
70
+ default_game_state.enemies.append(enemy)
71
+
72
+ bullet = Bullet(x=5, game_state=default_game_state)
73
+ bullet.y = 2.0
74
+ default_game_state.bullets.append(bullet)
75
+
76
+ bullet.animate()
77
+ assert enemy not in default_game_state.enemies
78
+
79
+ def test_explosion_created_on_collision(self, default_game_state: GameState) -> None:
80
+ """Test that explosion is created when bullet hits enemy."""
81
+
82
+ enemy = Enemy(x=5, y=3, health=2, game_state=default_game_state)
83
+ default_game_state.enemies.append(enemy)
84
+
85
+ bullet = Bullet(x=5, game_state=default_game_state)
86
+ bullet.y = 2.0
87
+ default_game_state.bullets.append(bullet)
88
+
89
+ bullet.animate()
90
+ assert len(default_game_state.explosions) == 1
91
+ assert default_game_state.explosions[0].max_frames == EXPLOSION_MAX_FRAMES_SMALL
92
+
93
+ def test_large_explosion_on_enemy_destroyed(self, default_game_state: GameState) -> None:
94
+ """Test that large explosion is created when enemy is destroyed."""
95
+
96
+ enemy = Enemy(x=5, y=3, health=1, game_state=default_game_state)
97
+ default_game_state.enemies.append(enemy)
98
+
99
+ bullet = Bullet(x=5, game_state=default_game_state)
100
+ bullet.y = 2.0
101
+ default_game_state.bullets.append(bullet)
102
+
103
+ bullet.animate()
104
+ # Should have 2 explosions: small from bullet hit, large from enemy destruction
105
+ assert len(default_game_state.explosions) == 2
106
+ explosion_max_frames = [exp.max_frames for exp in default_game_state.explosions]
107
+ assert EXPLOSION_MAX_FRAMES_SMALL in explosion_max_frames
108
+ assert EXPLOSION_MAX_FRAMES_LARGE in explosion_max_frames
109
+
110
+ def test_bullet_removed_when_off_screen(self, default_game_state: GameState) -> None:
111
+ """Test that bullet is removed when it goes off screen (y < -10)."""
112
+
113
+ bullet = Bullet(x=5, game_state=default_game_state)
114
+ bullet.y = -5.0
115
+ default_game_state.bullets.append(bullet)
116
+
117
+ for _ in range(50):
118
+ if bullet in default_game_state.bullets:
119
+ bullet.animate()
120
+
121
+ assert bullet not in default_game_state.bullets