import React, { useState, useEffect, useCallback } from 'react';
import { RefreshCw, Play, Trophy, Users } from 'lucide-react';
// --- Utility Functions for Simulation ---
/**
* Normalizes an array of numbers so their sum is 1, useful for probability weights.
* @param {number[]} weights Array of raw statistical weights.
* @returns {number[]} Array of normalized probabilities.
*/
const normalizeWeights = (weights) => {
const sum = weights.reduce((a, b) => a + b, 0);
if (sum === 0) return weights.map(() => 1 / weights.length); // Handle division by zero
return weights.map(w => w / sum);
};
/**
* Simulates a single offensive drive or possession.
* The core logic is a weighted choice based on team stats (Offense vs Defense).
* @param {object} offenseStats - The statistics of the team currently on offense.
* @param {object} defenseStats - The statistics of the team currently on defense.
* @returns {number} Points scored on the drive (0, 2, or 3).
*/
const simulateDrive = (offenseStats, defenseStats) => {
// Base offensive score potential (higher is better)
const offenseRating = offenseStats.offense + offenseStats.defense * 0.25; // Offense dominates, but Defense helps with turnovers/field position
const defenseRating = defenseStats.defense + defenseStats.offense * 0.15; // Defense is primary, but Offense helps set up better drives
// Calculate the relative offensive advantage (simplified)
const relativeAdvantage = offenseRating / (offenseRating + defenseRating);
// Define probabilities for outcomes based on relative advantage
// We use a simplified model: 3 points (best), 2 points (good), 0 points (fail)
// Higher relativeAdvantage pushes probabilities towards higher scores.
const baseProbabilities = [
{ score: 3, weight: relativeAdvantage * 0.5 }, // Field Goal or better
{ score: 2, weight: relativeAdvantage * 0.8 }, // Close Range Score
{ score: 0, weight: 1 - relativeAdvantage }, // Turnover/Miss/Punt
];
// Normalize the drive probabilities
const rawWeights = baseProbabilities.map(p => p.weight);
const normalizedProbs = normalizeWeights(rawWeights);
const outcomes = baseProbabilities.map((p, i) => ({
score: p.score,
prob: normalizedProbs[i]
}));
// Select the outcome using the normalized probabilities
let cumulativeProbability = 0;
const rand = Math.random();
for (const outcome of outcomes) {
cumulativeProbability += outcome.prob;
if (rand <= cumulativeProbability) {
return outcome.score;
}
}
// Fallback to 0 if something unexpected happens (shouldn't occur with normalization)
return 0;
};
// --- Component Definition ---
const initialTeamStats = {
name: 'Team A',
offense: 80,
defense: 75,
};
const initialTeamBStats = {
name: 'Team B',
offense: 78,
defense: 77,
};
// Simulation Parameters
const TOTAL_DRIVES = 25; // Number of possessions/drives per team
const App = () => {
const [teamA, setTeamA] = useState(initialTeamStats);
const [teamB, setTeamB] = useState(initialTeamBStats);
const [results, setResults] = useState(null);
const [isSimulating, setIsSimulating] = useState(false);
const [messages, setMessages] = useState([]);
// Load state from localStorage on mount (for persistence)
useEffect(() => {
try {
const storedA = localStorage.getItem('teamA');
const storedB = localStorage.getItem('teamB');
if (storedA) setTeamA(JSON.parse(storedA));
if (storedB) setTeamB(JSON.parse(storedB));
} catch (e) {
console.error("Could not load state from localStorage", e);
}
}, []);
// Save state to localStorage whenever teams change
useEffect(() => {
localStorage.setItem('teamA', JSON.stringify(teamA));
localStorage.setItem('teamB', JSON.stringify(teamB));
}, [teamA, teamB]);
const handleStatChange = (teamSetter, field, value) => {
// Ensure value is an integer between 1 and 100
const numericValue = Math.max(1, Math.min(100, parseInt(value, 10) || 1));
teamSetter(prev => ({
...prev,
[field]: field === 'name' ? value : numericValue
}));
setResults(null); // Clear results on input change
};
const generateMessages = useCallback((scoreA, scoreB) => {
const winner = scoreA > scoreB ? teamA.name : teamB.name;
const loser = scoreA > scoreB ? teamB.name : teamA.name;
const margin = Math.abs(scoreA - scoreB);
let narrative = [];
if (margin === 0) {
narrative.push("An absolute deadlock! The game ends in a rare tie.");
} else if (margin <= 4) {
narrative.push(`An instant classic! ${winner} barely edged out ${loser} in a thrilling, 1-possession game.`);
} else if (margin <= 10) {
narrative.push(`${winner} secured a solid victory, controlling the tempo just enough to keep ${loser} at bay.`);
} else {
narrative.push(`Dominance displayed! ${winner} overpowered ${loser} in a convincing rout.`);
}
// Performance analysis based on input stats vs output score
const expectedScoreDiff = (teamA.offense - teamB.defense) - (teamB.offense - teamA.defense);
const actualScoreDiff = scoreA - scoreB;
if (Math.abs(actualScoreDiff) > Math.abs(expectedScoreDiff) * 1.5) {
narrative.push("The result significantly exceeded expectations, suggesting a stellar performance from the winning team's key players.");
} else if (Math.abs(actualScoreDiff) < Math.abs(expectedScoreDiff) * 0.5) {
narrative.push("A surprisingly close finish given the statistical matchup. Defensive play dominated the simulation.");
}
return narrative;
}, [teamA.name, teamB.name]);
const runSimulation = useCallback(async () => {
if (isSimulating) return;
// Basic validation
if (!teamA.name || !teamB.name) {
setMessages(["Please enter names for both teams before simulating."]);
return;
}
if (teamA.name === teamB.name) {
setMessages(["Teams must have different names to avoid confusion!"]);
return;
}
setIsSimulating(true);
setResults(null);
setMessages([]);
let scoreA = 0;
let scoreB = 0;
const driveLog = [];
let isTeamATurn = true; // Determine who gets the first possession
// Simulate drive by drive
for (let i = 0; i < TOTAL_DRIVES * 2; i++) {
const offense = isTeamATurn ? teamA : teamB;
const defense = isTeamATurn ? teamB : teamA;
// Introduce a slight delay for visual effect (and to simulate processing)
await new Promise(resolve => setTimeout(resolve, 50));
const points = simulateDrive(offense, defense);
driveLog.push({
offense: offense.name,
defense: defense.name,
points: points,
currentA: scoreA,
currentB: scoreB
});
if (isTeamATurn) {
scoreA += points;
} else {
scoreB += points;
}
isTeamATurn = !isTeamATurn; // Switch possession
}
const finalResults = {
scoreA,
scoreB,
winner: scoreA === scoreB ? 'Tie' : (scoreA > scoreB ? teamA.name : teamB.name),
driveLog
};
setResults(finalResults);
setMessages(generateMessages(scoreA, scoreB));
setIsSimulating(false);
}, [isSimulating, teamA, teamB, generateMessages]);
const ResultDisplay = ({ team, score, isWinner }) => (
<div className={`flex flex-col items-center justify-center p-6 rounded-xl shadow-lg transition-colors duration-300 ${isWinner ? 'bg-green-100 border-green-500' : 'bg-gray-100 border-gray-300'} border-2 w-full`}>
<Users className={`h-8 w-8 mb-2 ${isWinner ? 'text-green-600' : 'text-gray-500'}`} />
<h3 className="text-xl font-extrabold text-gray-800 truncate max-w-full" title={team.name}>{team.name}</h3>
<p className="text-5xl font-black mt-2">
{score}
</p>
</div>
);
const TeamInput = ({ team, setTeam, color }) => (
<div className={`p-6 rounded-2xl shadow-xl transition-all duration-300 ${color} ring-4 ring-offset-4 ring-opacity-50 flex-1 min-w-[300px]`}>
<h2 className="text-3xl font-bold mb-4 text-white flex items-center">
<Users className="h-6 w-6 mr-2" />
{team.name || 'Set Team'}
</h2>
<div className="space-y-4">
<label className="block">
<span className="text-white font-medium">Team Name</span>
<input
type="text"
value={team.name}
onChange={(e) => handleStatChange(setTeam, 'name', e.target.value)}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 text-gray-800 focus:ring-blue-500 focus:border-blue-500"
maxLength={20}
/>
</label>
<label className="block">
<span className="text-white font-medium">Offense Rating (1-100)</span>
<input
type="number"
value={team.offense}
onChange={(e) => handleStatChange(setTeam, 'offense', e.target.value)}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 text-gray-800 focus:ring-blue-500 focus:border-blue-500"
min="1"
max="100"
/>
<progress
className="w-full h-2 rounded-full mt-1 appearance-none [&::-webkit-progress-bar]:bg-white/50 [&::-webkit-progress-value]:bg-yellow-300"
value={team.offense}
max="100"
></progress>
</label>
<label className="block">
<span className="text-white font-medium">Defense Rating (1-100)</span>
<input
type="number"
value={team.defense}
onChange={(e) => handleStatChange(setTeam, 'defense', e.target.value)}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 text-gray-800 focus:ring-blue-500 focus:border-blue-500"
min="1"
max="100"
/>
<progress
className="w-full h-2 rounded-full mt-1 appearance-none [&::-webkit-progress-bar]:bg-white/50 [&::-webkit-progress-value]:bg-red-400"
value={team.defense}
max="100"
></progress>
</label>
</div>
</div>
);
return (
<div className="min-h-screen bg-gray-50 p-4 md:p-8 font-sans">
<script src="https://cdn.tailwindcss.com"></script>
{/* Tailwind Config for Inter font and progress bar styling */}
<style>{`
body { font-family: 'Inter', sans-serif; }
.bg-gradient-blue { background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%); }
.bg-gradient-red { background: linear-gradient(135deg, #b91c1c 0%, #ef4444 100%); }
/* Custom progress bar styling for Safari/Firefox */
progress::-webkit-progress-value { transition: width 0.3s ease; }
progress::-moz-progress-bar { transition: width 0.3s ease; }
`}</style>
<div className="max-w-6xl mx-auto">
<header className="text-center mb-10">
<h1 className="text-5xl font-extrabold text-gray-900 flex items-center justify-center">
<Trophy className="h-10 w-10 text-yellow-500 mr-3" />
Ultimate Game Simulator
</h1>
<p className="text-gray-500 mt-2">Set the stats and predict the outcome of the matchup.</p>
</header>
{/* Team Input Section */}
<div className="flex flex-col lg:flex-row gap-8 mb-10">
<TeamInput team={teamA} setTeam={setTeamA} color="bg-gradient-blue ring-blue-500" />
<TeamInput team={teamB} setTeam={setTeamB} color="bg-gradient-red ring-red-500" />
</div>
{/* Simulation Control Button */}
<div className="flex justify-center mb-10">
<button
onClick={runSimulation}
disabled={isSimulating}
className={`flex items-center px-8 py-3 text-lg font-semibold rounded-full shadow-2xl transition-all duration-300 transform active:scale-95 ${
isSimulating
? 'bg-gray-400 cursor-not-allowed'
: 'bg-yellow-500 hover:bg-yellow-600 text-gray-900 hover:shadow-yellow-300/80'
}`}
>
{isSimulating ? (
<>
<RefreshCw className="h-5 w-5 mr-3 animate-spin" />
Simulating...
</>
) : (
<>
<Play className="h-5 w-5 mr-3 fill-current" />
Run Game Simulation ({TOTAL_DRIVES * 2} Drives)
</>
)}
</button>
</div>
{/* Results Display */}
{results && (
<div className="bg-white p-6 md:p-10 rounded-3xl shadow-2xl border-t-8 border-blue-600 animate-fadeInUp">
<h2 className="text-3xl font-bold mb-6 text-gray-800 text-center">
Final Score
</h2>
{/* Score Cards */}
<div className="flex flex-col md:flex-row justify-center gap-6 mb-8">
<ResultDisplay
team={teamA}
score={results.scoreA}
isWinner={results.winner === teamA.name}
/>
<div className="flex items-center justify-center text-4xl font-bold text-gray-500">
vs
</div>
<ResultDisplay
team={teamB}
score={results.scoreB}
isWinner={results.winner === teamB.name}
/>
</div>
{/* Narrative Summary */}
<div className="bg-blue-50 p-4 rounded-xl mb-6 shadow-inner">
<h3 className="text-xl font-semibold text-blue-800 mb-2">Game Summary</h3>
<ul className="list-disc list-inside text-gray-700 space-y-1">
{messages.map((msg, index) => (
<li key={index}>{msg}</li>
))}
</ul>
</div>
{/* Drive Log (Details) */}
<div className="mt-8">
<h3 className="text-2xl font-semibold text-gray-800 mb-4 border-b pb-2">
Drive-by-Drive Breakdown
</h3>
<div className="max-h-80 overflow-y-auto bg-gray-50 p-4 rounded-xl border border-gray-200">
{results.driveLog.map((log, index) => (
<div
key={index}
className={`flex justify-between items-center py-2 px-3 rounded-lg text-sm ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-100'
}`}
>
<span className="font-mono w-10 text-gray-500">{index + 1}.</span>
<span className="flex-1 truncate mr-4">
<span className="font-semibold">{log.offense}</span> vs {log.defense}
</span>
<span
className={`font-bold w-20 text-right ${
log.points > 0 ? 'text-green-600' : 'text-red-500'
}`}
>
+{log.points} Points
</span>
<span className="font-semibold text-gray-600 w-24 text-right">
({log.currentA} - {log.currentB})
</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default App;