adding basic game engine objects

This commit is contained in:
spinach 2025-09-29 13:08:50 -04:00
parent add88fb211
commit 5282323dd8
11 changed files with 396 additions and 1 deletions

3
pytest.ini Normal file
View File

@ -0,0 +1,3 @@
[pytest]
testpaths = test
pythonpath = src

View File

@ -1,3 +1,3 @@
# antikythera
2d bot
2d bot

View File

@ -0,0 +1,6 @@
#!/usr/bin/env python3
from .vector import Vec2
__all__ = ['Vec2']

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
import numpy as np
from .entity import Entity
from .grid import Grid
if __name__ == '__main__':
g = Grid(400, 800)
pos = np.array([10, 12], dtype=np.float32)
e = Entity(pos)
g.insert(e)
pos2 = np.array([2, 10], dtype=np.float32)
e2 = Entity(pos2)
g.insert(e2)
p3 = np.array([390, 485], dtype=np.float32)
e3 = Entity(p3)
g.insert(e3)
close = g.getCellOccupants(e.pos)
for e in close:
print(f"{e} needs to be processed")
# print(e, e2)
d = e.distanceFrom(e2)
print(f"e{e.id} is {d:.2f} units from e{e2.id}")

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Entity provides basic entity object for embedding
"""
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import time
import math
from .vector import Vec2, magnitude
class Entity:
# pos: Vec2
# vel: Vec2 = np.array([0,0] dtype=np.float32)
_next_id = 0
def __init__(self, pos: Vec2):
self.id: int = Entity._next_id
Entity._next_id += 1
self.age: float = 0.0
self.pos = pos
self.vel = np.zeros(2, dtype=np.float32)
def setVelocity(self, vel: Vec2):
# can perform additional validation
self.vel = vel
def update(self, dt):
self.pos += self.vel * dt
self.age += dt
def distanceFrom(self, other):
d = self.pos - other.pos
return magnitude(d)
def hitbox(self):
return 0
def __str__(self):
return f"e{self.id} at {self.pos} is {self.age}s old"

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Entity provides basic entity object for embedding
"""
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import time
import math
from .vector import Vec2
from .entity import Entity
class Config:
def __init__(self,
width: int,
height: int,
target_fps:int = 30,
max_bullets: int = 1000):
self.width = width
self.height = height
self.target_fps = target_fps
self.max_bullets = max_bullets
# class GameState(Enum):
# """Current Game State"""
# IDLE = "idle"
# RUNNING = "running"
# PLAYER_DEAD = "dead"
# WIN = "win"
class Game:
"""Game class holds all entities and performs basic game logic"""
def __init__(self, x: int = 400, y: int = 400):
# validate inputs
if x < 1 or y < 1:
err = f"({x},{y}) must be > 1"
raise ValueError(err)
# set width and height for game area
self.x = x
self.y = y
self.bullets = []
def insertBullet(self, e: Entity):
# add entity to grid cell after validation
self._validate(e.pos) # TODO might impair performance too much
cell = self._hash(e.pos)
if cell not in self.grid:
self.grid[cell] = []
self.grid[cell].append(e)
def getCellOccupants(self, pos: Vec2) -> [Entity]:
# get cell occupants
cell = self._hash(pos)
if cell not in self.grid:
return []
return self.grid[cell]
def _validate(self, pos: Vec2):
# validation
if pos[0] < 0 or pos[0] >= self.x:
err = f"X ({pos[0]}) out of grid bounds [0,{self.x})"
raise ValueError(err)
if pos[1] < 0 or pos[1] >= self.y:
err = f"Y ({pos[1]}) out of grid bounds [0,{self.y})"
raise ValueError(err)
def _hash(self, pos: Vec2) -> (int, int):
x = int(pos[0] // self.scale)
y = int(pos[1] // self.scale)
return (x, y)
def __str__(self):
#return f"e{self.id} at {self.pos} is {self.age}s old"
return "TODO"

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Grid provides space for game to occur, partitions based on proximity allowing for optimizations
"""
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import time
import math
from .vector import Vec2, magnitude
from .entity import Entity
class Grid:
def __init__(self, scale: int = 100, x: int = 8, y: int = 4):
# validate inputs
if x < 1 or y < 1:
err = f"({x},{y}) must be positive"
raise ValueError(err)
if scale < 1:
err = f"scaling factor ({scale}) must be greater than 1"
raise ValueError(err)
# set width and height for game area
self.x = x * scale
self.y = y * scale
self.scale = scale
self.grid = {}
def insert(self, e: Entity):
# add entity to grid cell after validation
self._validate(e.pos) # TODO might impair performance too much
cell = self._hash(e.pos)
if cell not in self.grid:
self.grid[cell] = []
self.grid[cell].append(e)
def getCellOccupants(self, pos: Vec2) -> [Entity]:
# get cell occupants
cell = self._hash(pos)
if cell not in self.grid:
return []
return self.grid[cell]
def _validate(self, pos: Vec2):
# validation
if pos[0] < 0 or pos[0] >= self.x:
err = f"X ({pos[0]}) out of grid bounds [0,{self.x})"
raise ValueError(err)
if pos[1] < 0 or pos[1] >= self.y:
err = f"Y ({pos[1]}) out of grid bounds [0,{self.y})"
raise ValueError(err)
def _hash(self, pos: Vec2) -> (int, int):
x = int(pos[0] // self.scale)
y = int(pos[1] // self.scale)
return (x, y)
def __str__(self):
#return f"e{self.id} at {self.pos} is {self.age}s old"
return "TODO"

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
Player provides player object with embedded entity
"""
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import time
import math
from .vector import Vec2, magnitude
from .entity import Entity
class Player(Entity):
def __init__(self, pos: Vec2, hitbox: int):
super().__init__(pos)
self.hitbox = hitbox
def insideHitbox(self, pos: Vec2) -> bool:
return False
def __str__(self):
return f"player ({e.id}) at {e.pos}"

View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""
Vector provides basic vector object and math
"""
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import math
Vec2 = NDArray[np.float32]
def create_vec2(x: float, y: float) -> Vec2:
return np.array([x,y], dtype=np.float32)
def magnitude(vec: Vec2) -> float:
return np.linalg.norm(vec)
def mag_squared(vec: Vec2) -> float:
return np.dot(vec, vec)
def normalize(vec: Vec2) -> Vec2:
mag = magnitude(vec)
# divide by 0 protection
if mag == 0:
return create_vec2(0,0)
return vec / mag
def distance(vec1: Vec2, vec2: Vec2) -> float:
v = v1 - v2
return magnitude(v)

48
test/test_entity.py Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import math
import pytest
from antikythera.engine.entity import Entity
class TestEntityUpdate:
@pytest.fixture
def entity(self):
return Entity(np.array([0,0]))
# x,y velocities
@pytest.mark.parametrize("x,y", [
(0,0),
(1,0),
(0,1),
(1,1),
(-1,0),
(0,-1),
(-1,-1),
(100,100),
])
def test_entity_update(self, x, y):
"""testing that updating entity returns correct pos"""
e = Entity(np.array([0,0], dtype=np.float32))
e.setVelocity(np.array([x,y]))
# checking starting position
assert e.pos[0] == 0
assert e.pos[1] == 0
startingPos = np.array([0,0], dtype=np.float32)
dt = 1/30 # 1 sec / 30 frames
e.update(dt)
exp_X = x * dt + startingPos[0]
exp_Y = y * dt + startingPos[1]
# checking ending location
assert e.pos[0] == exp_X
assert e.pos[1] == exp_Y

50
test/test_game.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""
Grid provides space for game to occur, partitions based on proximity allowing for optimizations
"""
import numpy as np
from numpy.typing import NDArray
from typing import List, Tuple, Optional
import math
import pytest
from antikythera.engine.entity import Entity
from antikythera.engine.game import Game
class TestGameCreation:
@pytest.fixture
def game(self):
return Game(x = 512, y = 512)
# legal coords
@pytest.mark.parametrize("x,y", [
(1,1),
(100,100),
(512,128),
])
def test_game_create(self, x, y):
"""testing that inserting entity at valid pos succeeds"""
g = Game(x, y)
assert g.x == x
assert g.y == y
# illegal game bounds
@pytest.mark.parametrize("x,y", [
(0, 0),
(100,-100),
(-100, 100),
(-100, -100),
])
def test_game_create_fail(self, x, y):
"""testing that creating invalid game board fails"""
with pytest.raises(ValueError):
Game(x, y)