Compare commits

..

1 Commits

Author SHA1 Message Date
Elizabeth Hunt 8ec7f53682 get "cats" up there
continuous-integration/drone/pr Build is failing Details
2024-09-12 17:23:30 -07:00
15 changed files with 178 additions and 16 deletions

View File

@ -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()

View File

@ -5,6 +5,7 @@ from enum import Enum
class ComponentType(str, Enum):
POSITION = "POSITION"
CONTROLLABLE = "CONTROLLABLE"
MARKOV = "MARKOV"
class Component:

View File

@ -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)}

View File

@ -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

View File

@ -6,6 +6,7 @@ from kennel.engine.components.component import Component, ComponentType
class EntityType(str, Enum):
LASER = "LASER"
CAT = "CAT"
class Entity:

View File

@ -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

View File

@ -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

View File

@ -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]:

View File

@ -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()]

View File

@ -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")

View File

@ -4,10 +4,9 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"watch": "./node_modules/nodemon/bin/nodemon.js --watch './src/**/*' -e ts,html --exec \"npm run build\""
"dev": "./node_modules/nodemon/bin/nodemon.js --watch './src/**/*' -e ts,html --exec \"npm run build\""
},
"devDependencies": {
"@rollup/plugin-inject": "^5.0.5",

View File

@ -1,12 +1,14 @@
import {
Component,
ComponentType,
PositionComponent,
RenderableComponent,
TrailingPositionComponent,
} from "./component";
export enum EntityType {
LASER = "LASER",
CAT = "CAT",
}
export interface Entity {
@ -28,3 +30,16 @@ export const create_laser = (base: Entity) => {
base.components[ComponentType.RENDERABLE] = renderable;
return base;
};
export const create_cat = (base: Entity) => {
const renderable: RenderableComponent = {
name: ComponentType.RENDERABLE,
};
base.components[ComponentType.RENDERABLE] = renderable;
base.components[ComponentType.POSITION] = {
component: ComponentType.POSITION,
x: Math.random() * 1_000,
y: Math.random() * 1_000,
} as unknown as PositionComponent; // TODO: hack
return base;
};

View File

@ -3,7 +3,7 @@ import {
PositionComponent,
TrailingPositionComponent,
} from "./component";
import { create_laser, Entity, EntityType } from "./entity";
import { create_cat, create_laser, Entity, EntityType } from "./entity";
import {
EntityBornEvent,
EntityDeathEvent,
@ -110,6 +110,9 @@ export class NetworkSystem extends System {
if (entity.entity_type === EntityType.LASER) {
return create_laser(entity);
}
if (entity.entity_type === EntityType.CAT) {
return create_cat(entity);
}
return entity;
}

View File

@ -1,4 +1,8 @@
import { ComponentType, TrailingPositionComponent } from "./component";
import {
ComponentType,
PositionComponent,
TrailingPositionComponent,
} from "./component";
import { Game } from "./game";
import { System, SystemType } from "./system";
import { drawLaserPen } from "laser-pen";
@ -25,6 +29,17 @@ export class RenderSystem extends System {
] as TrailingPositionComponent;
if (trailing_position.trails.length < 3) return;
drawLaserPen(ctx, trailing_position.trails);
return;
}
if (ComponentType.POSITION in entity.components) {
const position = entity.components[
ComponentType.POSITION
] as PositionComponent;
ctx.beginPath();
ctx.arc(position.x, position.y, 50, 0, 2 * Math.PI);
ctx.stroke();
return;
}
});
}

View File

@ -28,11 +28,11 @@ $(async () => {
const publisher: EventPublisher = new WebsocketEventPublisher(ws);
const network_system = new NetworkSystem(queue, publisher);
const gamecanvas = $("#gamecanvas").get(0)!;
const gamecanvas = $("#gamecanvas").get(0)! as HTMLCanvasElement;
const input_system = new InputSystem(publisher, gamecanvas);
setDelay(1_000);
const render_system = new RenderSystem(gamecanvas as HTMLCanvasElement);
setDelay(500);
const render_system = new RenderSystem(gamecanvas);
const trailing_position = new TrailingPositionSystem(drainPoints);