Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React, { useState } from 'react';
- import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar } from 'recharts';
- const GachaMCMC = () => {
- const [baseProbability, setBaseProbability] = useState(0.008);
- const [softPityStart, setSoftPityStart] = useState(65);
- const [softPityIncrement, setSoftPityIncrement] = useState(0.05);
- const [hardPity, setHardPity] = useState(80);
- const [limitedRate, setLimitedRate] = useState(0.5);
- const [guaranteePity, setGuaranteePity] = useState(120);
- const [bonusCopyInterval, setBonusCopyInterval] = useState(240);
- const [totalPulls, setTotalPulls] = useState(600);
- const [numSimulations, setNumSimulations] = useState(100000);
- const [isSimulating, setIsSimulating] = useState(false);
- const [results, setResults] = useState(null);
- const simulateGachaPull = (baseProb, softStart, increment, hard, pityCounter) => {
- // Hard pity - guaranteed 6*
- if (pityCounter >= hard) {
- return { success: true, newPity: 0 };
- }
- // Calculate current probability
- let currentProb = baseProb;
- if (pityCounter >= softStart) {
- const softPityPulls = pityCounter - softStart;
- currentProb = Math.min(1.0, baseProb + (softPityPulls * increment));
- }
- // Check if won 6*
- if (Math.random() < currentProb) {
- return { success: true, newPity: 0 };
- }
- return { success: false, newPity: pityCounter + 1 };
- };
- const simulateNPulls = (baseProb, softStart, increment, hard, limitedChance, guaranteePityCount, bonusInterval, N) => {
- let limitedCharsObtained = 0;
- let normalPity = 0;
- let guaranteePityCounter = 0;
- let bonusCopyCounter = 0;
- let hasFirstLimited = false;
- for (let i = 0; i < N; i++) {
- // Max 6 copies
- if (limitedCharsObtained >= 6) {
- break;
- }
- // Check for bonus copy (every 240 pulls after first limited, doesn't reset normal pity)
- if (hasFirstLimited) {
- bonusCopyCounter++;
- if (bonusCopyCounter >= bonusInterval) {
- limitedCharsObtained++;
- bonusCopyCounter = 0;
- // Continue to next pull without resetting normal pity
- continue;
- }
- }
- // Check for 120 guarantee pity
- guaranteePityCounter++;
- if (guaranteePityCounter >= guaranteePityCount && !hasFirstLimited) {
- // Get guaranteed limited at 120
- limitedCharsObtained++;
- hasFirstLimited = true;
- guaranteePityCounter = 0;
- normalPity = 0; // Reset normal pity
- bonusCopyCounter = 0; // Start bonus copy counter
- continue;
- }
- // Normal pull simulation
- const result = simulateGachaPull(baseProb, softStart, increment, hard, normalPity);
- if (result.success) {
- // Got a 6* - check if it's limited or standard
- if (Math.random() < limitedChance) {
- // Got limited!
- limitedCharsObtained++;
- if (!hasFirstLimited) {
- hasFirstLimited = true;
- guaranteePityCounter = 0; // Reset guarantee pity after first limited
- bonusCopyCounter = 0; // Start bonus copy counter
- }
- }
- // If standard, we don't count it but still reset pity
- normalPity = 0;
- } else {
- normalPity = result.newPity;
- }
- }
- return Math.min(limitedCharsObtained, 6);
- };
- const simulatePullsForCopies = (baseProb, softStart, increment, hard, limitedChance, guaranteePityCount, bonusInterval, targetCopies) => {
- let limitedCharsObtained = 0;
- let normalPity = 0;
- let guaranteePityCounter = 0;
- let bonusCopyCounter = 0;
- let hasFirstLimited = false;
- let totalPulls = 0;
- while (limitedCharsObtained < targetCopies) {
- totalPulls++;
- // Check for bonus copy (every 240 pulls after first limited, doesn't reset normal pity)
- if (hasFirstLimited) {
- bonusCopyCounter++;
- if (bonusCopyCounter >= bonusInterval) {
- limitedCharsObtained++;
- bonusCopyCounter = 0;
- if (limitedCharsObtained >= targetCopies) break;
- // Continue to next pull without resetting normal pity
- continue;
- }
- }
- // Check for 120 guarantee pity
- guaranteePityCounter++;
- if (guaranteePityCounter >= guaranteePityCount && !hasFirstLimited) {
- // Get guaranteed limited at 120
- limitedCharsObtained++;
- hasFirstLimited = true;
- guaranteePityCounter = 0;
- normalPity = 0; // Reset normal pity
- bonusCopyCounter = 0; // Start bonus copy counter
- if (limitedCharsObtained >= targetCopies) break;
- continue;
- }
- // Normal pull simulation
- const result = simulateGachaPull(baseProb, softStart, increment, hard, normalPity);
- if (result.success) {
- // Got a 6* - check if it's limited or standard
- if (Math.random() < limitedChance) {
- // Got limited!
- limitedCharsObtained++;
- if (!hasFirstLimited) {
- hasFirstLimited = true;
- guaranteePityCounter = 0; // Reset guarantee pity after first limited
- bonusCopyCounter = 0; // Start bonus copy counter
- }
- if (limitedCharsObtained >= targetCopies) break;
- }
- // If standard, we don't count it but still reset pity
- normalPity = 0;
- } else {
- normalPity = result.newPity;
- }
- }
- return totalPulls;
- };
- const runMCMC = () => {
- setIsSimulating(true);
- // Use setTimeout to prevent UI blocking
- setTimeout(() => {
- const characterCounts = [];
- const pullsFor1Copy = [];
- const pullsFor6Copies = [];
- // Run simulations
- for (let i = 0; i < numSimulations; i++) {
- const count = simulateNPulls(
- baseProbability,
- softPityStart,
- softPityIncrement,
- hardPity,
- limitedRate,
- guaranteePity,
- bonusCopyInterval,
- totalPulls
- );
- characterCounts.push(count);
- // Simulate pulls needed for 1 copy
- const pulls1 = simulatePullsForCopies(
- baseProbability,
- softPityStart,
- softPityIncrement,
- hardPity,
- limitedRate,
- guaranteePity,
- bonusCopyInterval,
- 1
- );
- pullsFor1Copy.push(pulls1);
- // Simulate pulls needed for 6 copies
- const pulls6 = simulatePullsForCopies(
- baseProbability,
- softPityStart,
- softPityIncrement,
- hardPity,
- limitedRate,
- guaranteePity,
- bonusCopyInterval,
- 6
- );
- pullsFor6Copies.push(pulls6);
- }
- // Calculate statistics
- const mean = characterCounts.reduce((a, b) => a + b, 0) / characterCounts.length;
- const sortedCounts = [...characterCounts].sort((a, b) => a - b);
- const median = sortedCounts[Math.floor(sortedCounts.length / 2)];
- // Calculate average pulls for 1 copy and 6 copies
- const avgPullsFor1Copy = pullsFor1Copy.reduce((a, b) => a + b, 0) / pullsFor1Copy.length;
- const avgPullsFor6Copies = pullsFor6Copies.reduce((a, b) => a + b, 0) / pullsFor6Copies.length;
- // Calculate percentiles
- const p25 = sortedCounts[Math.floor(sortedCounts.length * 0.25)];
- const p75 = sortedCounts[Math.floor(sortedCounts.length * 0.75)];
- const p90 = sortedCounts[Math.floor(sortedCounts.length * 0.90)];
- const p95 = sortedCounts[Math.floor(sortedCounts.length * 0.95)];
- const p10 = sortedCounts[Math.floor(sortedCounts.length * 0.10)];
- const p5 = sortedCounts[Math.floor(sortedCounts.length * 0.05)];
- // Calculate variance and standard deviation
- const variance = characterCounts.reduce((sum, count) => sum + Math.pow(count - mean, 2), 0) / characterCounts.length;
- const stdDev = Math.sqrt(variance);
- // Create histogram
- const histogram = {};
- characterCounts.forEach(count => {
- histogram[count] = (histogram[count] || 0) + 1;
- });
- const chartData = Object.entries(histogram)
- .map(([count, frequency]) => ({
- characters: parseInt(count),
- probability: (frequency / numSimulations) * 100,
- count: frequency
- }))
- .sort((a, b) => a.characters - b.characters);
- // Calculate cumulative distribution
- const cumulativeData = [];
- let cumulative = 0;
- chartData.forEach(point => {
- cumulative += point.probability;
- cumulativeData.push({
- characters: point.characters,
- cumulative: cumulative
- });
- });
- // Calculate probability of getting 0 limited chars
- const zeroChars = (histogram[0] || 0) / numSimulations * 100;
- setResults({
- mean,
- median,
- stdDev,
- p5,
- p10,
- p25,
- p75,
- p90,
- p95,
- min: Math.min(...characterCounts),
- max: Math.max(...characterCounts),
- chartData,
- cumulativeData,
- mode: parseInt(Object.entries(histogram).sort((a, b) => b[1] - a[1])[0][0]),
- zeroChars,
- avgPullsFor1Copy,
- avgPullsFor6Copies
- });
- setIsSimulating(false);
- }, 100);
- };
- return (
- <div className="w-full max-w-6xl mx-auto p-6 bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg shadow-lg">
- <h1 className="text-3xl font-bold text-purple-900 mb-6">Limited 6★ Character Distribution (MCMC)</h1>
- <div className="bg-white p-6 rounded-lg shadow mb-6">
- <h2 className="text-xl font-semibold text-gray-800 mb-4">Parameters</h2>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Total Pulls (N)
- </label>
- <input
- type="number"
- min="1"
- step="10"
- value={totalPulls}
- onChange={(e) => setTotalPulls(parseInt(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Base Probability (e.g., 0.008 = 0.8%)
- </label>
- <input
- type="number"
- step="0.001"
- min="0"
- max="1"
- value={baseProbability}
- onChange={(e) => setBaseProbability(parseFloat(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Soft Pity Start (pull number)
- </label>
- <input
- type="number"
- min="1"
- value={softPityStart}
- onChange={(e) => setSoftPityStart(parseInt(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Soft Pity Increment per Pull (e.g., 0.05 = 5%)
- </label>
- <input
- type="number"
- step="0.01"
- min="0"
- max="1"
- value={softPityIncrement}
- onChange={(e) => setSoftPityIncrement(parseFloat(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Hard Pity (guaranteed 6★)
- </label>
- <input
- type="number"
- min="1"
- value={hardPity}
- onChange={(e) => setHardPity(parseInt(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Limited Rate (0.5 = 50%)
- </label>
- <input
- type="number"
- step="0.1"
- min="0"
- max="1"
- value={limitedRate}
- onChange={(e) => setLimitedRate(parseFloat(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Guarantee Pity (first limited)
- </label>
- <input
- type="number"
- min="1"
- value={guaranteePity}
- onChange={(e) => setGuaranteePity(parseInt(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Bonus Copy Interval (after first limited)
- </label>
- <input
- type="number"
- min="1"
- value={bonusCopyInterval}
- onChange={(e) => setBonusCopyInterval(parseInt(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Number of Simulations
- </label>
- <input
- type="number"
- min="1000"
- step="1000"
- value={numSimulations}
- onChange={(e) => setNumSimulations(parseInt(e.target.value))}
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
- />
- </div>
- </div>
- <button
- onClick={runMCMC}
- disabled={isSimulating}
- className="mt-6 w-full bg-purple-600 text-white py-3 px-6 rounded-md font-semibold hover:bg-purple-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
- >
- {isSimulating ? 'Simulating...' : 'Run MCMC Simulation'}
- </button>
- </div>
- {results && (
- <>
- <div className="bg-white p-6 rounded-lg shadow mb-6">
- <h2 className="text-xl font-semibold text-gray-800 mb-4">
- Statistics for {totalPulls} Pulls
- </h2>
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- <div className="bg-blue-50 p-4 rounded">
- <div className="text-sm text-gray-600">Mean Limited 6★</div>
- <div className="text-2xl font-bold text-blue-600">{results.mean.toFixed(2)}</div>
- </div>
- <div className="bg-green-50 p-4 rounded">
- <div className="text-sm text-gray-600">Median</div>
- <div className="text-2xl font-bold text-green-600">{results.median}</div>
- </div>
- <div className="bg-indigo-50 p-4 rounded">
- <div className="text-sm text-gray-600">Mode (Most Common)</div>
- <div className="text-2xl font-bold text-indigo-600">{results.mode}</div>
- </div>
- <div className="bg-teal-50 p-4 rounded">
- <div className="text-sm text-gray-600">Std Deviation</div>
- <div className="text-2xl font-bold text-teal-600">{results.stdDev.toFixed(2)}</div>
- </div>
- <div className="bg-yellow-50 p-4 rounded">
- <div className="text-sm text-gray-600">5th Percentile</div>
- <div className="text-2xl font-bold text-yellow-600">{results.p5}</div>
- </div>
- <div className="bg-purple-50 p-4 rounded">
- <div className="text-sm text-gray-600">10th Percentile</div>
- <div className="text-2xl font-bold text-purple-600">{results.p10}</div>
- </div>
- <div className="bg-orange-50 p-4 rounded">
- <div className="text-sm text-gray-600">25th Percentile</div>
- <div className="text-2xl font-bold text-orange-600">{results.p25}</div>
- </div>
- <div className="bg-pink-50 p-4 rounded">
- <div className="text-sm text-gray-600">75th Percentile</div>
- <div className="text-2xl font-bold text-pink-600">{results.p75}</div>
- </div>
- <div className="bg-red-50 p-4 rounded">
- <div className="text-sm text-gray-600">90th Percentile</div>
- <div className="text-2xl font-bold text-red-600">{results.p90}</div>
- </div>
- <div className="bg-rose-50 p-4 rounded">
- <div className="text-sm text-gray-600">95th Percentile</div>
- <div className="text-2xl font-bold text-rose-600">{results.p95}</div>
- </div>
- <div className="bg-gray-50 p-4 rounded">
- <div className="text-sm text-gray-600">Minimum</div>
- <div className="text-2xl font-bold text-gray-600">{results.min}</div>
- </div>
- <div className="bg-gray-50 p-4 rounded">
- <div className="text-sm text-gray-600">Maximum</div>
- <div className="text-2xl font-bold text-gray-600">{results.max}</div>
- </div>
- </div>
- {results.zeroChars > 0 && (
- <div className="mt-4 p-4 bg-red-100 rounded-lg border border-red-300">
- <div className="text-sm font-medium text-red-800">
- Probability of getting 0 limited characters: {results.zeroChars.toFixed(2)}%
- </div>
- </div>
- )}
- <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="p-4 bg-emerald-100 rounded-lg border border-emerald-300">
- <div className="text-sm font-medium text-emerald-800 mb-1">Average Pulls for 1 Copy</div>
- <div className="text-3xl font-bold text-emerald-900">{results.avgPullsFor1Copy.toFixed(1)}</div>
- </div>
- <div className="p-4 bg-violet-100 rounded-lg border border-violet-300">
- <div className="text-sm font-medium text-violet-800 mb-1">Average Pulls for 6 Copies (Max)</div>
- <div className="text-3xl font-bold text-violet-900">{results.avgPullsFor6Copies.toFixed(1)}</div>
- </div>
- </div>
- </div>
- <div className="bg-white p-6 rounded-lg shadow mb-6">
- <h2 className="text-xl font-semibold text-gray-800 mb-4">
- Limited 6★ Count Histogram ({totalPulls} pulls)
- </h2>
- <ResponsiveContainer width="100%" height={400}>
- <BarChart data={results.chartData}>
- <CartesianGrid strokeDasharray="3 3" />
- <XAxis
- dataKey="characters"
- label={{ value: 'Number of Limited 6★ Obtained', position: 'insideBottom', offset: -5 }}
- />
- <YAxis
- label={{ value: 'Probability (%)', angle: -90, position: 'insideLeft' }}
- />
- <Tooltip
- formatter={(value, name) => {
- if (name === 'probability') return `${value.toFixed(3)}%`;
- return value;
- }}
- labelFormatter={(label) => `${label} limited characters`}
- />
- <Bar dataKey="probability" fill="#8b5cf6" name="Probability" />
- </BarChart>
- </ResponsiveContainer>
- </div>
- <div className="bg-white p-6 rounded-lg shadow">
- <h2 className="text-xl font-semibold text-gray-800 mb-4">Cumulative Distribution</h2>
- <ResponsiveContainer width="100%" height={300}>
- <LineChart data={results.cumulativeData}>
- <CartesianGrid strokeDasharray="3 3" />
- <XAxis
- dataKey="characters"
- label={{ value: 'Number of Limited 6★', position: 'insideBottom', offset: -5 }}
- />
- <YAxis
- label={{ value: 'Cumulative Probability (%)', angle: -90, position: 'insideLeft' }}
- />
- <Tooltip
- formatter={(value) => `${value.toFixed(2)}%`}
- labelFormatter={(label) => `≤ ${label} limited chars`}
- />
- <Legend />
- <Line
- type="monotone"
- dataKey="cumulative"
- stroke="#8b5cf6"
- strokeWidth={2}
- name="Cumulative Probability (%)"
- />
- </LineChart>
- </ResponsiveContainer>
- </div>
- </>
- )}
- <div className="mt-6 p-4 bg-blue-50 rounded-lg">
- <h3 className="font-semibold text-blue-900 mb-2">Pity System Explained:</h3>
- <ul className="text-sm text-blue-800 space-y-1 list-disc list-inside">
- <li><strong>Normal Pity:</strong> Base {(baseProbability * 100).toFixed(1)}% chance, increases by {(softPityIncrement * 100).toFixed(0)}% after pull {softPityStart}, guaranteed 6★ at {hardPity}</li>
- <li><strong>Limited vs Standard:</strong> Each 6★ has {(limitedRate * 100).toFixed(0)}% chance to be limited character</li>
- <li><strong>First Copy Guarantee:</strong> If no limited obtained by pull {guaranteePity}, guaranteed limited at {guaranteePity}</li>
- <li><strong>Bonus Copies:</strong> After first limited, get free copy every {bonusCopyInterval} pulls (doesn't reset pity)</li>
- </ul>
- </div>
- </div>
- );
- };
- export default GachaMCMC;
Advertisement
Add Comment
Please, Sign In to add comment