Compare commits

..

2 Commits

Author SHA1 Message Date
Elizabeth Hunt 6f374aac7f
send the initial state to clients
continuous-integration/drone/pr Build is failing Details
2024-08-20 18:17:33 -07:00
Elizabeth Hunt b414046001 init commit with base ECS and Network System 2024-08-20 17:34:18 -07:00
9 changed files with 63 additions and 17 deletions

View File

@ -5,8 +5,8 @@ load_dotenv()
class Config: class Config:
SIMULATION_WIDTH = int(os.getenv("SIMULATION_WIDTH", 1000)) WORLD_WIDTH = int(os.getenv("WORLD_WIDTH", 1000))
SIMULATION_HEIGHT = int(os.getenv("SIMULATION_HEIGHT", 1000)) WORLD_HEIGHT = int(os.getenv("WORLD_HEIGHT", 1000))
HATECOMPUTERS_ENDPOINT = os.getenv( HATECOMPUTERS_ENDPOINT = os.getenv(
"HATECOMPUTERS_ENDPOINT", "https://hatecomputers.club" "HATECOMPUTERS_ENDPOINT", "https://hatecomputers.club"

View File

@ -10,4 +10,5 @@ class Controllable(Component):
return f"Controllable(by={self.by})" return f"Controllable(by={self.by})"
def dict(self) -> dict: def dict(self) -> dict:
return {"by": self.by} # don't serialize who owns this
return {}

View File

@ -23,6 +23,13 @@ class Entity:
def add_component(self, component: Component) -> None: def add_component(self, component: Component) -> None:
self.components[component.component_type] = component self.components[component.component_type] = component
def to_dict(self) -> dict:
return {
"entity_type": self.entity_type,
"id": self.id,
"components": {k: v.dict() for k, v in self.components.items()},
}
class EntityManager: class EntityManager:
def __init__(self): def __init__(self):
@ -58,3 +65,6 @@ class EntityManager:
def get_entity(self, entity_id: str) -> Optional[Entity]: def get_entity(self, entity_id: str) -> Optional[Entity]:
return self.entities.get(entity_id) return self.entities.get(entity_id)
def to_dict(self) -> dict:
return {k: v.to_dict() for k, v in self.entities.items()}

View File

@ -6,6 +6,7 @@ import asyncio
class EventType(str, Enum): class EventType(str, Enum):
INITIAL_STATE = "INITIAL_STATE"
ENTITY_BORN = "ENTITY_BORN" ENTITY_BORN = "ENTITY_BORN"
ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
ENTITY_DEATH = "ENTITY_DEATH" ENTITY_DEATH = "ENTITY_DEATH"

View File

@ -5,6 +5,7 @@ from abc import abstractmethod
class SystemType(str, Enum): class SystemType(str, Enum):
NETWORK = "NETWORK" NETWORK = "NETWORK"
WORLD = "WORLD"
class System: class System:

View File

@ -0,0 +1,24 @@
from kennel.engine.systems.system import System, SystemType
from kennel.engine.entities.entity import EntityManager
from kennel.engine.components.component import ComponentType
from kennel.app import logger
class WorldSystem(System):
def __init__(self, width: int, height: int):
super().__init__(SystemType.WORLD)
self.width = width
self.height = height
async def update(self, entity_manager: EntityManager, delta_time: float):
entities = entity_manager.get_entities_with_component(ComponentType.POSITION)
for entity in entities:
position = entity.get_component(ComponentType.POSITION)
if position is None:
logger.error(f"Entity {entity} has no position component")
continue
position.x = max(0, min(self.width, position.x))
position.y = max(0, min(self.height, position.y))
entity.add_component(position)

View File

@ -10,6 +10,7 @@ from kennel.engine.systems.network import (
Event, Event,
EventType, EventType,
) )
from kennel.engine.systems.world import WorldSystem
from kennel.config import config from kennel.config import config
from .app import logger from .app import logger
from typing import List from typing import List
@ -59,9 +60,6 @@ class KennelClientEventTransformer(ClientEventTransformer):
logger.error(f"Entity {id} does not exist") logger.error(f"Entity {id} does not exist")
return event return event
for component_type in entity.components: for component_type in entity.components:
if component_type == ComponentType.CONTROLLABLE:
# Do not send controllable components to clients
continue
component = entity.get_component(component_type) component = entity.get_component(component_type)
if component is not None: if component is not None:
event.data[component_type] = component.dict() event.data[component_type] = component.dict()
@ -69,8 +67,9 @@ class KennelClientEventTransformer(ClientEventTransformer):
return event return event
system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
system_manager.add_system( system_manager.add_system(
NetworkSystem(EventProcessor(), KennelClientEventTransformer()) NetworkSystem(EventProcessor(), KennelClientEventTransformer()),
) )
kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP)

View File

@ -12,16 +12,16 @@ from fastapi.staticfiles import StaticFiles
from kennel.engine.systems.system import SystemType 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
from typing import Annotated, Optional from typing import Annotated, Optional
from .kennel import ( from kennel.kennel import (
kennel, kennel,
system_manager, system_manager,
entity_manager, entity_manager,
create_session_controllable_entities, create_session_controllable_entities,
) )
from .app import app, templates, logger from kennel.app import app, templates, logger
from .kennelcats import KennelCatService from kennel.kennelcats import KennelCatService
from .middleware import logger_middleware from kennel.middleware import logger_middleware
from .config import config from kennel.config import config
import asyncio import asyncio
import uuid import uuid
@ -83,8 +83,19 @@ async def websocket_endpoint(
session: Annotated[str, Depends(get_cookie_or_token)], session: Annotated[str, Depends(get_cookie_or_token)],
): ):
await websocket.accept() await websocket.accept()
session_entities = create_session_controllable_entities(session)
logger.info(f"Websocket connection established for session {session}") 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(),
},
}
)
session_entities = create_session_controllable_entities(session)
try: try:
network_system = system_manager.get_system(SystemType.NETWORK) network_system = system_manager.get_system(SystemType.NETWORK)
if network_system is None: if network_system is None:
@ -97,9 +108,8 @@ async def websocket_endpoint(
network_system.add_client(session, WebSocketClient(websocket)) network_system.add_client(session, WebSocketClient(websocket))
while True: while True:
network_system.client_event( message = await websocket.receive_json()
session, Event.from_dict(await websocket.receive_json()) network_system.client_event(session, Event.from_dict(message))
)
except Exception as e: except Exception as e:
logger.error(f"WebSocket exception {e}") logger.error(f"WebSocket exception {e}")
finally: finally:

View File

@ -5,10 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Kennel Club</title> <title>Kennel Club</title>
<script src='https://unpkg.com/panzoom@9.4.0/dist/panzoom.min.js'></script>
</head> </head>
<body> <body>
<div id="kennel-window"></div> <div id="kennel-window"></div>
<script src="{{ url_for('static', path='/index.js') }}"></script> <script src="{{ url_for('static', path='/index.js') }}"></script>
</body> </body>
</html> </html>