snipes-game 0.1.0__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.
- snipes_game-0.1.0/LICENSE +21 -0
- snipes_game-0.1.0/PKG-INFO +79 -0
- snipes_game-0.1.0/README.md +54 -0
- snipes_game-0.1.0/pyproject.toml +40 -0
- snipes_game-0.1.0/setup.cfg +4 -0
- snipes_game-0.1.0/snipe/__init__.py +3 -0
- snipes_game-0.1.0/snipe/__main__.py +6 -0
- snipes_game-0.1.0/snipe/ai.py +88 -0
- snipes_game-0.1.0/snipe/camera.py +30 -0
- snipes_game-0.1.0/snipe/entities.py +71 -0
- snipes_game-0.1.0/snipe/main.py +375 -0
- snipes_game-0.1.0/snipe/maze.py +201 -0
- snipes_game-0.1.0/snipe/physics.py +116 -0
- snipes_game-0.1.0/snipe/renderer.py +92 -0
- snipes_game-0.1.0/snipe/screens.py +71 -0
- snipes_game-0.1.0/snipe/settings.py +114 -0
- snipes_game-0.1.0/snipes_game.egg-info/PKG-INFO +79 -0
- snipes_game-0.1.0/snipes_game.egg-info/SOURCES.txt +20 -0
- snipes_game-0.1.0/snipes_game.egg-info/dependency_links.txt +1 -0
- snipes_game-0.1.0/snipes_game.egg-info/entry_points.txt +2 -0
- snipes_game-0.1.0/snipes_game.egg-info/requires.txt +1 -0
- snipes_game-0.1.0/snipes_game.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jerry Westrick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snipes-game
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A recreation of the classic 1983 Novell NetWare game Snipes
|
|
5
|
+
Author: Jerry Westrick
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JerryWestrick/snipes
|
|
8
|
+
Project-URL: Repository, https://github.com/JerryWestrick/snipes
|
|
9
|
+
Project-URL: Issues, https://github.com/JerryWestrick/snipes/issues
|
|
10
|
+
Keywords: game,pygame,retro,arcade,snipes,novell
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Games/Entertainment :: Arcade
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pygame-ce>=2.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Snipes
|
|
27
|
+
|
|
28
|
+
A Python/Pygame recreation of the classic 1983 Novell NetWare game.
|
|
29
|
+
|
|
30
|
+
The original Snipes was one of the first network games for personal computers, created by Drew Major, Dale Neibaur, and Kyle Powell to test and debug Novell's networking software. This version brings the gameplay to modern systems using Pygame.
|
|
31
|
+
|
|
32
|
+
## Gameplay
|
|
33
|
+
|
|
34
|
+
Navigate a randomly generated maze, destroy enemy hives and the snipes they spawn. Clear all hives and snipes to advance to the next level.
|
|
35
|
+
|
|
36
|
+
- **9 levels** with increasingly larger mazes, more hives, faster and more numerous enemies
|
|
37
|
+
- **8-direction shooting** — combine two keys for diagonal shots
|
|
38
|
+
- **Diagonal shots ricochet** off walls — bank shots around corners
|
|
39
|
+
- Score and lives carry across levels
|
|
40
|
+
|
|
41
|
+
## Controls
|
|
42
|
+
|
|
43
|
+
| Keys | Action |
|
|
44
|
+
|------|--------|
|
|
45
|
+
| Arrow keys | Move |
|
|
46
|
+
| W A S D | Shoot (combine for diagonals) |
|
|
47
|
+
| P | Pause |
|
|
48
|
+
| ESC | Quit |
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install snipe
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or from source:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/JerryWestrick/snipes.git
|
|
60
|
+
cd snipes
|
|
61
|
+
pip install .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Run
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
snipe
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m snipe
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Python 3.10+
|
|
79
|
+
- pygame-ce
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Snipes
|
|
2
|
+
|
|
3
|
+
A Python/Pygame recreation of the classic 1983 Novell NetWare game.
|
|
4
|
+
|
|
5
|
+
The original Snipes was one of the first network games for personal computers, created by Drew Major, Dale Neibaur, and Kyle Powell to test and debug Novell's networking software. This version brings the gameplay to modern systems using Pygame.
|
|
6
|
+
|
|
7
|
+
## Gameplay
|
|
8
|
+
|
|
9
|
+
Navigate a randomly generated maze, destroy enemy hives and the snipes they spawn. Clear all hives and snipes to advance to the next level.
|
|
10
|
+
|
|
11
|
+
- **9 levels** with increasingly larger mazes, more hives, faster and more numerous enemies
|
|
12
|
+
- **8-direction shooting** — combine two keys for diagonal shots
|
|
13
|
+
- **Diagonal shots ricochet** off walls — bank shots around corners
|
|
14
|
+
- Score and lives carry across levels
|
|
15
|
+
|
|
16
|
+
## Controls
|
|
17
|
+
|
|
18
|
+
| Keys | Action |
|
|
19
|
+
|------|--------|
|
|
20
|
+
| Arrow keys | Move |
|
|
21
|
+
| W A S D | Shoot (combine for diagonals) |
|
|
22
|
+
| P | Pause |
|
|
23
|
+
| ESC | Quit |
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install snipe
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or from source:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/JerryWestrick/snipes.git
|
|
35
|
+
cd snipes
|
|
36
|
+
pip install .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Run
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
snipe
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m snipe
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Python 3.10+
|
|
54
|
+
- pygame-ce
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snipes-game"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A recreation of the classic 1983 Novell NetWare game Snipes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Jerry Westrick"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["game", "pygame", "retro", "arcade", "snipes", "novell"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Games/Entertainment :: Arcade",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pygame-ce>=2.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/JerryWestrick/snipes"
|
|
33
|
+
Repository = "https://github.com/JerryWestrick/snipes"
|
|
34
|
+
Issues = "https://github.com/JerryWestrick/snipes/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
snipe = "snipe.main:main"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
include = ["snipe*"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Snipe AI behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
import math
|
|
7
|
+
from snipe.entities import Snipe, Bullet
|
|
8
|
+
from snipe.settings import DIRECTIONS, SNIPE_SHOOT_CHANCE
|
|
9
|
+
from snipe.physics import move_circle
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_DIR_NAMES = list(DIRECTIONS.keys())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def update_snipe(snipe: Snipe, player_x: float, player_y: float,
|
|
16
|
+
dt: float, walls: list, rng: random.Random) -> Bullet | None:
|
|
17
|
+
"""Update a snipe's movement and maybe shoot. Returns a bullet or None."""
|
|
18
|
+
if not snipe.alive:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
# Movement timer
|
|
22
|
+
snipe.move_timer += dt
|
|
23
|
+
if snipe.move_timer >= snipe.move_interval:
|
|
24
|
+
snipe.move_timer = 0.0
|
|
25
|
+
snipe.direction = _pick_direction(snipe, player_x, player_y, rng)
|
|
26
|
+
|
|
27
|
+
# Move in current direction
|
|
28
|
+
dvx, dvy = DIRECTIONS[snipe.direction]
|
|
29
|
+
dx = dvx * snipe.speed * dt
|
|
30
|
+
dy = dvy * snipe.speed * dt
|
|
31
|
+
snipe.x, snipe.y = move_circle(snipe.x, snipe.y, dx, dy, snipe.radius, walls)
|
|
32
|
+
|
|
33
|
+
# Maybe shoot
|
|
34
|
+
if rng.random() < SNIPE_SHOOT_CHANCE:
|
|
35
|
+
return _shoot_at_player(snipe, player_x, player_y)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _pick_direction(snipe: Snipe, player_x: float, player_y: float,
|
|
40
|
+
rng: random.Random) -> str:
|
|
41
|
+
"""Pick a direction biased toward the player."""
|
|
42
|
+
dx = player_x - snipe.x
|
|
43
|
+
dy = player_y - snipe.y
|
|
44
|
+
|
|
45
|
+
# Target direction components
|
|
46
|
+
tx = 1 if dx > 0 else (-1 if dx < 0 else 0)
|
|
47
|
+
ty = 1 if dy > 0 else (-1 if dy < 0 else 0)
|
|
48
|
+
|
|
49
|
+
# Weight directions toward player
|
|
50
|
+
candidates = []
|
|
51
|
+
for name, (dvx, dvy) in DIRECTIONS.items():
|
|
52
|
+
weight = 1
|
|
53
|
+
# Normalize diagonal components for comparison
|
|
54
|
+
sx = 1 if dvx > 0 else (-1 if dvx < 0 else 0)
|
|
55
|
+
sy = 1 if dvy > 0 else (-1 if dvy < 0 else 0)
|
|
56
|
+
if sx == tx and tx != 0:
|
|
57
|
+
weight += 2
|
|
58
|
+
if sy == ty and ty != 0:
|
|
59
|
+
weight += 2
|
|
60
|
+
candidates.extend([name] * weight)
|
|
61
|
+
|
|
62
|
+
return rng.choice(candidates)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _shoot_at_player(snipe: Snipe, player_x: float, player_y: float) -> Bullet:
|
|
66
|
+
"""Shoot toward the player using the nearest of 8 directions."""
|
|
67
|
+
dx = player_x - snipe.x
|
|
68
|
+
dy = player_y - snipe.y
|
|
69
|
+
angle = math.atan2(dy, dx)
|
|
70
|
+
|
|
71
|
+
# Find nearest of 8 directions
|
|
72
|
+
best_dir = "N"
|
|
73
|
+
best_diff = float("inf")
|
|
74
|
+
for name, (dvx, dvy) in DIRECTIONS.items():
|
|
75
|
+
dir_angle = math.atan2(dvy, dvx)
|
|
76
|
+
diff = abs(angle - dir_angle)
|
|
77
|
+
if diff > math.pi:
|
|
78
|
+
diff = 2 * math.pi - diff
|
|
79
|
+
if diff < best_diff:
|
|
80
|
+
best_diff = diff
|
|
81
|
+
best_dir = name
|
|
82
|
+
|
|
83
|
+
return Bullet(
|
|
84
|
+
x=snipe.x,
|
|
85
|
+
y=snipe.y,
|
|
86
|
+
direction=best_dir,
|
|
87
|
+
owner="snipe",
|
|
88
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Viewport camera centered on the player."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from snipe.settings import SCREEN_WIDTH, SCREEN_HEIGHT
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Camera:
|
|
9
|
+
"""Tracks a target position and provides an offset for rendering."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, maze_width: float, maze_height: float):
|
|
12
|
+
self.maze_width = maze_width
|
|
13
|
+
self.maze_height = maze_height
|
|
14
|
+
self.x: float = 0
|
|
15
|
+
self.y: float = 0
|
|
16
|
+
|
|
17
|
+
def update(self, target_x: float, target_y: float) -> None:
|
|
18
|
+
"""Center the camera on the target."""
|
|
19
|
+
self.x = target_x - SCREEN_WIDTH / 2
|
|
20
|
+
self.y = target_y - SCREEN_HEIGHT / 2
|
|
21
|
+
|
|
22
|
+
def apply(self, x: float, y: float) -> tuple[float, float]:
|
|
23
|
+
"""Convert world coordinates to screen coordinates."""
|
|
24
|
+
return (x - self.x, y - self.y)
|
|
25
|
+
|
|
26
|
+
def is_visible(self, x: float, y: float, margin: float = 50) -> bool:
|
|
27
|
+
"""Check if a world position is visible on screen."""
|
|
28
|
+
sx, sy = self.apply(x, y)
|
|
29
|
+
return (-margin < sx < SCREEN_WIDTH + margin and
|
|
30
|
+
-margin < sy < SCREEN_HEIGHT + margin)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Game entities — all positioned with float coordinates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from snipe.settings import (
|
|
7
|
+
PLAYER_RADIUS, PLAYER_SPEED, PLAYER_START_LIVES,
|
|
8
|
+
SNIPE_RADIUS, SNIPE_SPEED, SNIPE_MOVE_INTERVAL,
|
|
9
|
+
HIVE_RADIUS,
|
|
10
|
+
BULLET_RADIUS, BULLET_SPEED, BULLET_MAX_AGE,
|
|
11
|
+
DIRECTIONS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Player:
|
|
17
|
+
x: float
|
|
18
|
+
y: float
|
|
19
|
+
radius: float = PLAYER_RADIUS
|
|
20
|
+
speed: float = PLAYER_SPEED
|
|
21
|
+
lives: int = PLAYER_START_LIVES
|
|
22
|
+
alive: bool = True
|
|
23
|
+
invulnerable: float = 0.0 # seconds remaining
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_invulnerable(self) -> bool:
|
|
27
|
+
return self.invulnerable > 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Bullet:
|
|
32
|
+
x: float
|
|
33
|
+
y: float
|
|
34
|
+
direction: str # one of the 8 direction keys: "N", "NE", etc.
|
|
35
|
+
owner: str # "player" or "snipe"
|
|
36
|
+
radius: float = BULLET_RADIUS
|
|
37
|
+
speed: float = BULLET_SPEED
|
|
38
|
+
age: float = 0.0
|
|
39
|
+
max_age: float = BULLET_MAX_AGE
|
|
40
|
+
alive: bool = True
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def vx(self) -> float:
|
|
44
|
+
return DIRECTIONS[self.direction][0] * self.speed
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def vy(self) -> float:
|
|
48
|
+
return DIRECTIONS[self.direction][1] * self.speed
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Snipe:
|
|
53
|
+
x: float
|
|
54
|
+
y: float
|
|
55
|
+
direction: str = "N"
|
|
56
|
+
radius: float = SNIPE_RADIUS
|
|
57
|
+
speed: float = SNIPE_SPEED
|
|
58
|
+
alive: bool = True
|
|
59
|
+
move_timer: float = 0.0
|
|
60
|
+
move_interval: float = SNIPE_MOVE_INTERVAL
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Hive:
|
|
65
|
+
x: float
|
|
66
|
+
y: float
|
|
67
|
+
radius: float = HIVE_RADIUS
|
|
68
|
+
health: int = 1
|
|
69
|
+
alive: bool = True
|
|
70
|
+
spawn_timer: float = 0.0
|
|
71
|
+
spawn_interval: float = 6.0
|