diff --git a/kennel/config.py b/kennel/config.py index a731c84..8a09500 100644 --- a/kennel/config.py +++ b/kennel/config.py @@ -17,5 +17,7 @@ class Config: COOKIE_MAX_AGE = int(os.getenv("COOKIE_MAX_AGE", 60 * 60 * 24 * 7)) # 1 week + KENNEL_CATS_POLL_SEC = int(os.getenv("KENNEL_CATS_POLL_SEC", 10)) + config = Config() diff --git a/kennel/engine/components/component.py b/kennel/engine/components/component.py index 9f56407..90506b9 100644 --- a/kennel/engine/components/component.py +++ b/kennel/engine/components/component.py @@ -5,6 +5,7 @@ from enum import Enum class ComponentType(str, Enum): POSITION = "POSITION" CONTROLLABLE = "CONTROLLABLE" + MARKOV = "MARKOV" class Component: diff --git a/kennel/engine/components/markov_transition_state.py b/kennel/engine/components/markov_transition_state.py new file mode 100644 index 0000000..cec8223 --- /dev/null +++ b/kennel/engine/components/markov_transition_state.py @@ -0,0 +1,24 @@ +from kennel.engine.components.component import Component, ComponentType + + +class MarkovTransitionState(Component): + def __init__( + self, + state_names: dict[int, str], + initial_state_vector: list[float], + transition_matrix: list[list[float]], + ): + # TODO: Poll rate per state? + # TODO: State being an enum instead of a vector, just choose max and map + self.state_names = state_names + self.state = initial_state_vector + self.transition_matrix = transition_matrix + + super().__init__(ComponentType.MARKOV) + + def get_max_state_name(self, state_vector: list[float]): + max_val = max(state_vector) + return self.state_names[state_vector.index(max_val)] + + def to_dict(self): + return {"state": self.get_max_state_name(self.state)} diff --git a/kennel/engine/entities/cat.py b/kennel/engine/entities/cat.py index 02fdb33..edcde37 100644 --- a/kennel/engine/entities/cat.py +++ b/kennel/engine/entities/cat.py @@ -1,6 +1,18 @@ +from kennel.engine.components.position import Position + +from .entity import Entity, EntityType + + +class Cat(Entity): + def __init__(self, id: str): + components = [Position(0, 0)] + + super().__init__(EntityType.CAT, id, components) + + # # # IDLE, FROLICKING, EEPY, ALERT, CHASING_CURSOR, CHASING_CAT, SCRATCHING, ITCHY -# state_stochastic_matrix = [ +# state_stochastic_matrix = [ [1, 0] # # IDLE # [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0], # # FROLICKING diff --git a/kennel/engine/entities/entity.py b/kennel/engine/entities/entity.py index d40ab19..30e325a 100644 --- a/kennel/engine/entities/entity.py +++ b/kennel/engine/entities/entity.py @@ -6,6 +6,7 @@ from kennel.engine.components.component import Component, ComponentType class EntityType(str, Enum): LASER = "LASER" + CAT = "CAT" class Entity: diff --git a/kennel/engine/systems/markov_transition_state_system.py b/kennel/engine/systems/markov_transition_state_system.py new file mode 100644 index 0000000..ad3de65 --- /dev/null +++ b/kennel/engine/systems/markov_transition_state_system.py @@ -0,0 +1,13 @@ +from kennel.engine.components.component import ComponentType +from kennel.engine.entities.entity import EntityManager +from kennel.engine.systems.system import System, SystemType +from kennel.engine.systems.network import NetworkSystem + + +class MarkovTransitionStateSystem(System): + def __init__(self, network_system: NetworkSystem): + super().__init__(SystemType.MARKOV) + + def update(self, entity_manager: EntityManager, delta_time: float): + entity_manager.get_entities_with_component(ComponentType.MARKOV) + return diff --git a/kennel/engine/systems/system.py b/kennel/engine/systems/system.py index e3ddb27..8c577a6 100644 --- a/kennel/engine/systems/system.py +++ b/kennel/engine/systems/system.py @@ -7,6 +7,7 @@ from kennel.engine.entities.entity import EntityManager class SystemType(str, Enum): NETWORK = "NETWORK" WORLD = "WORLD" + MARKOV = "MARKOV" class System: @@ -14,7 +15,7 @@ class System: self.system_type = system_type @abstractmethod - async def update(self, entity_manager: EntityManager, delta_time: float): + async def update(self, entity_manager: EntityManager, delta_time: float) -> None: pass diff --git a/kennel/kennel.py b/kennel/kennel.py index b4d7b7b..7123b7f 100644 --- a/kennel/kennel.py +++ b/kennel/kennel.py @@ -1,18 +1,27 @@ +import asyncio import uuid +import time from typing import List, Optional from kennel.config import config from kennel.engine.components.component import ComponentType from kennel.engine.entities.entity import Entity, EntityManager from kennel.engine.entities.laser import Laser +from kennel.engine.entities.cat import Cat from kennel.engine.game import Game +from kennel.engine.systems.markov_transition_state_system import ( + MarkovTransitionStateSystem, +) from kennel.engine.systems.network import ( EntityPositionUpdateEvent, + EntityBornEvent, + EntityDeathEvent, Event, EventType, NetworkSystem, UpstreamEventProcessor, ) +from kennel.kennelcats import KennelCatService, KennelCat from kennel.engine.systems.system import SystemManager from kennel.engine.systems.world import WorldSystem @@ -58,12 +67,76 @@ class KennelEventProcessor(UpstreamEventProcessor): return event -system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT)) -system_manager.add_system( - NetworkSystem(KennelEventProcessor()), -) +class KennelCatsManager: + kennel_cat_service: KennelCatService + entity_manager: EntityManager + network_system: NetworkSystem + last_seen: set[str] + poll_interval_sec: int + running: bool + + def __init__( + self, + kennel_cat_service: KennelCatService, + entity_manager: EntityManager, + network_system: NetworkSystem, + poll_interval_sec: int, + ): + self.kennel_cat_service = kennel_cat_service + self.entity_manager = entity_manager + self.network_system = network_system + self.poll_interval_sec = poll_interval_sec + + self.last_seen = set() + self.running = False + + async def start(self) -> None: + logger.info("starting kennel cats manager") + if self.running: + return + + self.running = True + while self.running: + logger.info("polling kennel cats service") + cats = self.kennel_cat_service.get_kennel() + cats_table = {cat.id: cat for cat in cats} + cat_ids = set([cat.id for cat in cats]) + + removed_cats = [cats_table[id] for id in self.last_seen.difference(cat_ids)] + added_cats = [cats_table[id] for id in cat_ids.difference(self.last_seen)] + logger.info(f"removing {removed_cats}") + logger.info(f"adding {added_cats}") + + for removed in removed_cats: + self.entity_manager.remove_entity(removed) + entity_death = EntityDeathEvent(removed.id) + self.network_system.server_global_event(entity_death) + + for added in added_cats: + new_cat = Cat(added.id) + self.entity_manager.add_entity(new_cat) + entity_born = EntityBornEvent(new_cat) + self.network_system.server_global_event(entity_born) + + self.last_seen = cat_ids + await asyncio.sleep(self.poll_interval_sec) + + def stop(self) -> None: + logger.info("stopping kennel cats manager") + self.running = False + + +network_system = NetworkSystem(KennelEventProcessor()) +world_system = WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT) +markov_transition_state_system = MarkovTransitionStateSystem(network_system) +system_manager.add_system(network_system) +system_manager.add_system(world_system) kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) +kennel_cat_service = KennelCatService(config.HATECOMPUTERS_ENDPOINT) +kennel_cats_manager = KennelCatsManager( + kennel_cat_service, entity_manager, network_system, config.KENNEL_CATS_POLL_SEC +) def create_session_controllable_entities(session: str) -> List[Entity]: diff --git a/kennel/kennelcats.py b/kennel/kennelcats.py index 970e44f..2cb2f29 100644 --- a/kennel/kennelcats.py +++ b/kennel/kennelcats.py @@ -37,10 +37,10 @@ class KennelCat: class KennelCatService: - def __init__(self, endpoint: str): - self.endpoint = endpoint + def __init__(self, hc_endpoint: str): + self.hc_endpoint = hc_endpoint def get_kennel(self) -> List[KennelCat]: - response = requests.get(f"{self.endpoint}/kennel") + response = requests.get(f"{self.hc_endpoint}/kennel") response.raise_for_status() return [KennelCat.from_dict(cat) for cat in response.json()] diff --git a/kennel/main.py b/kennel/main.py index 22d3d39..49d62d4 100644 --- a/kennel/main.py +++ b/kennel/main.py @@ -30,6 +30,7 @@ from kennel.kennel import ( create_session_controllable_entities, entity_manager, kennel, + kennel_cats_manager, system_manager, ) @@ -43,12 +44,14 @@ loop = asyncio.get_event_loop() async def startup_event(): logger.info("Starting Kennel...") loop.create_task(kennel.run()) + loop.create_task(kennel_cats_manager.start()) @app.on_event("shutdown") async def shutdown_event(): logger.info("Stopping Kennel...") kennel.stop() + kennel_cats_manager.stop() loop.stop() logger.info("Kennel stopped") diff --git a/static/index.html b/static/index.html index 3770eaa..ca9ed9c 100644 --- a/static/index.html +++ b/static/index.html @@ -9,7 +9,14 @@