From 5282323dd85753b2eb2886e65a62f1068d1f7d53 Mon Sep 17 00:00:00 2001 From: spinach Date: Mon, 29 Sep 2025 13:08:50 -0400 Subject: [PATCH] adding basic game engine objects --- pytest.ini | 3 ++ README.md => readme.md | 2 +- src/antikythera/engine/__init__.py | 6 +++ src/antikythera/engine/__main__.py | 30 +++++++++++ src/antikythera/engine/entity.py | 45 ++++++++++++++++ src/antikythera/engine/game.py | 82 ++++++++++++++++++++++++++++++ src/antikythera/engine/grid.py | 69 +++++++++++++++++++++++++ src/antikythera/engine/player.py | 29 +++++++++++ src/antikythera/engine/vector.py | 33 ++++++++++++ test/test_entity.py | 48 +++++++++++++++++ test/test_game.py | 50 ++++++++++++++++++ 11 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 pytest.ini rename README.md => readme.md (68%) create mode 100644 src/antikythera/engine/__init__.py create mode 100644 src/antikythera/engine/__main__.py create mode 100644 src/antikythera/engine/entity.py create mode 100644 src/antikythera/engine/game.py create mode 100644 src/antikythera/engine/grid.py create mode 100644 src/antikythera/engine/player.py create mode 100644 src/antikythera/engine/vector.py create mode 100644 test/test_entity.py create mode 100644 test/test_game.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0fe907c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = test +pythonpath = src diff --git a/README.md b/readme.md similarity index 68% rename from README.md rename to readme.md index 0946e27..51ef7b7 100644 --- a/README.md +++ b/readme.md @@ -1,3 +1,3 @@ # antikythera -2d bot \ No newline at end of file +2d bot diff --git a/src/antikythera/engine/__init__.py b/src/antikythera/engine/__init__.py new file mode 100644 index 0000000..ba28a7e --- /dev/null +++ b/src/antikythera/engine/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from .vector import Vec2 + +__all__ = ['Vec2'] + diff --git a/src/antikythera/engine/__main__.py b/src/antikythera/engine/__main__.py new file mode 100644 index 0000000..c42e98c --- /dev/null +++ b/src/antikythera/engine/__main__.py @@ -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}") diff --git a/src/antikythera/engine/entity.py b/src/antikythera/engine/entity.py new file mode 100644 index 0000000..b4a214c --- /dev/null +++ b/src/antikythera/engine/entity.py @@ -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" diff --git a/src/antikythera/engine/game.py b/src/antikythera/engine/game.py new file mode 100644 index 0000000..b8a0b2d --- /dev/null +++ b/src/antikythera/engine/game.py @@ -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" diff --git a/src/antikythera/engine/grid.py b/src/antikythera/engine/grid.py new file mode 100644 index 0000000..e22598e --- /dev/null +++ b/src/antikythera/engine/grid.py @@ -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" diff --git a/src/antikythera/engine/player.py b/src/antikythera/engine/player.py new file mode 100644 index 0000000..9e98e84 --- /dev/null +++ b/src/antikythera/engine/player.py @@ -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}" diff --git a/src/antikythera/engine/vector.py b/src/antikythera/engine/vector.py new file mode 100644 index 0000000..43255b8 --- /dev/null +++ b/src/antikythera/engine/vector.py @@ -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) diff --git a/test/test_entity.py b/test/test_entity.py new file mode 100644 index 0000000..150af42 --- /dev/null +++ b/test/test_entity.py @@ -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 diff --git a/test/test_game.py b/test/test_game.py new file mode 100644 index 0000000..e466eda --- /dev/null +++ b/test/test_game.py @@ -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)