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