adding basic game engine objects
This commit is contained in:
parent
add88fb211
commit
5282323dd8
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
testpaths = test
|
||||||
|
pythonpath = src
|
6
src/antikythera/engine/__init__.py
Normal file
6
src/antikythera/engine/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from .vector import Vec2
|
||||||
|
|
||||||
|
__all__ = ['Vec2']
|
||||||
|
|
30
src/antikythera/engine/__main__.py
Normal file
30
src/antikythera/engine/__main__.py
Normal 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}")
|
45
src/antikythera/engine/entity.py
Normal file
45
src/antikythera/engine/entity.py
Normal 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"
|
82
src/antikythera/engine/game.py
Normal file
82
src/antikythera/engine/game.py
Normal 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"
|
69
src/antikythera/engine/grid.py
Normal file
69
src/antikythera/engine/grid.py
Normal 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"
|
29
src/antikythera/engine/player.py
Normal file
29
src/antikythera/engine/player.py
Normal 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}"
|
33
src/antikythera/engine/vector.py
Normal file
33
src/antikythera/engine/vector.py
Normal 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
48
test/test_entity.py
Normal 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
50
test/test_game.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user