Compare commits
2 Commits
88be03b689
...
6f374aac7f
Author | SHA1 | Date |
---|---|---|
Elizabeth Hunt | 6f374aac7f | |
Elizabeth Hunt | b414046001 |
|
@ -1,2 +0,0 @@
|
|||
*.sqlite
|
||||
*.db
|
|
@ -0,0 +1,13 @@
|
|||
import structlog
|
||||
from fastapi import FastAPI
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
app = FastAPI(
|
||||
servers = [
|
||||
{"url": "https://kennel.hatecomputers.club", "description": "prod"}
|
||||
]
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
|
@ -0,0 +1,20 @@
|
|||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Config:
|
||||
WORLD_WIDTH = int(os.getenv("WORLD_WIDTH", 1000))
|
||||
WORLD_HEIGHT = int(os.getenv("WORLD_HEIGHT", 1000))
|
||||
|
||||
HATECOMPUTERS_ENDPOINT = os.getenv(
|
||||
"HATECOMPUTERS_ENDPOINT", "https://hatecomputers.club"
|
||||
)
|
||||
|
||||
MIN_TIME_STEP = float(os.getenv("MIN_TIME_STEP", 0.05))
|
||||
|
||||
COOKIE_MAX_AGE = int(os.getenv("COOKIE_MAX_AGE", 60 * 60 * 24 * 7)) # 1 week
|
||||
|
||||
|
||||
config = Config()
|
|
@ -0,0 +1,16 @@
|
|||
from enum import Enum
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class ComponentType(str, Enum):
|
||||
POSITION = "POSITION"
|
||||
CONTROLLABLE = "CONTROLLABLE"
|
||||
|
||||
|
||||
class Component:
|
||||
def __init__(self, component_type: ComponentType):
|
||||
self.component_type = component_type
|
||||
|
||||
@abstractmethod
|
||||
def dict(self) -> dict:
|
||||
pass
|
|
@ -0,0 +1,14 @@
|
|||
from .component import Component, ComponentType
|
||||
|
||||
|
||||
class Controllable(Component):
|
||||
def __init__(self, by: str):
|
||||
super().__init__(ComponentType.CONTROLLABLE)
|
||||
self.by = by
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Controllable(by={self.by})"
|
||||
|
||||
def dict(self) -> dict:
|
||||
# don't serialize who owns this
|
||||
return {}
|
|
@ -0,0 +1,14 @@
|
|||
from .component import Component, ComponentType
|
||||
|
||||
|
||||
class Position(Component):
|
||||
def __init__(self, x: float, y: float):
|
||||
super().__init__(ComponentType.POSITION)
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Position(x={self.x}, y={self.y})"
|
||||
|
||||
def dict(self) -> dict:
|
||||
return {"x": self.x, "y": self.y}
|
|
@ -0,0 +1,33 @@
|
|||
#
|
||||
# # IDLE, FROLICKING, EEPY, ALERT, CHASING_CURSOR, CHASING_CAT, SCRATCHING, ITCHY
|
||||
# state_stochastic_matrix = [
|
||||
# # IDLE
|
||||
# [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0],
|
||||
# # FROLICKING
|
||||
# [0.1, 0.5, 0.1, 0.1, 0.1, 0.05, 0.05, 0],
|
||||
# # EEPY
|
||||
# [0.1, 0.1, 0.5, 0.1, 0.1, 0.05, 0.05, 0],
|
||||
# # ALERT
|
||||
# [0.1, 0.1, 0.1, 0.5, 0.1, 0.05, 0.05, 0],
|
||||
# # CHASING_CURSOR
|
||||
# [0.1, 0.1, 0.1, 0.1, 0.5, 0.05, 0.05, 0],
|
||||
# # CHASING_CAT
|
||||
# [0.1, 0.1, 0.1, 0.1, 0.1, 0.5, 0.05, 0],
|
||||
# # SCRATCHING
|
||||
# [0.1, 0.1, 0.1, 0.1, 0.1, 0.05, 0.5, 0],
|
||||
# # ITCHY
|
||||
# [0, 0, 0, 0, 0, 0, 0, 1],
|
||||
# ]
|
||||
#
|
||||
#
|
||||
# class CatState(Enum):
|
||||
# IDLE = 0
|
||||
# FROLICKING = 1
|
||||
# EEPY = 2
|
||||
# ALERT = 3
|
||||
# CHASING_CURSOR = 4
|
||||
# CHASING_CAT = 5
|
||||
# SCRATCHING = 6
|
||||
# ITCHY = 7
|
||||
#
|
||||
#
|
|
@ -0,0 +1,70 @@
|
|||
from enum import Enum
|
||||
from kennel.engine.components.component import Component, ComponentType
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class EntityType(str, Enum):
|
||||
LASER = "LASER"
|
||||
|
||||
|
||||
class Entity:
|
||||
def __init__(
|
||||
self, entity_type: EntityType, id: str, component_list: List[Component]
|
||||
):
|
||||
self.entity_type = entity_type
|
||||
self.id = id
|
||||
self.components = {}
|
||||
for component in component_list:
|
||||
self.add_component(component)
|
||||
|
||||
def get_component(self, component_type: ComponentType) -> Optional[Component]:
|
||||
return self.components[component_type]
|
||||
|
||||
def add_component(self, component: Component) -> None:
|
||||
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:
|
||||
def __init__(self):
|
||||
self.entities = {}
|
||||
self.component_entities = {}
|
||||
|
||||
def update(self) -> None:
|
||||
self.component_entities = {}
|
||||
for entity in self.entities.values():
|
||||
for component in entity.components.values():
|
||||
if component.component_type not in self.component_entities:
|
||||
self.component_entities[component.component_type] = set()
|
||||
self.component_entities[component.component_type].add(entity)
|
||||
|
||||
def get_entities_with_component(
|
||||
self, component_type: ComponentType
|
||||
) -> List[Entity]:
|
||||
return self.component_entities.get(component_type, [])
|
||||
|
||||
def add_entity(self, entity: Entity) -> None:
|
||||
self.entities[entity.id] = entity
|
||||
for component in entity.components.values():
|
||||
if component.component_type not in self.component_entities:
|
||||
self.component_entities[component.component_type] = set()
|
||||
self.component_entities[component.component_type].add(entity)
|
||||
|
||||
def remove_entity(self, entity_id: str) -> None:
|
||||
if entity_id in self.entities:
|
||||
entity = self.entities[entity_id]
|
||||
for component in entity.components.values():
|
||||
if component.component_type in self.component_entities:
|
||||
self.component_entities[component.component_type].remove(entity)
|
||||
|
||||
def get_entity(self, entity_id: str) -> Optional[Entity]:
|
||||
return self.entities.get(entity_id)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {k: v.to_dict() for k, v in self.entities.items()}
|
|
@ -0,0 +1,13 @@
|
|||
from .entity import Entity, EntityType
|
||||
from kennel.engine.components.position import Position
|
||||
from kennel.engine.components.controllable import Controllable
|
||||
|
||||
|
||||
class Laser(Entity):
|
||||
def __init__(self, id: str, controllable_by: str):
|
||||
components = [
|
||||
Position(0, 0),
|
||||
Controllable(controllable_by),
|
||||
]
|
||||
|
||||
super().__init__(EntityType.LASER, id, components)
|
|
@ -0,0 +1,53 @@
|
|||
from kennel.engine.entities.entity import EntityManager
|
||||
from kennel.engine.systems.system import SystemManager
|
||||
from kennel.app import logger
|
||||
from typing import List, Optional
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(
|
||||
self,
|
||||
entity_manager: EntityManager,
|
||||
system_manager: SystemManager,
|
||||
min_time_step: float,
|
||||
):
|
||||
self.entity_manager = entity_manager
|
||||
self.system_manager = system_manager
|
||||
self.min_time_step = min_time_step
|
||||
|
||||
self.last_time = time.time()
|
||||
self.running = False
|
||||
|
||||
async def update(self, delta_time: float) -> None:
|
||||
self.entity_manager.update()
|
||||
await self.system_manager.update(self.entity_manager, delta_time)
|
||||
|
||||
async def run(self) -> None:
|
||||
logger.info("Game started")
|
||||
self.running = True
|
||||
self.last_time = time.time()
|
||||
steps_since_log = 0
|
||||
|
||||
while self.running:
|
||||
current_time = time.time()
|
||||
if (int(current_time) - int(self.last_time)) == 1 and int(
|
||||
current_time
|
||||
) % 10 == 0:
|
||||
logger.info(f"Game loop: {steps_since_log} steps since last log time")
|
||||
steps_since_log = 0
|
||||
|
||||
delta_time = current_time - self.last_time
|
||||
self.last_time = current_time
|
||||
|
||||
await self.update(delta_time)
|
||||
await asyncio.sleep(
|
||||
max(self.min_time_step - (time.time() - current_time), 0)
|
||||
)
|
||||
|
||||
steps_since_log += 1
|
||||
|
||||
def stop(self) -> None:
|
||||
self.running = False
|
||||
logger.info("Game stopped")
|
|
@ -0,0 +1,89 @@
|
|||
from enum import Enum
|
||||
from kennel.engine.entities.entity import EntityManager
|
||||
from .system import System, SystemType
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
INITIAL_STATE = "INITIAL_STATE"
|
||||
ENTITY_BORN = "ENTITY_BORN"
|
||||
ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
|
||||
ENTITY_DEATH = "ENTITY_DEATH"
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, event_type: EventType, data: dict):
|
||||
self.event_type = event_type
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
return f"Event({self.event_type}, {self.data})"
|
||||
|
||||
def dict(self):
|
||||
return {"event_type": self.event_type, "data": self.data}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict):
|
||||
return Event(EventType(data["event_type"]), data["data"])
|
||||
|
||||
|
||||
class Publishable:
|
||||
@abstractmethod
|
||||
async def publish(self, event: Event):
|
||||
pass
|
||||
|
||||
|
||||
class ClientEventTransformer:
|
||||
@abstractmethod
|
||||
def apply(self, event: Event) -> Event:
|
||||
pass
|
||||
|
||||
|
||||
class EventProcessor:
|
||||
@abstractmethod
|
||||
def accept(self, entity_manager: EntityManager, event: Event) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class NetworkSystem(System):
|
||||
def __init__(
|
||||
self,
|
||||
event_processor: EventProcessor,
|
||||
client_event_transformer: ClientEventTransformer,
|
||||
):
|
||||
super().__init__(SystemType.NETWORK)
|
||||
self.event_processor = event_processor
|
||||
self.client_event_transformer = client_event_transformer
|
||||
|
||||
self.events = []
|
||||
self.clients = {}
|
||||
|
||||
async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
|
||||
for event in self.events:
|
||||
self.event_processor.accept(entity_manager, event)
|
||||
client_events = [
|
||||
self.client_event_transformer.apply(entity_manager, event)
|
||||
for event in self.events
|
||||
]
|
||||
await asyncio.gather(
|
||||
*[
|
||||
client.publish(event)
|
||||
for client in self.clients.values()
|
||||
for event in client_events
|
||||
]
|
||||
)
|
||||
self.events = []
|
||||
|
||||
def client_event(self, client_id: str, event: Event) -> None:
|
||||
event.data["client_id"] = client_id
|
||||
self.events.append(event)
|
||||
|
||||
def add_event(self, event: Event) -> None:
|
||||
self.events.append(event)
|
||||
|
||||
def add_client(self, client_id: str, client: Publishable) -> None:
|
||||
self.clients[client_id] = client
|
||||
|
||||
def remove_client(self, client_id: str) -> None:
|
||||
del self.clients[client_id]
|
|
@ -0,0 +1,32 @@
|
|||
from enum import Enum
|
||||
from kennel.engine.entities.entity import EntityManager
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class SystemType(str, Enum):
|
||||
NETWORK = "NETWORK"
|
||||
WORLD = "WORLD"
|
||||
|
||||
|
||||
class System:
|
||||
def __init__(self, system_type: SystemType):
|
||||
self.system_type = system_type
|
||||
|
||||
@abstractmethod
|
||||
async def update(self, entity_manager: EntityManager, delta_time: float):
|
||||
pass
|
||||
|
||||
|
||||
class SystemManager:
|
||||
def __init__(self):
|
||||
self.systems = {}
|
||||
|
||||
def add_system(self, system: System) -> None:
|
||||
self.systems[system.system_type] = system
|
||||
|
||||
def get_system(self, system_type: SystemType) -> System:
|
||||
return self.systems.get(system_type)
|
||||
|
||||
async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
|
||||
for system in self.systems.values():
|
||||
await system.update(entity_manager, delta_time)
|
|
@ -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)
|
|
@ -0,0 +1,80 @@
|
|||
from kennel.engine.game import Game
|
||||
from kennel.engine.components.component import ComponentType
|
||||
from kennel.engine.entities.entity import EntityManager, Entity
|
||||
from kennel.engine.entities.laser import Laser
|
||||
from kennel.engine.systems.system import SystemManager
|
||||
from kennel.engine.systems.network import (
|
||||
NetworkSystem,
|
||||
EventProcessor,
|
||||
ClientEventTransformer,
|
||||
Event,
|
||||
EventType,
|
||||
)
|
||||
from kennel.engine.systems.world import WorldSystem
|
||||
from kennel.config import config
|
||||
from .app import logger
|
||||
from typing import List
|
||||
import uuid
|
||||
|
||||
entity_manager = EntityManager()
|
||||
system_manager = SystemManager()
|
||||
|
||||
|
||||
class KennelEventProcessor(EventProcessor):
|
||||
def accept(self, entity_manager: EntityManager, event: Event) -> None:
|
||||
if event.event_type == EventType.ENTITY_POSITION_UPDATE:
|
||||
self._process_entity_position_update(entity_manager, event)
|
||||
|
||||
def _process_entity_position_update(
|
||||
self, entity_manager: EntityManager, event: Event
|
||||
) -> None:
|
||||
entity = entity_manager.get_entity(event.data["entity_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")
|
||||
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']}"
|
||||
)
|
||||
return
|
||||
position = entity.get_component(ComponentType.POSITION)
|
||||
if position is None:
|
||||
logger.error(f"Entity {entity} has no position")
|
||||
return
|
||||
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()),
|
||||
)
|
||||
|
||||
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)
|
||||
return [laser]
|
|
@ -0,0 +1,45 @@
|
|||
from datetime import datetime
|
||||
from typing import List
|
||||
import requests
|
||||
|
||||
|
||||
class KennelCat:
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
user_id: str,
|
||||
name: str,
|
||||
link: str,
|
||||
description: str,
|
||||
spritesheet: str,
|
||||
created_at: datetime,
|
||||
):
|
||||
self.id = id
|
||||
self.user_id = user_id
|
||||
self.name = name
|
||||
self.link = link
|
||||
self.description = description
|
||||
self.spritesheet = spritesheet
|
||||
self.created_at = created_at
|
||||
|
||||
@staticmethod
|
||||
def from_dict(dict: dict) -> "KennelCat":
|
||||
return KennelCat(
|
||||
id=dict["id"],
|
||||
user_id=dict["user_id"],
|
||||
name=dict["name"],
|
||||
link=dict["link"],
|
||||
description=dict["description"],
|
||||
spritesheet=dict["spritesheet"],
|
||||
created_at=datetime.fromisoformat(dict["created_at"]),
|
||||
)
|
||||
|
||||
|
||||
class KennelCatService:
|
||||
def __init__(self, endpoint: str):
|
||||
self.endpoint = endpoint
|
||||
|
||||
def get_kennel(self) -> List[KennelCat]:
|
||||
response = requests.get(f"{self.endpoint}/kennel")
|
||||
response.raise_for_status()
|
||||
return [KennelCat.from_dict(cat) for cat in response.json()]
|
157
kennel/main.py
157
kennel/main.py
|
@ -1,57 +1,128 @@
|
|||
from fastapi import (
|
||||
FastAPI,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
WebSocketException,
|
||||
status,
|
||||
Cookie,
|
||||
Depends,
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from kennel.engine.systems.system import SystemType
|
||||
from kennel.engine.systems.network import Event, Publishable, EventType
|
||||
from typing import Annotated, Optional
|
||||
from kennel.kennel import (
|
||||
kennel,
|
||||
system_manager,
|
||||
entity_manager,
|
||||
create_session_controllable_entities,
|
||||
)
|
||||
from kennel.app import app, templates, logger
|
||||
from kennel.kennelcats import KennelCatService
|
||||
from kennel.middleware import logger_middleware
|
||||
from kennel.config import config
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
import structlog
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
app = FastAPI(
|
||||
servers = [
|
||||
{"url": "https://kennel.hatecomputers.club", "description": "prod"}
|
||||
]
|
||||
)
|
||||
logger = structlog.get_logger()
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def logger_middleware(request: Request, call_next):
|
||||
structlog.contextvars.clear_contextvars()
|
||||
structlog.contextvars.bind_contextvars(
|
||||
path=request.url.path,
|
||||
method=request.method,
|
||||
client_host=request.client.host,
|
||||
request_id=str(uuid.uuid4()),
|
||||
)
|
||||
response = await call_next(request)
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
logger.info("Starting Kennel...")
|
||||
loop.create_task(kennel.run())
|
||||
|
||||
structlog.contextvars.bind_contextvars(
|
||||
status_code=response.status_code,
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
logger.info("Stopping Kennel...")
|
||||
kennel.stop()
|
||||
loop.stop()
|
||||
logger.info("Kennel stopped")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request):
|
||||
return templates.TemplateResponse(request=request, name="index.html")
|
||||
|
||||
|
||||
@app.get("/assign")
|
||||
async def assign(
|
||||
response: Response,
|
||||
session: Annotated[Optional[str], Cookie()] = None,
|
||||
):
|
||||
if session is None:
|
||||
session = str(uuid.uuid4().hex)
|
||||
response.set_cookie(key="session", value=session, max_age=config.COOKIE_MAX_AGE)
|
||||
return {"session": session}
|
||||
|
||||
|
||||
async def get_cookie_or_token(
|
||||
websocket: WebSocket,
|
||||
session: Annotated[str | None, Cookie()] = None,
|
||||
):
|
||||
if session is None:
|
||||
raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return session
|
||||
|
||||
|
||||
class WebSocketClient(Publishable):
|
||||
def __init__(self, websocket: WebSocket):
|
||||
self.websocket = websocket
|
||||
|
||||
async def publish(self, event: Event):
|
||||
await self.websocket.send_json(event.dict())
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
session: Annotated[str, Depends(get_cookie_or_token)],
|
||||
):
|
||||
await websocket.accept()
|
||||
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(),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Exclude /healthcheck endpoint from producing logs
|
||||
if request.url.path != "/healthcheck":
|
||||
if 400 <= response.status_code < 500:
|
||||
logger.warn("Client error")
|
||||
elif response.status_code >= 500:
|
||||
logger.error("Server error")
|
||||
else:
|
||||
logger.info("OK")
|
||||
session_entities = create_session_controllable_entities(session)
|
||||
try:
|
||||
network_system = system_manager.get_system(SystemType.NETWORK)
|
||||
if network_system is None:
|
||||
raise "Network system not found"
|
||||
|
||||
return response
|
||||
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}))
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
network_system.add_client(session, WebSocketClient(websocket))
|
||||
while True:
|
||||
message = await websocket.receive_json()
|
||||
network_system.client_event(session, Event.from_dict(message))
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket exception {e}")
|
||||
finally:
|
||||
logger.info("Websocket connection closed")
|
||||
for entity in session_entities:
|
||||
logger.info(f"Removing entity {entity.id} for session {session}")
|
||||
entity_manager.remove_entity(entity.id)
|
||||
network_system.add_event(Event(EventType.ENTITY_DEATH, {"id": entity.id}))
|
||||
|
||||
network_system.remove_client(session)
|
||||
await websocket.close()
|
||||
|
||||
|
||||
@app.get("/healthcheck")
|
||||
async def healthcheck():
|
||||
return Response("hello")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def read_main(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request=request, name="index.html"
|
||||
)
|
||||
|
||||
app.mount("/static", StaticFiles(directory = "static"), name = "static")
|
||||
|
||||
return Response("healthy")
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import uuid
|
||||
import structlog
|
||||
from fastapi import Request, Response
|
||||
from .app import app, logger
|
||||
|
||||
@app.middleware("http")
|
||||
async def logger_middleware(request: Request, call_next):
|
||||
structlog.contextvars.clear_contextvars()
|
||||
structlog.contextvars.bind_contextvars(
|
||||
path=request.url.path,
|
||||
method=request.method,
|
||||
client_host=request.client.host,
|
||||
request_id=str(uuid.uuid4()),
|
||||
)
|
||||
response = await call_next(request)
|
||||
|
||||
structlog.contextvars.bind_contextvars(
|
||||
status_code=response.status_code,
|
||||
)
|
||||
|
||||
# Exclude /healthcheck endpoint from producing logs
|
||||
if request.url.path != "/healthcheck":
|
||||
if 400 <= response.status_code < 500:
|
||||
logger.warn("Client error")
|
||||
elif response.status_code >= 500:
|
||||
logger.error("Server error")
|
||||
else:
|
||||
logger.info("OK")
|
||||
|
||||
return response
|
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,9 @@ fastapi = "^0.108.0"
|
|||
structlog = "^23.2.0"
|
||||
uvicorn = { extras = ["standard"], version = "^0.25.0" }
|
||||
jinja2 = "^3.1.4"
|
||||
python-dotenv = "^1.0.1"
|
||||
websockets = "^12.0"
|
||||
requests = "^2.32.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.1.9"
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
window.onload = () => {
|
||||
console.log('from js');
|
||||
const kennelWindowEle = document.querySelector('#kennel-window');
|
||||
kennelWindowEle.innerHTML = 'rendered from static/index.js';
|
||||
}
|
||||
const ws = new WebSocket("/ws");
|
||||
ws.onopen = () => {
|
||||
console.log("connected");
|
||||
};
|
||||
ws.onmessage = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
ws.onclose = (e) => {
|
||||
console.log("disconnected", e);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Kennel Club</title>
|
||||
<script src='https://unpkg.com/panzoom@9.4.0/dist/panzoom.min.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="kennel-window"></div>
|
||||
<script src="{{ url_for('static', path='/index.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
Loading…
Reference in New Issue