in infrastructure/load-balancing-for-inference/components/Play.tsx [152:582]
export default function Play() {
// New GPU positions for each player.
const player1GpuPositions = [-4.90, -3.69, -2.52, -1.31];
const player2GpuPositions = [1.19, 2.43, 3.64, 4.86];
const totalGameTime = 60;
const [robotTargetX, setRobotTargetX] = useState(3.2);
const [isGripping, setIsGripping] = useState(false);
// TODO: Add pseudonyms for users
const [playerOneEnd, setPlayerOneEnd] = useState<[number, number, number]>(
playerEndPositions[0]
);
const [timeElapsed, setTimeElapsed] = useState(-5);
const [quarterSecondCounter, setQuarterSecondCounter] = useState(0);
const [playerName, setPlayerName] = useState("You are the...");
const [failBlocks, setFailBlocks] = useState<FailResultBlock[]>([]);
const [successBlocks, setSuccessBlocks] = useState<ResultBlock[]>([]);
const [vmStats, setVmStats] = useState<VmStats>(defaultVmStats);
const [playerTwoTotal, setTotalPlayerTwoTotal] = useState(0);
const [gameStarted, setGameStarted] = useState(false);
const [showResults, setShowResults] = useState(false);
const [blocks, setBlocks] = useState<
{
uuid: string;
gpuIndex: number;
stackPosition: number; // NEW: Tracks stacking inside GPU
}[]
>([]);
const userBoxTexture = useLoader(TextureLoader, "/assets/user-box.png");
const userFunnelTexture = useLoader(TextureLoader, "/assets/user-funnel.png");
const yellowDataBlockTexture = useLoader(
TextureLoader,
"/assets/yellow-datablock.png"
);
// ✅ Define Robot Position Variables
const robotGripperPosition: [number, number, number] = [3.0, -0.2, 0]; // Gripper at the end of Arm 2
// ✅ Load Textures
const baseTexture = useLoader(TextureLoader, "/assets/base.png");
const arm1Texture = useLoader(TextureLoader, "/assets/arm1.png");
const arm2Texture = useLoader(TextureLoader, "/assets/arm2.png");
const railTexture = useLoader(TextureLoader, "/assets/rail.png");
const gripperTexture = useLoader(TextureLoader, "/assets/gripper.png");
// Blue side assets
const player2FunnelXPositions = [1.2, 2.4, 3.6, 4.8];
const [player2FunnelX, setPlayer2FunnelX] = useState(player2FunnelXPositions[0]);
const playerTwoActiveRef = useRef<number>(0);
useEffect(() => {
const interval = setInterval(() => {
const randomIndex = Math.floor(Math.random() * player2FunnelXPositions.length);
setPlayer2FunnelX(player2FunnelXPositions[randomIndex]);
}, 500); // update every 0.5 seconds for a faster funnel movement
return () => clearInterval(interval);
}, []);
// ✅ Define Robot Base Position
const robotBasePosition: [number, number, number] = [.0, 1.0, 0];
// ✅ Define Arm Positions Relative to Base
const robotArm1Position: [number, number, number] = [
robotBasePosition[0],
robotBasePosition[1] - 0.5,
0,
];
const robotArm2Position: [number, number, number] = [
robotArm1Position[0],
robotArm1Position[1] - 0.5,
0,
];
// ✅ Define Rail Position
const railPosition: [number, number, number] = [3.0, 0.5, 0];
const userFunnelBaseY = 0.09; // ✅ Base Y-position for funnel (adjust as needed)
// ✅ Define Constants for UserBox & UserFunnel Positioning
const userFunnelBasePosition: [number, number, number] = [
player1VmXPositions[0],
userFunnelBaseY,
0,
]; // ✅ Moves based on keypress
// calculated values based on variables
const [activeGpuIndex, setActiveGpuIndex] = useState(0);
const playerOneMid: [number, number, number] = [
playerOneEnd[0],
playerMidYPosition,
0,
];
const player2NextVmIndex = playerTwoTotal % 4;
const player2NextXPosition = player2VmXPositions[player2NextVmIndex];
const playerTwoEnd: [number, number, number] = [
player2NextXPosition,
vmYPositionBottom,
0,
];
const playerTwoMid: [number, number, number] = [
playerTwoEnd[0],
playerMidYPosition,
0,
];
const playerOneActiveVmIndex = activeGpuIndex; // Use activeGpuIndex instead of finding position
const playerOneAtMaxCapacity =
vmStats.statusArray[playerOneActiveVmIndex].atMaxCapacity;
const playerOneActiveVmId = playerOneActiveVmIndex + 1;
const playerTwoActiveVmIndex = player2VmXPositions.reduce((bestIndex, pos, i) => {
return Math.abs(pos - player2FunnelX) < Math.abs(player2VmXPositions[bestIndex] - player2FunnelX)
? i
: bestIndex;
}, 0);
const playerTwoAtMaxCapacity =
vmStats.statusArray[playerTwoActiveVmIndex + 4].atMaxCapacity;
const playerOneScore = timeElapsed < 1 ? 0 : vmStats.playerOneScore;
const playerTwoScore = timeElapsed < 1 ? 0 : vmStats.playerTwoScore;
const timeRemaining = Math.min(totalGameTime - timeElapsed, totalGameTime);
useEffect(() => {
// add success block to any vm that has more than 0% in the queue
const newSuccessXPosition = [];
// add player one success block
if (vmStats.statusArray[player2NextVmIndex].queue > 0) {
newSuccessXPosition.push(player1VmXPositions[player2NextVmIndex]);
}
// add player two success block
if (vmStats.statusArray[player2NextVmIndex + 4].queue > 0) {
newSuccessXPosition.push(player2VmXPositions[player2NextVmIndex]);
}
const newSuccessBlocks = newSuccessXPosition.map((xPosition) => {
const startingPosition: [number, number, number] = [
xPosition,
vmYPosition + 0.42,
-0.5,
];
const uid = crypto.randomUUID();
return { uid, startingPosition };
});
// limit to 100 blocks to prevent the screen from freezing up
setSuccessBlocks([...newSuccessBlocks, ...successBlocks].slice(0, 20));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [player2NextVmIndex]);
useEffect(() => {
const getStartLoader = async (playerName: string) => {
try {
setPlayerName(playerName);
} catch (error) {
console.error("Failed to start");
}
};
if (!gameStarted && timeElapsed > -2) {
setGameStarted(true);
getStartLoader(playerName);
}
}, [gameStarted, playerName, timeElapsed]);
useEffect(() => {
const getVMStatus = async () => {
if (timeElapsed < totalGameTime + 5) {
// During GET READY (timeElapsed < 0), force GPU utilizations to 0.
if (timeElapsed < 0) {
const resetStats = {
...vmStats,
statusArray: vmStats.statusArray.map((vm) => ({ ...vm, utilization: 0 })),
playerOneScore: 0,
playerTwoScore: 0,
};
setVmStats(resetStats);
return;
}
// Compute active right GPU index based on player2FunnelX:
const playerTwoActiveVmIndex = player2GpuPositions.reduce((bestIndex, pos, i) => {
return Math.abs(pos - player2FunnelX) < Math.abs(player2GpuPositions[bestIndex] - player2FunnelX)
? i
: bestIndex;
}, 0);
// Update rates:
const leftSideIncreaseRate = 0.035;
const leftSideDecreaseRate = 0.01;
const rightSideIncreaseRate = 0.08; // Increased active fill rate for right side
const rightSideDecreaseRate = 0.02; // Decreased drain rate for inactive right GPUs
const updatedVmStats = {
...vmStats,
statusArray: vmStats.statusArray.map((vmDetails, index) => {
if (index < 4) {
// Left side GPUs.
if (index === playerOneActiveVmIndex) {
return {
...vmDetails,
utilization: Math.min(
vmDetails.utilization + leftSideIncreaseRate,
1
),
};
} else {
return {
...vmDetails,
utilization: Math.max(vmDetails.utilization - leftSideDecreaseRate, 0),
};
}
} else {
// Right side GPUs (indices 4–7): compare (index - 4) with playerTwoActiveVmIndex.
if ((index - 4) === playerTwoActiveVmIndex) {
return {
...vmDetails,
utilization: Math.min(vmDetails.utilization + rightSideIncreaseRate, 1),
};
} else {
return {
...vmDetails,
utilization: Math.max(vmDetails.utilization - rightSideDecreaseRate, 0),
};
}
}
}),
playerOneScore:
vmStats.playerOneScore +
vmStats.statusArray.slice(0, 4).reduce((agg, vm) => agg + (vm.utilization > 0 ? 1 : 0), 0),
playerTwoScore:
vmStats.playerTwoScore +
vmStats.statusArray.slice(4).reduce((agg, vm) => agg + (vm.utilization > 0 ? 1 : 0), 0),
playerOneAtMaxCapacity: vmStats.statusArray[playerOneActiveVmIndex].utilization >= 1,
playerTwoAtMaxCapacity:
vmStats.statusArray[playerTwoActiveVmIndex + 4].utilization >= 1,
};
setVmStats(updatedVmStats);
}
};
getVMStatus();
}, [quarterSecondCounter, timeElapsed, player2FunnelX]);
// ✅ Define Constants for UserBox & UserFunnel Positioning
// const userBoxPosition: [number, number, number] = [-2.9, 1.2, 0]; // ✅ Fixed position
// ✅ State to Track Funnel Movement (Moves Horizontally)
const [userFunnelPosition, setUserFunnelPosition] = useState<
[number, number, number]
>([
player1VmXPositions[0], // Starts at GPU 1
userFunnelBaseY, // Using the new base Y position
0,
]);
// Add activeGpuIndex state to track which GPU is currently selected
// Update the handleKeyDown function in your useEffect
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
let newXPosition: number | null = null;
let newGpuIndex: number = activeGpuIndex;
switch (event.code) {
case "Digit1":
newXPosition = player1VmXPositions[0];
newGpuIndex = 0;
break;
case "Digit2":
newXPosition = player1VmXPositions[1];
newGpuIndex = 1;
break;
case "Digit3":
newXPosition = player1VmXPositions[2];
newGpuIndex = 2;
break;
case "Digit4":
newXPosition = player1VmXPositions[3];
newGpuIndex = 3;
break;
default:
break;
}
if (newXPosition !== null) {
setUserFunnelPosition([newXPosition, userFunnelBaseY, 0]);
setActiveGpuIndex(newGpuIndex);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
useEffect(() => {
const playerName = generatePlayerName();
setPlayerName(playerName);
}, []);
useEffect(() => {
//Implementing the setInterval method
const interval = setInterval(() => {
setTimeElapsed(timeElapsed + 1);
}, 1000);
//Clearing the interval
return () => clearInterval(interval);
}, [timeElapsed]);
useEffect(() => {
//Implementing the setInterval method
const fastInterval = setInterval(() => {
setQuarterSecondCounter(quarterSecondCounter + 1);
}, 250);
//Clearing the interval
return () => clearInterval(fastInterval);
}, [quarterSecondCounter]);
useEffect(() => {
if (timeElapsed >= totalGameTime && !showResults) {
setShowResults(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeElapsed]);
const addFailBlock = (
failBlockStartingPosition: [number, number, number]
) => {
const [x, y, z] = failBlockStartingPosition;
if (x < 0) {
const audio = new Audio("/beep.wav");
audio.volume = 0.1;
// audio.play();
} else {
setTotalPlayerTwoTotal(playerTwoTotal + 1);
}
const startingPosition: [number, number, number] = [
x + Math.random() / 4 - 0.1,
y,
z,
];
const uid = crypto.randomUUID();
const side: "LEFT" | "RIGHT" = ["LEFT", "RIGHT"][
Math.floor(Math.random() * 2)
] as "LEFT" | "RIGHT";
const newBlock = { uid, startingPosition, side };
// limit to 100 blocks to prevent the screen from freezing up
setFailBlocks([newBlock, ...failBlocks].slice(0, 10));
};
return (
<main className="flex h-screen flex-col items-center justify-between">
<div className="flex w-full overflow-clip justify-center text-7xl font-mono">
<div
className="flex flex-row transition-all duration-1000 justify-end px-2 text-[#1a212c]"
style={{
height: "80px",
width: `${Math.max(playerOneScore, 50)}rem`,
backgroundColor: colors.player1,
}}
>
<h3 className={`text-7xl font-mono ${jersey15.className}`}>
{playerOneScore.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
</h3>
</div>
<div
className="flex flex-row transition-all duration-1000 justify-start px-2 text-white"
style={{
height: "80px",
width: `${Math.max(playerTwoScore, 50)}rem`,
backgroundColor: colors.player2,
}}
>
<h3 className={`text-7xl font-mono ${jersey15.className}`}>
{playerTwoScore.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
</h3>
</div>
</div>
<Canvas style={{ background: "white" }}>
<ambientLight intensity={Math.PI / 2} />
<pointLight position={[-10, -10, -10]} decay={0} intensity={2} />
<pointLight position={[5, 5, 5]} decay={0} intensity={3} />
<Player1 showResults={false} />
<Player2 showResults={showResults} funnelX={player2FunnelX} />
<GpuGroup
positions={player1GpuPositions}
stats={vmStats.statusArray.slice(0, 4)}
/>
{/* Right side GPUs */}
<GpuGroupRight
positions={player2GpuPositions}
stats={vmStats.statusArray.slice(4, 8)}
/>
</Canvas>
<CountdownOverlay
timeElapsed={timeElapsed}
timeRemaining={timeRemaining}
playerOneScore={playerOneScore}
playerTwoScore={playerTwoScore}
/>
<ResultsOverlay
showResults={showResults}
playerOneScore={playerOneScore}
playerTwoScore={playerTwoScore}
playerName={playerName}
/>
</main>
);
}