supergames-env / simulator.py
pvs333's picture
initial
0681dc4
Raw
History Blame Contribute Delete
5.66 kB
"""
Simulator for the Supergames Environment.
Given an action from the agent, simulates a sprint and returns the results.
"""
from typing import List, Tuple
import copy
from models import Game, WorkItem, WorkType, StaffPool, SupergamesAction
DAYS_PER_SPRINT = 10 # each staff member contributes 10 days per sprint
def applyAssignments(
workQueue: List[WorkItem],
action: SupergamesAction,
staffPool: StaffPool,
) -> Tuple[List[WorkItem], str]:
"""
Apply the agent's staff assignments to work items.
Returns updated work queue and a message if something went wrong.
"""
# check total staff requested doesn't exceed available
totalRequested = sum(a.staff for a in action.assignments)
if totalRequested > staffPool.available:
return workQueue, f"Overallocation: requested {totalRequested}, only {staffPool.available} available"
# build a quick lookup so we don't loop the whole queue for every assignment
queueMap = {item.id: item for item in workQueue}
for assignment in action.assignments:
item = queueMap.get(assignment.workItemID)
if item is None:
return workQueue, f"Unknown work item: {assignment.workItemID}"
# add progress to the item
daysContributed = assignment.staff * DAYS_PER_SPRINT
item.daysWorked += daysContributed
item.lastSprintStaff = assignment.staff
return list(queueMap.values()), "ok"
def getCompletedItems(workQueue: List[WorkItem]) -> Tuple[List[WorkItem], List[WorkItem]]:
"""
Split the work queue into completed and remaining items.
An item is complete when daysWorked >= effort.
"""
completedItems = []
remainingItems = []
for item in workQueue:
if item.daysWorked >= item.effort:
completedItems.append(item)
else:
remainingItems.append(item)
return completedItems, remainingItems
def updateChurn(
games: List[Game],
remainingQueue: List[WorkItem],
completedItems: List[WorkItem],
) -> List[Game]:
"""
Update churn multiplier for each game based on unresolved bugs.
- Unresolved CRITICAL/BLOCKER bugs increase churnMult
- Completing a bug reduces churnMult by its churnReduction value
"""
games = copy.deepcopy(games)
completedIds = {item.id for item in completedItems}
for game in games:
# count unresolved critical/blocker bugs for this game
badBugs = [
item for item in remainingQueue
if item.gameId == game.id
and item.workType == WorkType.BUG
and item.severity == 4
and item.id not in completedIds
]
veryBadBugs = [
item for item in remainingQueue
if item.gameId == game.id
and item.workType == WorkType.BUG
and item.severity == 5
and item.id not in completedIds
]
# each unresolved bad bug adds 0.15 to churn multiplier
game.churnMult = 1.0 + (len(badBugs) * 0.15) + (len(veryBadBugs) * 0.25)
# completed bugs reduce churn
for item in completedItems:
if item.gameId == game.id and item.workType == WorkType.BUG:
game.churnMult = max(1.0, game.churnMult - item.churnReduction)
return games
def calculateRevenue(
games: List[Game],
completedItems: List[WorkItem],
currentStep: int,
) -> float:
"""
Calculate total portfolio revenue for this sprint.
For each game:
base = monthlyRevenue * (1 - churnRate * churnMult)
+ revenueImpact of completed items (adjusted for impactDelay)
"""
total = 0.0
for game in games:
# base revenue shrinks if churn is high
base = game.monthlyRevenue * (1.0 - game.churnRate * game.churnMult)
# add revenue from completed items
for item in completedItems:
if item.gameId != game.id:
continue
if item.impactDelay == 0:
total += item.revenueImpact
elif item.impactDelay == 1:
total += item.revenueImpact * 0.5 # half now, half next sprint
# impactDelay == 2 means no impact yet this sprint
total += base
return round(total, 2)
def simulateSprint(
games: List[Game],
workQueue: List[WorkItem],
staffPool: StaffPool,
action: SupergamesAction,
currentStep: int,
) -> Tuple[List[Game], List[WorkItem], List[WorkItem], float, str]:
"""
Run one sprint of simulation.
Returns:
updatedGames - games with updated churn multipliers
remainingQueue - work items not yet complete
completedItems - work items finished this sprint
sprintRevenue - total revenue earned this sprint
message - what happened (for observation)
"""
# step 1 - apply assignments, update daysWorked
workQueue, message = applyAssignments(workQueue, action, staffPool)
if message != "ok":
return games, workQueue, [], 0.0, message
# step 2 - figure out what got completed
completedItems, remainingItems = getCompletedItems(workQueue)
# step 3 - update churn based on what's still unresolved
updatedGames = updateChurn(games, remainingItems, completedItems)
# step 4 - calculate revenue
sprintRevenue = calculateRevenue(updatedGames, completedItems, currentStep)
message = (
f"Completed {len(completedItems)} items: "
f"{', '.join(i.title for i in completedItems) or 'none'}. "
f"Revenue: ${sprintRevenue}"
)
return updatedGames, remainingItems, completedItems, sprintRevenue, message