aboutsummaryrefslogtreecommitdiff
path: root/kennel
diff options
context:
space:
mode:
Diffstat (limited to 'kennel')
-rw-r--r--kennel/engine/components/component.py2
-rw-r--r--kennel/engine/components/controllable.py2
-rw-r--r--kennel/engine/components/position.py2
-rw-r--r--kennel/engine/entities/entity.py2
-rw-r--r--kennel/engine/systems/network.py104
-rw-r--r--kennel/kennel.py50
-rw-r--r--kennel/main.py42
7 files changed, 131 insertions, 73 deletions
diff --git a/kennel/engine/components/component.py b/kennel/engine/components/component.py
index c8ef2d0..da55138 100644
--- a/kennel/engine/components/component.py
+++ b/kennel/engine/components/component.py
@@ -12,5 +12,5 @@ class Component:
self.component_type = component_type
@abstractmethod
- def dict(self) -> dict:
+ def to_dict(self) -> dict:
pass
diff --git a/kennel/engine/components/controllable.py b/kennel/engine/components/controllable.py
index 25ec835..a9173af 100644
--- a/kennel/engine/components/controllable.py
+++ b/kennel/engine/components/controllable.py
@@ -9,6 +9,6 @@ class Controllable(Component):
def __repr__(self) -> str:
return f"Controllable(by={self.by})"
- def dict(self) -> dict:
+ def to_dict(self) -> dict:
# don't serialize who owns this
return {}
diff --git a/kennel/engine/components/position.py b/kennel/engine/components/position.py
index 2a4da4a..5c5b372 100644
--- a/kennel/engine/components/position.py
+++ b/kennel/engine/components/position.py
@@ -10,5 +10,5 @@ class Position(Component):
def __repr__(self) -> str:
return f"Position(x={self.x}, y={self.y})"
- def dict(self) -> dict:
+ def to_dict(self) -> dict:
return {"x": self.x, "y": self.y}
diff --git a/kennel/engine/entities/entity.py b/kennel/engine/entities/entity.py
index 6623512..1d8c34d 100644
--- a/kennel/engine/entities/entity.py
+++ b/kennel/engine/entities/entity.py
@@ -27,7 +27,7 @@ class Entity:
return {
"entity_type": self.entity_type,
"id": self.id,
- "components": {k: v.dict() for k, v in self.components.items()},
+ "components": {k: v.to_dict() for k, v in self.components.items()},
}
diff --git a/kennel/engine/systems/network.py b/kennel/engine/systems/network.py
index c9696c8..8dbf4cf 100644
--- a/kennel/engine/systems/network.py
+++ b/kennel/engine/systems/network.py
@@ -1,15 +1,17 @@
from enum import Enum
-from kennel.engine.entities.entity import EntityManager
+from kennel.engine.entities.entity import Entity, EntityType, EntityManager
from .system import System, SystemType
from abc import abstractmethod
+from typing import Optional, List
import asyncio
class EventType(str, Enum):
INITIAL_STATE = "INITIAL_STATE"
+ SET_CONTROLLABLE = "SET_CONTROLLABLE"
ENTITY_BORN = "ENTITY_BORN"
- ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
ENTITY_DEATH = "ENTITY_DEATH"
+ ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
class Event:
@@ -17,26 +19,83 @@ class Event:
self.event_type = event_type
self.data = data
- def __str__(self):
+ def __str__(self) -> str:
return f"Event({self.event_type}, {self.data})"
- def dict(self):
+ def to_dict(self) -> dict:
return {"event_type": self.event_type, "data": self.data}
@staticmethod
- def from_dict(data: dict):
+ def from_dict(data: dict) -> Optional["Event"]:
+ if "event_type" not in data or "data" not in data:
+ return
+ event_type = data["event_type"]
+ if event_type == EventType.INITIAL_STATE:
+ return InitialStateEvent(
+ data["data"]["world"]["width"],
+ data["data"]["world"]["height"],
+ data["data"]["entities"],
+ )
+ if event_type == EventType.SET_CONTROLLABLE:
+ return SetControllableEvent(data["data"]["id"], data["data"]["client_id"])
+ if event_type == EventType.ENTITY_BORN:
+ return EntityBornEvent(data["data"])
+ if event_type == EventType.ENTITY_POSITION_UPDATE:
+ return EntityPositionUpdateEvent(
+ data["data"]["id"], data["data"]["position"]
+ )
+ if event_type == EventType.ENTITY_DEATH:
+ return EntityDeathEvent(data["data"]["id"])
+
+ logger.warn(f"Unknown event type: {data['event_type']}")
return Event(EventType(data["event_type"]), data["data"])
-class Publishable:
- @abstractmethod
- async def publish(self, event: Event):
- pass
+class InitialStateEvent(Event):
+ def __init__(self, world_width: int, world_height: int, entities: List[Entity]):
+ self.world_width = world_width
+ self.world_height = world_height
+ self.entities = entities
+ super().__init__(
+ EventType.INITIAL_STATE,
+ {
+ "world": {"width": world_width, "height": world_height},
+ "entities": [entity.to_dict() for entity in entities],
+ },
+ )
+
+
+class SetControllableEvent(Event):
+ def __init__(self, entity_id: str, client_id: str):
+ self.entity_id = entity_id
+ self.client_id = client_id
+ super().__init__(
+ EventType.SET_CONTROLLABLE, {"id": entity_id, "client_id": client_id}
+ )
-class ClientEventTransformer:
+class EntityBornEvent(Event):
+ def __init__(self, entity: Entity):
+ self.entity = entity
+ super().__init__(EventType.ENTITY_BORN, {"entity": entity.to_dict()})
+
+
+class EntityPositionUpdateEvent(Event):
+ def __init__(self, entity_id: str, position: dict):
+ super().__init__(
+ EventType.ENTITY_POSITION_UPDATE,
+ {"id": entity_id, "position": position},
+ )
+
+
+class EntityDeathEvent(Event):
+ def __init__(self, entity_id: str):
+ super().__init__(EventType.ENTITY_DEATH, {"id": entity_id})
+
+
+class Publishable:
@abstractmethod
- def apply(self, event: Event) -> Event:
+ async def publish(self, event: Event):
pass
@@ -47,37 +106,32 @@ class EventProcessor:
class NetworkSystem(System):
- def __init__(
- self,
- event_processor: EventProcessor,
- client_event_transformer: ClientEventTransformer,
- ):
+ def __init__(self, event_processor: EventProcessor):
super().__init__(SystemType.NETWORK)
self.event_processor = event_processor
- self.client_event_transformer = client_event_transformer
self.events = []
+ self.client_events = []
self.clients = {}
async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
- for event in self.events:
+ if len(self.events) + len(self.client_events) == 0:
+ return
+ for event in self.events + self.client_events:
self.event_processor.accept(entity_manager, event)
- client_events = [
- self.client_event_transformer.apply(entity_manager, event)
- for event in self.events
- ]
+ client_sendable = self.events + [event for event, _ in self.client_events]
await asyncio.gather(
*[
client.publish(event)
for client in self.clients.values()
- for event in client_events
+ for event in client_sendable
]
)
self.events = []
+ self.client_events = []
def client_event(self, client_id: str, event: Event) -> None:
- event.data["client_id"] = client_id
- self.events.append(event)
+ self.client_events.append((event, client_id))
def add_event(self, event: Event) -> None:
self.events.append(event)
diff --git a/kennel/kennel.py b/kennel/kennel.py
index 61847a5..bb0438c 100644
--- a/kennel/kennel.py
+++ b/kennel/kennel.py
@@ -6,7 +6,6 @@ from kennel.engine.systems.system import SystemManager
from kennel.engine.systems.network import (
NetworkSystem,
EventProcessor,
- ClientEventTransformer,
Event,
EventType,
)
@@ -21,28 +20,33 @@ system_manager = SystemManager()
class KennelEventProcessor(EventProcessor):
- def accept(self, entity_manager: EntityManager, event: Event) -> None:
+ def accept(
+ self, entity_manager: EntityManager, event: type[Event | tuple[Event, str]]
+ ) -> None:
+ if isinstance(event, tuple):
+ client_event, client_id = event
+ self._process_client_event(entity_manager, client_event, client_id)
+ return
+
+ def _process_client_event(
+ self, entity_manager: EntityManager, event: Event, client_id: str
+ ) -> None:
if event.event_type == EventType.ENTITY_POSITION_UPDATE:
- self._process_entity_position_update(entity_manager, event)
+ self._process_entity_position_update(entity_manager, event, client_id)
def _process_entity_position_update(
- self, entity_manager: EntityManager, event: Event
+ self, entity_manager: EntityManager, event: Event, client_id: str
) -> None:
- entity = entity_manager.get_entity(event.data["entity_id"])
+ entity = entity_manager.get_entity(event.data["id"])
if entity is None:
- logger.error(f"Entity {event.data['entity_id']} does not exist")
- return
- if event.data["client_id"] is None:
- logger.error("Client ID is required for position updates")
+ logger.error(f"Entity(id={event.data['id']}) does not exist")
return
controllable = entity.get_component(ComponentType.CONTROLLABLE)
if controllable is None:
logger.error(f"Entity {entity} is not controllable")
return
- if controllable.by != event.data["client_id"]:
- logger.error(
- f"Entity {entity} is not controllable by client {event.data['client_id']}"
- )
+ if controllable.by != client_id:
+ logger.error(f"Entity {entity} is not controllable by client {client_id}")
return
position = entity.get_component(ComponentType.POSITION)
if position is None:
@@ -51,30 +55,14 @@ class KennelEventProcessor(EventProcessor):
entity.add_component(position)
-class KennelClientEventTransformer(ClientEventTransformer):
- def apply(self, entity_manager: EntityManager, event: Event) -> Event:
- if event.event_type == EventType.ENTITY_BORN:
- id = event.data["id"]
- entity = entity_manager.get_entity(id)
- if entity is None:
- logger.error(f"Entity {id} does not exist")
- return event
- for component_type in entity.components:
- component = entity.get_component(component_type)
- if component is not None:
- event.data[component_type] = component.dict()
-
- return event
-
-
system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
system_manager.add_system(
- NetworkSystem(EventProcessor(), KennelClientEventTransformer()),
+ NetworkSystem(KennelEventProcessor()),
)
kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP)
def create_session_controllable_entities(session: str) -> List[Entity]:
- laser = Laser(uuid.uuid4().hex, session)
+ laser = Laser(uuid.uuid4().hex[:10], session)
return [laser]
diff --git a/kennel/main.py b/kennel/main.py
index 6f661cb..fc0fd0e 100644
--- a/kennel/main.py
+++ b/kennel/main.py
@@ -11,7 +11,14 @@ from fastapi import (
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from kennel.engine.systems.system import SystemType
-from kennel.engine.systems.network import Event, Publishable, EventType
+from kennel.engine.systems.network import (
+ Event,
+ Publishable,
+ EventType,
+ InitialStateEvent,
+ SetControllableEvent,
+ EntityBornEvent,
+)
from typing import Annotated, Optional
from kennel.kennel import (
kennel,
@@ -76,7 +83,7 @@ class WebSocketClient(Publishable):
self.websocket = websocket
async def publish(self, event: Event):
- await self.websocket.send_json(event.dict())
+ await self.websocket.send_json([event.event_type, event.data])
@app.websocket("/ws")
@@ -85,19 +92,16 @@ async def websocket_endpoint(
session: Annotated[str, Depends(get_cookie_or_token)],
):
await websocket.accept()
+ client = WebSocketClient(websocket)
logger.info(f"Websocket connection established for session {session}")
- await websocket.send_json(
- {
- "event_type": EventType.INITIAL_STATE,
- "data": {
- "world": {"width": config.WORLD_WIDTH, "height": config.WORLD_HEIGHT},
- "entities": kennel.entity_manager.to_dict(),
- },
- }
+ initial_state = InitialStateEvent(
+ config.WORLD_WIDTH, config.WORLD_HEIGHT, kennel.entity_manager.to_dict()
)
+ await client.publish(initial_state)
session_entities = create_session_controllable_entities(session)
+ logger.info(f"Creating {len(session_entities)} entities for session {session}")
try:
network_system = system_manager.get_system(SystemType.NETWORK)
if network_system is None:
@@ -106,14 +110,26 @@ async def websocket_endpoint(
for entity in session_entities:
logger.info(f"Adding entity {entity.id} for session {session}")
entity_manager.add_entity(entity)
- network_system.add_event(Event(EventType.ENTITY_BORN, {"id": entity.id}))
+ network_system.add_event(EntityBornEvent(entity))
+
+ set_controllable_event = SetControllableEvent(entity.id, session)
+ await client.publish(set_controllable_event)
network_system.add_client(session, WebSocketClient(websocket))
while True:
message = await websocket.receive_json()
- network_system.client_event(session, Event.from_dict(message))
+ if type(message) is not list:
+ message = [message]
+ events = [Event.from_dict(event) for event in message]
+ if not all([event is not None for event in events]):
+ logger.info(f"Invalid events in: {message}"[:100])
+ continue
+ for event in events:
+ network_system.client_event(session, event)
+ except WebSocketDisconnect as e:
+ logger.info(f"Websocket connection closed by client: {session}")
except Exception as e:
- logger.error(f"WebSocket exception {e}")
+ logger.error("Exception occurred", exc_info=e)
finally:
logger.info("Websocket connection closed")
for entity in session_entities: