From 9c0d52421527f2440e7c1e8403dbe58794e398b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Sat, 1 Feb 2025 18:41:15 +0100 Subject: [PATCH] add metrics collection; integrate metrics aggregator and update dev configuration for metrics settings --- .../src/screens/play/dev/dev-config-panel.tsx | 118 +++++++++- .../src/screens/play/dev/dev-config.ts | 19 +- .../play/game-world/game-world-update.ts | 3 + .../definitions/lion-prey-interaction.ts | 5 +- .../states/prey/prey-fleeing-state.ts | 4 + .../src/utils/metrics/metrics-aggregator.ts | 217 ++++++++++++++++++ 6 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 games/hungry-lion/src/utils/metrics/metrics-aggregator.ts diff --git a/games/hungry-lion/src/screens/play/dev/dev-config-panel.tsx b/games/hungry-lion/src/screens/play/dev/dev-config-panel.tsx index b08d1d25..787d9db7 100644 --- a/games/hungry-lion/src/screens/play/dev/dev-config-panel.tsx +++ b/games/hungry-lion/src/screens/play/dev/dev-config-panel.tsx @@ -32,6 +32,18 @@ export function DevConfigPanel() { setConfig(newConfig); }; + const handleNumberChange = (key: keyof DevConfig, value: string) => { + const numValue = parseInt(value, 10); + if (!isNaN(numValue)) { + const newConfig = { + ...config, + [key]: numValue, + }; + setDevConfig(newConfig); + setConfig(newConfig); + } + }; + return ( Dev Configuration @@ -39,18 +51,58 @@ export function DevConfigPanel() {
Debug Options - handleToggle('debugVitals')} /> + handleToggle('debugVitals')} + /> Debug vitals + + handleToggle('debugStateMachine')} + /> + Debug state + +
+ +
+ Metrics Collection + + handleToggle('metricsEnabled')} + /> + Enable metrics collection + + + Time window (ms): + handleNumberChange('metricsTimeWindow', e.target.value)} + min="1000" + max="60000" + step="1000" + disabled={!config.metricsEnabled} + /> + + + Max payload size: + handleNumberChange('metricsMaxPayloadSize', e.target.value)} + min="512" + max="8192" + step="512" + disabled={!config.metricsEnabled} + /> +
- - handleToggle('debugStateMachine')} - /> - Debug state -
); } @@ -112,6 +164,47 @@ const ToggleContainer = styled.label` } `; +const InputContainer = styled.div` + display: flex; + align-items: center; + margin-bottom: 8px; + font-size: 12px; + + &:last-child { + margin-bottom: 0; + } +`; + +const InputLabel = styled.label` + flex: 1; + margin-right: 8px; + color: rgba(255, 255, 255, 0.9); +`; + +const NumberInput = styled.input` + width: 80px; + padding: 4px; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 3px; + background: transparent; + color: white; + font-size: 12px; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &:focus { + outline: none; + border-color: #4a9eff; + } + + &:hover:not(:disabled) { + border-color: #4a9eff; + } +`; + const ToggleInput = styled.input` margin-right: 8px; cursor: pointer; @@ -143,4 +236,9 @@ const ToggleInput = styled.input` &:hover { border-color: #4a9eff; } -`; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +`; \ No newline at end of file diff --git a/games/hungry-lion/src/screens/play/dev/dev-config.ts b/games/hungry-lion/src/screens/play/dev/dev-config.ts index cb56377e..5b8d969f 100644 --- a/games/hungry-lion/src/screens/play/dev/dev-config.ts +++ b/games/hungry-lion/src/screens/play/dev/dev-config.ts @@ -5,8 +5,13 @@ export interface DevConfig { // Setting to enable debug rendering of vital information debugVitals: boolean; - // Setin to enable state machine debugging + // Setting to enable state machine debugging debugStateMachine: boolean; + + // Metrics collection settings + metricsEnabled: boolean; + metricsTimeWindow: number; + metricsMaxPayloadSize: number; } /** @@ -15,6 +20,10 @@ export interface DevConfig { const DEFAULT_CONFIG: DevConfig = { debugVitals: false, debugStateMachine: false, + // Default metrics settings + metricsEnabled: false, + metricsTimeWindow: 10000, // 10 seconds + metricsMaxPayloadSize: 2048, // characters }; /** @@ -38,9 +47,15 @@ function deserializeConfig(hash: string): Partial { const params = new URLSearchParams(match[1]); const config: Partial = {}; + params.forEach((value, key) => { if (key in DEFAULT_CONFIG) { - config[key as keyof DevConfig] = value === 'true'; + if (key === 'metricsTimeWindow' || key === 'metricsMaxPayloadSize') { + config[key] = Number(value); + } else { + const boolKey = key as keyof Omit; + config[boolKey] = value === 'true'; + } } }); return config; diff --git a/games/hungry-lion/src/screens/play/game-world/game-world-update.ts b/games/hungry-lion/src/screens/play/game-world/game-world-update.ts index d5ae9061..9672ec3c 100644 --- a/games/hungry-lion/src/screens/play/game-world/game-world-update.ts +++ b/games/hungry-lion/src/screens/play/game-world/game-world-update.ts @@ -1,4 +1,5 @@ import { createEntities, entitiesUpdate } from './entities/entities-update'; +import { metricsAggregator } from '../../../utils/metrics/metrics-aggregator'; import { environmentInit, environmentUpdate } from './environment/environment-update'; import { GameWorldState, UpdateContext } from './game-world-types'; import { interactionsUpdate } from './interactions/interactions-update'; @@ -15,6 +16,8 @@ export function gameWorldUpdate(updateContext: UpdateContext): GameWorldState { environmentUpdate(updateContext); updateContext.gameState.time += updateContext.deltaTime; + + metricsAggregator.aggregateAndSend(); return updateContext.gameState; } diff --git a/games/hungry-lion/src/screens/play/game-world/interactions/definitions/lion-prey-interaction.ts b/games/hungry-lion/src/screens/play/game-world/interactions/definitions/lion-prey-interaction.ts index 69c7fb06..31947881 100644 --- a/games/hungry-lion/src/screens/play/game-world/interactions/definitions/lion-prey-interaction.ts +++ b/games/hungry-lion/src/screens/play/game-world/interactions/definitions/lion-prey-interaction.ts @@ -1,6 +1,7 @@ import { InteractionDefinition } from '../interactions-types'; import { vectorDistance, vectorNormalize, vectorSubtract, vectorScale } from '../../utils/math-utils'; -import { PreyEntity } from '../../entities/entities-types'; +import { metricsAggregator } from '../../../../../utils/metrics/metrics-aggregator'; +import { LionEntity, PreyEntity } from '../../entities/entities-types'; const HEALTH_DECREMENT = 1; const FORCE_STRENGTH = 0.005; @@ -19,8 +20,10 @@ export const LION_PREY_INTERACTION: InteractionDefinition = { }, perform: (source, target, updateContext) => { + const lion = source as LionEntity; const prey = target as PreyEntity; + metricsAggregator.recordCatchEvent(lion.hungerLevel); // Reduce prey health prey.health = Math.max(prey.health - HEALTH_DECREMENT, 0); diff --git a/games/hungry-lion/src/screens/play/game-world/state-machine/states/prey/prey-fleeing-state.ts b/games/hungry-lion/src/screens/play/game-world/state-machine/states/prey/prey-fleeing-state.ts index 2289eb04..36334d0f 100644 --- a/games/hungry-lion/src/screens/play/game-world/state-machine/states/prey/prey-fleeing-state.ts +++ b/games/hungry-lion/src/screens/play/game-world/state-machine/states/prey/prey-fleeing-state.ts @@ -1,4 +1,5 @@ import { PreyEntity } from '../../../entities/entities-types'; +import { metricsAggregator } from '../../../../../../utils/metrics/metrics-aggregator'; import { BaseStateData, State } from '../../state-machine-types'; import { shouldFlee, updateFleeing } from './prey-state-utils'; @@ -18,6 +19,9 @@ export const PREY_FLEEING_STATE: State = { update: (data, context) => { const { entity } = context; const currentTime = context.updateContext.gameState.time; + if (data.enteredAt === currentTime) { + metricsAggregator.recordFleeEvent(); + } const timeInState = currentTime - data.enteredAt; // Only consider stopping fleeing after minimum flee time diff --git a/games/hungry-lion/src/utils/metrics/metrics-aggregator.ts b/games/hungry-lion/src/utils/metrics/metrics-aggregator.ts new file mode 100644 index 00000000..256aece6 --- /dev/null +++ b/games/hungry-lion/src/utils/metrics/metrics-aggregator.ts @@ -0,0 +1,217 @@ +import { devConfig } from '../../screens/play/dev/dev-config'; +import { updateAppContext } from '../genaicode-context'; + +/** + * Raw metrics data collected during gameplay + */ +interface RawMetrics { + /** Time when the metrics were collected */ + timestamp: number; + /** Type of event that generated these metrics */ + type: 'flee' | 'catch'; + /** Data specific to the event type */ + data: { + /** Lion's hunger level at the time of catch (only for catch events) */ + hungerLevel?: number; + }; +} + +/** + * Aggregated metrics data ready to be sent to GenAIcode + */ +interface AggregatedMetrics { + /** Time window start timestamp */ + startTime: number; + /** Time window end timestamp */ + endTime: number; + /** Number of flee events in the time window */ + fleeCount: number; + /** Number of catch events in the time window */ + catchCount: number; + /** Average lion hunger level at catch during the time window */ + avgHungerAtCatch: number; + /** Catch success rate (catches / flee events) */ + catchRate: number; +} + +/** + * Circular buffer implementation for storing metrics data + */ +class CircularBuffer { + private buffer: T[]; + private start: number = 0; + private size: number = 0; + + constructor(private capacity: number) { + this.buffer = new Array(capacity); + } + + /** + * Add an item to the buffer + */ + push(item: T): void { + const index = (this.start + this.size) % this.capacity; + this.buffer[index] = item; + + if (this.size < this.capacity) { + this.size++; + } else { + // Buffer is full, move start pointer + this.start = (this.start + 1) % this.capacity; + } + } + + /** + * Get all items in the buffer + */ + getItems(): T[] { + const items: T[] = []; + for (let i = 0; i < this.size; i++) { + const index = (this.start + i) % this.capacity; + items.push(this.buffer[index]); + } + return items; + } + + /** + * Clear the buffer + */ + clear(): void { + this.start = 0; + this.size = 0; + } +} + +/** + * Class responsible for collecting and aggregating gameplay metrics + */ +class MetricsAggregator { + /** Buffer for storing raw metrics data */ + private metricsBuffer: CircularBuffer; + /** Last time metrics were sent to GenAIcode */ + private lastSendTime: number = 0; + /** Default buffer capacity (store up to 1000 events) */ + private readonly DEFAULT_BUFFER_CAPACITY = 1000; + /** Flag to track initialization status */ + private initialized: boolean = false; + + constructor() { + this.metricsBuffer = new CircularBuffer(this.DEFAULT_BUFFER_CAPACITY); + } + + /** + * Initialize the aggregator with the current time + */ + private initialize(currentTime: number): void { + if (!this.initialized) { + this.lastSendTime = currentTime; + this.initialized = true; + } + } + + /** + * Record a flee event + */ + recordFleeEvent(): void { + if (!devConfig.metricsEnabled) return; + + const currentTime = performance.now(); + this.initialize(currentTime); + + this.metricsBuffer.push({ + timestamp: currentTime, + type: 'flee', + data: {} + }); + } + + /** + * Record a catch event + */ + recordCatchEvent(hungerLevel: number): void { + if (!devConfig.metricsEnabled) return; + + const currentTime = performance.now(); + this.initialize(currentTime); + + this.metricsBuffer.push({ + timestamp: currentTime, + type: 'catch', + data: { + hungerLevel + } + }); + } + + /** + * Aggregate metrics from the buffer within the specified time window + */ + private aggregateMetrics(startTime: number, endTime: number): AggregatedMetrics { + const items = this.metricsBuffer.getItems() + .filter(item => item.timestamp >= startTime && item.timestamp <= endTime); + + const fleeEvents = items.filter(item => item.type === 'flee'); + const catchEvents = items.filter(item => item.type === 'catch'); + + const hungerLevels = catchEvents + .map(event => event.data.hungerLevel) + .filter((level): level is number => level !== undefined); + + const avgHunger = hungerLevels.length > 0 + ? hungerLevels.reduce((sum, level) => sum + level, 0) / hungerLevels.length + : 0; + + return { + startTime, + endTime, + fleeCount: fleeEvents.length, + catchCount: catchEvents.length, + avgHungerAtCatch: avgHunger, + catchRate: fleeEvents.length > 0 ? catchEvents.length / fleeEvents.length : 0 + }; + } + + /** + * Check if the payload size is within limits + */ + private checkPayloadSize(metrics: AggregatedMetrics): boolean { + const payload = JSON.stringify(metrics); + return payload.length <= devConfig.metricsMaxPayloadSize; + } + + /** + * Aggregate and send metrics to GenAIcode if the time window has elapsed + */ + async aggregateAndSend(): Promise { + if (!devConfig.metricsEnabled || !this.initialized) return; + + const currentTime = performance.now(); + const timeWindow = devConfig.metricsTimeWindow; + + // Check if it's time to send metrics + if (currentTime - this.lastSendTime >= timeWindow) { + try { + // Aggregate metrics for the time window + const metrics = this.aggregateMetrics(this.lastSendTime, currentTime); + + // Check payload size + if (!this.checkPayloadSize(metrics)) { + console.warn('Metrics payload size exceeds limit, skipping...'); + return; + } + + // Send metrics to GenAIcode + await updateAppContext('gameMetrics', JSON.stringify(metrics)); + + // Update last send time and clear old data + this.lastSendTime = currentTime; + this.metricsBuffer.clear(); + } catch (error) { + console.error('Failed to send metrics to GenAIcode:', error); + } + } + } +} + +// Export a singleton instance +export const metricsAggregator = new MetricsAggregator(); \ No newline at end of file