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
|
@ -1,3 +1,3 @@
|
||||
# antikythera
|
||||
|
||||
2d bot
|
||||
2d bot
|
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