Spaces:
Sleeping
Sleeping
| """ | |
| 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 |