gh-space-shooter 0.0.1__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/__init__.py +19 -0
- gh_space_shooter/cli.py +177 -0
- gh_space_shooter/console_printer.py +65 -0
- gh_space_shooter/constants.py +11 -0
- gh_space_shooter/game/__init__.py +27 -0
- gh_space_shooter/game/animator.py +101 -0
- gh_space_shooter/game/drawables/__init__.py +17 -0
- gh_space_shooter/game/drawables/bullet.py +83 -0
- gh_space_shooter/game/drawables/drawable.py +29 -0
- gh_space_shooter/game/drawables/enemy.py +58 -0
- gh_space_shooter/game/drawables/explosion.py +72 -0
- gh_space_shooter/game/drawables/ship.py +107 -0
- gh_space_shooter/game/drawables/starfield.py +65 -0
- gh_space_shooter/game/game_state.py +80 -0
- gh_space_shooter/game/render_context.py +56 -0
- gh_space_shooter/game/renderer.py +44 -0
- gh_space_shooter/game/strategies/__init__.py +14 -0
- gh_space_shooter/game/strategies/base_strategy.py +43 -0
- gh_space_shooter/game/strategies/column_strategy.py +46 -0
- gh_space_shooter/game/strategies/random_strategy.py +61 -0
- gh_space_shooter/game/strategies/row_strategy.py +41 -0
- gh_space_shooter/github_client.py +172 -0
- gh_space_shooter/py.typed +0 -0
- gh_space_shooter-0.0.1.dist-info/METADATA +141 -0
- gh_space_shooter-0.0.1.dist-info/RECORD +28 -0
- gh_space_shooter-0.0.1.dist-info/WHEEL +4 -0
- gh_space_shooter-0.0.1.dist-info/entry_points.txt +2 -0
- gh_space_shooter-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Player ship object."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from PIL import ImageDraw
|
|
6
|
+
|
|
7
|
+
from ...constants import SHIP_POSITION_Y, SHIP_SPEED
|
|
8
|
+
from .drawable import Drawable
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..game_state import GameState
|
|
12
|
+
from ..render_context import RenderContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Ship(Drawable):
|
|
16
|
+
"""Represents the player's ship."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, game_state: "GameState"):
|
|
19
|
+
"""Initialize the ship at starting position."""
|
|
20
|
+
self.x: float = 25 # Start middle of screen
|
|
21
|
+
self.target_x = self.x
|
|
22
|
+
self.shoot_cooldown = 0 # Frames until ship can shoot again
|
|
23
|
+
self.game_state = game_state
|
|
24
|
+
|
|
25
|
+
def move_to(self, x: int):
|
|
26
|
+
"""
|
|
27
|
+
Move ship to a new x position.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
x: Target x position
|
|
31
|
+
"""
|
|
32
|
+
self.target_x = x
|
|
33
|
+
|
|
34
|
+
def is_moving(self) -> bool:
|
|
35
|
+
"""Check if ship is moving to a new position."""
|
|
36
|
+
return self.x != self.target_x
|
|
37
|
+
|
|
38
|
+
def can_shoot(self) -> bool:
|
|
39
|
+
"""Check if ship can shoot (cooldown has finished)."""
|
|
40
|
+
return self.shoot_cooldown == 0
|
|
41
|
+
|
|
42
|
+
def animate(self) -> None:
|
|
43
|
+
"""Update ship position, moving toward target at constant speed."""
|
|
44
|
+
if self.x < self.target_x:
|
|
45
|
+
self.x = min(self.x + SHIP_SPEED, self.target_x)
|
|
46
|
+
elif self.x > self.target_x:
|
|
47
|
+
self.x = max(self.x - SHIP_SPEED, self.target_x)
|
|
48
|
+
|
|
49
|
+
# Decrement shoot cooldown
|
|
50
|
+
if self.shoot_cooldown > 0:
|
|
51
|
+
self.shoot_cooldown -= 1
|
|
52
|
+
|
|
53
|
+
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
54
|
+
"""Draw a simple Galaga-style ship."""
|
|
55
|
+
x, y = context.get_cell_position(self.x, SHIP_POSITION_Y)
|
|
56
|
+
|
|
57
|
+
# Calculate ship dimensions
|
|
58
|
+
center_x = x + context.cell_size // 2
|
|
59
|
+
height = context.cell_size
|
|
60
|
+
wing_width = 8
|
|
61
|
+
|
|
62
|
+
# Draw left wing
|
|
63
|
+
draw.polygon(
|
|
64
|
+
[
|
|
65
|
+
(center_x - 2, y + height * 0.5),
|
|
66
|
+
(center_x - wing_width, y + height * 0.8),
|
|
67
|
+
(center_x - wing_width, y + height * 1),
|
|
68
|
+
(center_x - 2, y + height * 0.7),
|
|
69
|
+
],
|
|
70
|
+
fill=(*context.ship_color, 128)
|
|
71
|
+
)
|
|
72
|
+
draw.rectangle(
|
|
73
|
+
[
|
|
74
|
+
center_x - wing_width - 1, y + height * 0.5,
|
|
75
|
+
center_x - wing_width, y + height * 1,
|
|
76
|
+
],
|
|
77
|
+
fill=context.ship_color
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Draw right wing
|
|
81
|
+
draw.polygon(
|
|
82
|
+
[
|
|
83
|
+
(center_x + 2, y + height * 0.5),
|
|
84
|
+
(center_x + wing_width, y + height * 0.8),
|
|
85
|
+
(center_x + wing_width, y + height * 1),
|
|
86
|
+
(center_x + 2, y + height * 0.7),
|
|
87
|
+
],
|
|
88
|
+
fill=(*context.ship_color, 128)
|
|
89
|
+
)
|
|
90
|
+
draw.rectangle(
|
|
91
|
+
[
|
|
92
|
+
center_x + wing_width, y + height * 0.5,
|
|
93
|
+
center_x + wing_width + 1, y + height * 1
|
|
94
|
+
],
|
|
95
|
+
fill=context.ship_color
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
draw.polygon(
|
|
100
|
+
[
|
|
101
|
+
(center_x, y),
|
|
102
|
+
(center_x - 3, y + height * 0.7),
|
|
103
|
+
(center_x, y + height),
|
|
104
|
+
(center_x + 3, y + height * 0.7),
|
|
105
|
+
],
|
|
106
|
+
fill=context.ship_color
|
|
107
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Animated starfield background."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from PIL import ImageDraw
|
|
7
|
+
|
|
8
|
+
from ...constants import NUM_WEEKS, SHIP_POSITION_Y
|
|
9
|
+
from .drawable import Drawable
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..render_context import RenderContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Starfield(Drawable):
|
|
16
|
+
"""Animated starfield background with slowly moving stars."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize the starfield with random stars."""
|
|
20
|
+
self.stars = []
|
|
21
|
+
# Generate about 100 stars across the play area
|
|
22
|
+
for _ in range(100):
|
|
23
|
+
# Random position across the entire grid area
|
|
24
|
+
x = random.uniform(-2, NUM_WEEKS + 2)
|
|
25
|
+
y = random.uniform(-2, SHIP_POSITION_Y + 4)
|
|
26
|
+
# Brightness: 0.2 to 1.0 (dimmer stars for depth)
|
|
27
|
+
brightness = random.uniform(0.2, 1.0)
|
|
28
|
+
# Size: 1-2 pixels
|
|
29
|
+
size = random.choice([1, 1, 1, 2]) # More 1-pixel stars
|
|
30
|
+
# Speed: slower for dimmer (farther) stars
|
|
31
|
+
speed = 0.02 + (brightness * 0.03) # 0.02-0.05 cells per frame
|
|
32
|
+
self.stars.append([x, y, brightness, size, speed])
|
|
33
|
+
|
|
34
|
+
def animate(self) -> None:
|
|
35
|
+
"""Move stars downward, wrapping around when they go off screen."""
|
|
36
|
+
for star in self.stars:
|
|
37
|
+
# star[1] is the y position, star[4] is the speed
|
|
38
|
+
star[1] += star[4]
|
|
39
|
+
|
|
40
|
+
# Wrap around: if star goes below the screen, move it back to the top
|
|
41
|
+
if star[1] > SHIP_POSITION_Y + 4:
|
|
42
|
+
star[1] = -2
|
|
43
|
+
# Randomize x position when wrapping for variety
|
|
44
|
+
star[0] = random.uniform(-2, NUM_WEEKS + 2)
|
|
45
|
+
|
|
46
|
+
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
47
|
+
"""Draw all stars at their current positions."""
|
|
48
|
+
for star_x, star_y, brightness, size, _ in self.stars:
|
|
49
|
+
# Convert grid position to pixel position
|
|
50
|
+
x, y = context.get_cell_position(star_x, star_y)
|
|
51
|
+
|
|
52
|
+
# Calculate star color (white with varying brightness)
|
|
53
|
+
star_brightness = int(255 * brightness)
|
|
54
|
+
star_color = (star_brightness, star_brightness, star_brightness, 255)
|
|
55
|
+
|
|
56
|
+
# Draw star as a small rectangle or point
|
|
57
|
+
if size == 1:
|
|
58
|
+
# Single pixel star
|
|
59
|
+
draw.point([(x, y)], fill=star_color)
|
|
60
|
+
else:
|
|
61
|
+
# Slightly larger star (2x2)
|
|
62
|
+
draw.rectangle(
|
|
63
|
+
[x, y, x + size - 1, y + size - 1],
|
|
64
|
+
fill=star_color
|
|
65
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Game state management for tracking enemies, ship, and bullets."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, List
|
|
4
|
+
|
|
5
|
+
from PIL import ImageDraw
|
|
6
|
+
|
|
7
|
+
from ..constants import SHIP_SHOOT_COOLDOWN_FRAMES
|
|
8
|
+
from ..github_client import ContributionData
|
|
9
|
+
from .drawables import Bullet, Drawable, Enemy, Explosion, Ship, Starfield
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .render_context import RenderContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GameState(Drawable):
|
|
16
|
+
"""Manages the current state of the game."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, contribution_data: ContributionData):
|
|
19
|
+
"""
|
|
20
|
+
Initialize game state from contribution data.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
contribution_data: The GitHub contribution data
|
|
24
|
+
"""
|
|
25
|
+
self.starfield = Starfield()
|
|
26
|
+
self.ship = Ship(self)
|
|
27
|
+
self.enemies: List[Enemy] = []
|
|
28
|
+
self.bullets: List[Bullet] = []
|
|
29
|
+
self.explosions: List[Explosion] = []
|
|
30
|
+
|
|
31
|
+
self._initialize_enemies(contribution_data)
|
|
32
|
+
|
|
33
|
+
def _initialize_enemies(self, contribution_data: ContributionData):
|
|
34
|
+
"""Create enemies based on contribution levels."""
|
|
35
|
+
weeks = contribution_data["weeks"]
|
|
36
|
+
for week_idx, week in enumerate(weeks):
|
|
37
|
+
for day_idx, day in enumerate(week["days"]):
|
|
38
|
+
level = day["level"]
|
|
39
|
+
if level <= 0:
|
|
40
|
+
continue
|
|
41
|
+
enemy = Enemy(x=week_idx, y=day_idx, health=level, game_state=self)
|
|
42
|
+
self.enemies.append(enemy)
|
|
43
|
+
|
|
44
|
+
def shoot(self) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Ship shoots a bullet and starts cooldown timer.
|
|
47
|
+
"""
|
|
48
|
+
bullet = Bullet(int(self.ship.x), game_state=self)
|
|
49
|
+
self.bullets.append(bullet)
|
|
50
|
+
self.ship.shoot_cooldown = SHIP_SHOOT_COOLDOWN_FRAMES
|
|
51
|
+
|
|
52
|
+
def is_complete(self) -> bool:
|
|
53
|
+
"""Check if game is complete (all enemies destroyed)."""
|
|
54
|
+
return len(self.enemies) == 0
|
|
55
|
+
|
|
56
|
+
def can_take_action(self) -> bool:
|
|
57
|
+
"""Check if ship can take an action (not moving and can shoot)."""
|
|
58
|
+
return not self.ship.is_moving() and self.ship.can_shoot()
|
|
59
|
+
|
|
60
|
+
def animate(self) -> None:
|
|
61
|
+
"""Update all game objects for next frame."""
|
|
62
|
+
self.starfield.animate()
|
|
63
|
+
self.ship.animate()
|
|
64
|
+
for enemy in self.enemies:
|
|
65
|
+
enemy.animate()
|
|
66
|
+
for bullet in self.bullets:
|
|
67
|
+
bullet.animate()
|
|
68
|
+
for explosion in self.explosions:
|
|
69
|
+
explosion.animate()
|
|
70
|
+
|
|
71
|
+
def draw(self, draw: ImageDraw.ImageDraw, context: "RenderContext") -> None:
|
|
72
|
+
"""Draw all game objects including the grid."""
|
|
73
|
+
self.starfield.draw(draw, context)
|
|
74
|
+
for enemy in self.enemies:
|
|
75
|
+
enemy.draw(draw, context)
|
|
76
|
+
for explosion in self.explosions:
|
|
77
|
+
explosion.draw(draw, context)
|
|
78
|
+
for bullet in self.bullets:
|
|
79
|
+
bullet.draw(draw, context)
|
|
80
|
+
self.ship.draw(draw, context)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Rendering context for drawable objects."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RenderContext:
|
|
9
|
+
"""
|
|
10
|
+
Context providing rendering helpers and constants to drawable objects.
|
|
11
|
+
|
|
12
|
+
This encapsulates all the information needed to render game objects,
|
|
13
|
+
including colors, sizes, and helper functions. Can be extended for theming.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Size constants
|
|
17
|
+
cell_size: int
|
|
18
|
+
cell_spacing: int
|
|
19
|
+
padding: int
|
|
20
|
+
|
|
21
|
+
# Colors - can be customized for different themes
|
|
22
|
+
background_color: Tuple[int, int, int]
|
|
23
|
+
grid_color: Tuple[int, int, int]
|
|
24
|
+
ship_color: Tuple[int, int, int]
|
|
25
|
+
bullet_color: Tuple[int, int, int]
|
|
26
|
+
enemy_colors: dict[int, Tuple[int, int, int]] # Maps health level to color
|
|
27
|
+
|
|
28
|
+
def get_cell_position(self, x: float, y: float) -> tuple[float, float]:
|
|
29
|
+
"""
|
|
30
|
+
Get the pixel position (x, y) for a grid coordinate.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
week: Week position (0-51, can be fractional for smooth animation)
|
|
34
|
+
day: Day position (0-6, can be fractional for smooth animation)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (x, y) pixel coordinates
|
|
38
|
+
"""
|
|
39
|
+
return (
|
|
40
|
+
self.padding + x * (self.cell_size + self.cell_spacing),
|
|
41
|
+
self.padding + y * (self.cell_size + self.cell_spacing),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def darkmode() -> "RenderContext":
|
|
46
|
+
"""Predefined dark mode rendering context."""
|
|
47
|
+
return RenderContext(
|
|
48
|
+
cell_size=12,
|
|
49
|
+
cell_spacing=3,
|
|
50
|
+
padding=40,
|
|
51
|
+
background_color=(13, 17, 23),
|
|
52
|
+
grid_color=(22, 27, 34),
|
|
53
|
+
enemy_colors={1: (0, 109, 50), 2: (38, 166, 65), 3: (57, 211, 83), 4: (87, 242, 135)},
|
|
54
|
+
ship_color=(68, 147, 248),
|
|
55
|
+
bullet_color=(255, 223, 0),
|
|
56
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Renderer for drawing game frames using Pillow."""
|
|
2
|
+
|
|
3
|
+
from PIL import Image, ImageDraw
|
|
4
|
+
|
|
5
|
+
from ..constants import NUM_WEEKS, SHIP_POSITION_Y
|
|
6
|
+
from .game_state import GameState
|
|
7
|
+
from .render_context import RenderContext
|
|
8
|
+
|
|
9
|
+
class Renderer:
|
|
10
|
+
"""Renders game state as PIL Images."""
|
|
11
|
+
def __init__(self, game_state: GameState, render_context: RenderContext):
|
|
12
|
+
"""
|
|
13
|
+
Initialize renderer.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
game_state: The game state to render
|
|
17
|
+
render_context: Rendering configuration and theming
|
|
18
|
+
"""
|
|
19
|
+
self.game_state = game_state
|
|
20
|
+
|
|
21
|
+
self.context = render_context
|
|
22
|
+
|
|
23
|
+
self.grid_width = NUM_WEEKS * (self.context.cell_size + self.context.cell_spacing)
|
|
24
|
+
self.grid_height = SHIP_POSITION_Y * (self.context.cell_size + self.context.cell_spacing)
|
|
25
|
+
self.width = self.grid_width + 2 * self.context.padding
|
|
26
|
+
self.height = self.grid_height + 2 * self.context.padding
|
|
27
|
+
|
|
28
|
+
def render_frame(self) -> Image.Image:
|
|
29
|
+
"""
|
|
30
|
+
Render the current game state as an image.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
PIL Image of the current frame
|
|
34
|
+
"""
|
|
35
|
+
# Create image with background color
|
|
36
|
+
img = Image.new("RGB", (self.width, self.height), self.context.background_color)
|
|
37
|
+
|
|
38
|
+
# Draw game state
|
|
39
|
+
overlay = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0))
|
|
40
|
+
draw = ImageDraw.Draw(overlay, "RGBA")
|
|
41
|
+
self.game_state.draw(draw, self.context)
|
|
42
|
+
combined = Image.alpha_composite(img.convert("RGBA"), overlay)
|
|
43
|
+
|
|
44
|
+
return combined.convert("RGB").convert("P", palette=Image.Palette.ADAPTIVE)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Strategy implementations for enemy clearing."""
|
|
2
|
+
|
|
3
|
+
from .base_strategy import Action, BaseStrategy
|
|
4
|
+
from .column_strategy import ColumnStrategy
|
|
5
|
+
from .random_strategy import RandomStrategy
|
|
6
|
+
from .row_strategy import RowStrategy
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseStrategy",
|
|
10
|
+
"Action",
|
|
11
|
+
"ColumnStrategy",
|
|
12
|
+
"RowStrategy",
|
|
13
|
+
"RandomStrategy",
|
|
14
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Base strategy interface for enemy clearing strategies."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Iterator
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..game_state import GameState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Action:
|
|
11
|
+
"""Represents a single action in the game."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, x: int, shoot: bool = False):
|
|
14
|
+
"""
|
|
15
|
+
Initialize an action.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
x: Week position (0-51) where ship should move
|
|
19
|
+
shoot: Whether to shoot at this position
|
|
20
|
+
"""
|
|
21
|
+
self.x = x
|
|
22
|
+
self.shoot = shoot
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
action_type = "SHOOT" if self.shoot else "MOVE"
|
|
26
|
+
return f"Action({action_type} x={self.x})"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseStrategy(ABC):
|
|
30
|
+
"""Abstract base class for enemy clearing strategies."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
|
|
34
|
+
"""
|
|
35
|
+
Generate sequence of actions for the ship to clear enemies.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
game_state: The current game state with enemies, ship, and bullets
|
|
39
|
+
|
|
40
|
+
Yields:
|
|
41
|
+
Action objects representing ship movements and shots
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Column-by-column strategy: Ship moves week by week (left to right)."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Iterator
|
|
4
|
+
|
|
5
|
+
from ...constants import NUM_WEEKS
|
|
6
|
+
from .base_strategy import Action, BaseStrategy
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..game_state import GameState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ColumnStrategy(BaseStrategy):
|
|
13
|
+
"""
|
|
14
|
+
Ship moves column by column (week by week) from left to right.
|
|
15
|
+
|
|
16
|
+
For each week, the ship shoots at all enemies in that column until destroyed,
|
|
17
|
+
reacting to the actual game state rather than planning ahead.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
|
|
21
|
+
"""
|
|
22
|
+
Generate actions moving week by week, reacting to living enemies.
|
|
23
|
+
|
|
24
|
+
The ship moves through each week (column), shooting at enemies
|
|
25
|
+
until all enemies in that week are destroyed, then moves to the next week.
|
|
26
|
+
Uses actual game state to determine which enemies are still alive.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
game_state: The current game state with living enemies
|
|
30
|
+
|
|
31
|
+
Yields:
|
|
32
|
+
Action objects representing ship movements and shots
|
|
33
|
+
"""
|
|
34
|
+
# Process each week (column) from left to right
|
|
35
|
+
for week_idx in range(NUM_WEEKS):
|
|
36
|
+
# Keep shooting at enemies in this week until none remain
|
|
37
|
+
while True:
|
|
38
|
+
# Find all living enemies in this week
|
|
39
|
+
enemies_in_week = [e for e in game_state.enemies if e.x == week_idx]
|
|
40
|
+
total_health = sum(e.health for e in enemies_in_week)
|
|
41
|
+
flying_bullets = len([b for b in game_state.bullets if int(b.x) == week_idx])
|
|
42
|
+
|
|
43
|
+
if flying_bullets >= total_health:
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
yield Action(x=week_idx, shoot=True)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Random strategy: Pick random columns and shoot from bottom up."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from typing import TYPE_CHECKING, Iterator
|
|
5
|
+
|
|
6
|
+
from .base_strategy import Action, BaseStrategy
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..game_state import GameState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RandomStrategy(BaseStrategy):
|
|
13
|
+
"""
|
|
14
|
+
Ship uses weighted random selection to pick columns based on distance.
|
|
15
|
+
|
|
16
|
+
Takes the 8 closest columns and applies distance-based weights for selection,
|
|
17
|
+
creating a balanced mix of efficiency and unpredictability.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
|
|
21
|
+
"""
|
|
22
|
+
Generate actions using weighted random selection based on distance.
|
|
23
|
+
|
|
24
|
+
Sorts columns by distance, takes the 8 closest, and applies weights:
|
|
25
|
+
- Distance 0 (same position): weight 3
|
|
26
|
+
- Distance 1-3: weight 5 (highest priority)
|
|
27
|
+
- Distance 4+: weight 1 (lowest priority)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
game_state: The current game state with living enemies
|
|
31
|
+
|
|
32
|
+
Yields:
|
|
33
|
+
Action objects representing ship movements and shots
|
|
34
|
+
"""
|
|
35
|
+
while game_state.enemies:
|
|
36
|
+
columns_with_enemies = list(set(e.x for e in game_state.enemies))
|
|
37
|
+
ship_x = game_state.ship.x
|
|
38
|
+
|
|
39
|
+
# Take the first 8 closest columns
|
|
40
|
+
columns_by_distance = sorted(columns_with_enemies, key=lambda col: abs(col - ship_x))
|
|
41
|
+
candidate_columns = columns_by_distance[:8]
|
|
42
|
+
|
|
43
|
+
# Assign weights based on distance
|
|
44
|
+
weights = []
|
|
45
|
+
for col in candidate_columns:
|
|
46
|
+
distance = abs(col - ship_x)
|
|
47
|
+
if distance == 0:
|
|
48
|
+
weights.append(10)
|
|
49
|
+
elif 1 <= distance <= 3:
|
|
50
|
+
weights.append(100)
|
|
51
|
+
else: # distance >= 4
|
|
52
|
+
weights.append(1)
|
|
53
|
+
|
|
54
|
+
# Choose randomly with weights
|
|
55
|
+
target_column = random.choices(candidate_columns, weights=weights)[0]
|
|
56
|
+
|
|
57
|
+
enemies_in_column = [e for e in game_state.enemies if e.x == target_column]
|
|
58
|
+
lowest_enemy = max(enemies_in_column, key=lambda e: e.y)
|
|
59
|
+
|
|
60
|
+
for _ in range(lowest_enemy.health):
|
|
61
|
+
yield Action(x=target_column, shoot=True)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Row-by-row strategy: Process enemies row by row (day by day)."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Iterator
|
|
4
|
+
|
|
5
|
+
from ...constants import NUM_DAYS
|
|
6
|
+
from .base_strategy import Action, BaseStrategy
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..game_state import GameState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RowStrategy(BaseStrategy):
|
|
13
|
+
"""
|
|
14
|
+
Ship processes enemies row by row (day by day) from top to bottom.
|
|
15
|
+
|
|
16
|
+
For each row (day), the ship moves to each enemy position in that row
|
|
17
|
+
and shoots until all enemies in the row are destroyed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def generate_actions(self, game_state: "GameState") -> Iterator[Action]:
|
|
21
|
+
"""
|
|
22
|
+
Generate actions processing enemies row by row.
|
|
23
|
+
|
|
24
|
+
The ship processes each day (row) from Sunday (0) to Saturday (6),
|
|
25
|
+
moving horizontally to shoot at all enemies in that row before
|
|
26
|
+
proceeding to the next row.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
game_state: The current game state with living enemies
|
|
30
|
+
|
|
31
|
+
Yields:
|
|
32
|
+
Action objects representing ship movements and shots
|
|
33
|
+
"""
|
|
34
|
+
# Process each day (row) from top to bottom
|
|
35
|
+
for day_idx in range(NUM_DAYS - 1, -1, -1):
|
|
36
|
+
enemies_in_row = [e for e in game_state.enemies if e.y == day_idx]
|
|
37
|
+
enemies_in_row.sort(key=lambda e: e.x * (day_idx % 2 * 2 - 1)) # zig-zag
|
|
38
|
+
|
|
39
|
+
for enemy in enemies_in_row:
|
|
40
|
+
for _ in range(enemy.health):
|
|
41
|
+
yield Action(x=enemy.x, shoot=True)
|