import type { MapMetrics } from './metrics.js'; import { metricDuration, metricPercent } from './ui/metrics.js'; /** Product hypotheses for advisory experiments; these are not demand forecasts. */ export const HOSTING_RULES = { pressureExpansion: 0.2, occupancyExpansion: 0.8, idleUtilization: 0.1, quietPressure: 0.01, minimumEvidenceHours: 1 } as const; export interface MetricRecommendation { game: string; map: string; kind: 'expand' | 'consolidate' | 'capacity' | 'watch'; title: string; reason: string; evidence: string; outcome: string; tentative: boolean; } /** Apply the same visible rules to all selected maps, including sparse observations. */ export function metricRecommendations(maps: MapMetrics[]): MetricRecommendation[] { return maps.map(map => { const utilization = map.observed > 0 ? map.active / map.observed : null; const pressure = map.capacityObserved > 0 ? map.full / map.capacityObserved : null; const knownShare = map.observed + map.unknown > 0 ? map.observed / (map.observed + map.unknown) : 0; const base = { game: map.game, map: map.map, evidence: `${metricDuration(map.active)} with players / ${metricDuration(map.observed)} known server time (${metricPercent(map.active, map.observed)}). ` + (pressure === null ? 'Capacity unavailable.' : `All lobbies full ${metricDuration(map.full)} / ${metricDuration(map.capacityObserved)} known map time (${metricPercent(map.full, map.capacityObserved)}).`), tentative: map.observed < HOSTING_RULES.minimumEvidenceHours || pressure === null || map.capacityObserved < HOSTING_RULES.minimumEvidenceHours || knownShare < 0.8 }; if (utilization === null) return { ...base, kind: 'capacity' as const, title: map.unknown > 0 ? 'Check occupancy reporting' : 'Let observations accumulate', reason: map.unknown > 0 ? 'Captured status is unknown. Check a server’s telemetry or RCON diagnostics before choosing a hosting change.' : 'This map has no elapsed observations in the selected window. Wait for another status check or try a wider range.', outcome: 'Watch for known server time in the next report.', tentative: true }; if (pressure !== null && pressure >= HOSTING_RULES.pressureExpansion) return { ...base, kind: 'expand' as const, title: 'Trial one additional lobby', reason: 'Recorded lobbies were all full for at least 20% of known capacity time. Try one extra lobby during busy hours.', outcome: 'Compare all-full time and use of the additional lobby over the next comparable period.' }; if (pressure === null && utilization >= HOSTING_RULES.occupancyExpansion) return { ...base, kind: 'expand' as const, title: 'Trial an extra lobby during busy hours', reason: 'Players were present for at least 80% of known server time. Capacity is unknown, so this experiment uses occupancy as a proxy, not evidence of saturation.', outcome: 'Check whether players use the extra lobby, and measure capacity if the server reports it.', tentative: true }; if (utilization <= HOSTING_RULES.idleUtilization && (pressure === null || pressure < HOSTING_RULES.quietPressure)) return { ...base, kind: 'consolidate' as const, title: 'Trial less idle hosting', reason: 'Players were present for at most 10% of known time, with less than 1% measured pressure or unavailable capacity. Review one idle lobby or a shorter hosting window; preserve access to this map.', outcome: 'Watch for increased all-full time or loss of player-hours, and restore the lineup if the trial hurts availability.' }; if (pressure === null) return { ...base, kind: 'capacity' as const, title: 'Check capacity during busy hours', reason: 'Occupancy is recorded, but player limits are unavailable. Check capacity reporting while keeping the current lineup.', outcome: 'Look for known-capacity time and all-full periods before the next hosting trial.' }; return { ...base, kind: 'watch' as const, title: 'Keep the lineup and watch peaks', reason: 'These observations do not trigger the expansion or idle-hosting rules. Keep the current lineup for another comparison period.', outcome: 'Watch whether occupied time and measured capacity pressure change.' }; }).sort((a, b) => { const priority = { expand: 0, consolidate: 1, capacity: 2, watch: 3 }; return priority[a.kind] - priority[b.kind] || a.map.localeCompare(b.map) || a.game.localeCompare(b.game); }); }