Guest User

Untitled

a guest
Jan 9th, 2026
48
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 22.82 KB | None | 0 0
  1. import React, { useState } from 'react';
  2. import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar } from 'recharts';
  3.  
  4. const GachaMCMC = () => {
  5. const [baseProbability, setBaseProbability] = useState(0.008);
  6. const [softPityStart, setSoftPityStart] = useState(65);
  7. const [softPityIncrement, setSoftPityIncrement] = useState(0.05);
  8. const [hardPity, setHardPity] = useState(80);
  9. const [limitedRate, setLimitedRate] = useState(0.5);
  10. const [guaranteePity, setGuaranteePity] = useState(120);
  11. const [bonusCopyInterval, setBonusCopyInterval] = useState(240);
  12. const [totalPulls, setTotalPulls] = useState(600);
  13. const [numSimulations, setNumSimulations] = useState(100000);
  14. const [isSimulating, setIsSimulating] = useState(false);
  15. const [results, setResults] = useState(null);
  16.  
  17. const simulateGachaPull = (baseProb, softStart, increment, hard, pityCounter) => {
  18. // Hard pity - guaranteed 6*
  19. if (pityCounter >= hard) {
  20. return { success: true, newPity: 0 };
  21. }
  22.  
  23. // Calculate current probability
  24. let currentProb = baseProb;
  25. if (pityCounter >= softStart) {
  26. const softPityPulls = pityCounter - softStart;
  27. currentProb = Math.min(1.0, baseProb + (softPityPulls * increment));
  28. }
  29.  
  30. // Check if won 6*
  31. if (Math.random() < currentProb) {
  32. return { success: true, newPity: 0 };
  33. }
  34.  
  35. return { success: false, newPity: pityCounter + 1 };
  36. };
  37.  
  38. const simulateNPulls = (baseProb, softStart, increment, hard, limitedChance, guaranteePityCount, bonusInterval, N) => {
  39. let limitedCharsObtained = 0;
  40. let normalPity = 0;
  41. let guaranteePityCounter = 0;
  42. let bonusCopyCounter = 0;
  43. let hasFirstLimited = false;
  44.  
  45. for (let i = 0; i < N; i++) {
  46. // Max 6 copies
  47. if (limitedCharsObtained >= 6) {
  48. break;
  49. }
  50.  
  51. // Check for bonus copy (every 240 pulls after first limited, doesn't reset normal pity)
  52. if (hasFirstLimited) {
  53. bonusCopyCounter++;
  54. if (bonusCopyCounter >= bonusInterval) {
  55. limitedCharsObtained++;
  56. bonusCopyCounter = 0;
  57. // Continue to next pull without resetting normal pity
  58. continue;
  59. }
  60. }
  61.  
  62. // Check for 120 guarantee pity
  63. guaranteePityCounter++;
  64. if (guaranteePityCounter >= guaranteePityCount && !hasFirstLimited) {
  65. // Get guaranteed limited at 120
  66. limitedCharsObtained++;
  67. hasFirstLimited = true;
  68. guaranteePityCounter = 0;
  69. normalPity = 0; // Reset normal pity
  70. bonusCopyCounter = 0; // Start bonus copy counter
  71. continue;
  72. }
  73.  
  74. // Normal pull simulation
  75. const result = simulateGachaPull(baseProb, softStart, increment, hard, normalPity);
  76. if (result.success) {
  77. // Got a 6* - check if it's limited or standard
  78. if (Math.random() < limitedChance) {
  79. // Got limited!
  80. limitedCharsObtained++;
  81. if (!hasFirstLimited) {
  82. hasFirstLimited = true;
  83. guaranteePityCounter = 0; // Reset guarantee pity after first limited
  84. bonusCopyCounter = 0; // Start bonus copy counter
  85. }
  86. }
  87. // If standard, we don't count it but still reset pity
  88. normalPity = 0;
  89. } else {
  90. normalPity = result.newPity;
  91. }
  92. }
  93.  
  94. return Math.min(limitedCharsObtained, 6);
  95. };
  96.  
  97. const simulatePullsForCopies = (baseProb, softStart, increment, hard, limitedChance, guaranteePityCount, bonusInterval, targetCopies) => {
  98. let limitedCharsObtained = 0;
  99. let normalPity = 0;
  100. let guaranteePityCounter = 0;
  101. let bonusCopyCounter = 0;
  102. let hasFirstLimited = false;
  103. let totalPulls = 0;
  104.  
  105. while (limitedCharsObtained < targetCopies) {
  106. totalPulls++;
  107.  
  108. // Check for bonus copy (every 240 pulls after first limited, doesn't reset normal pity)
  109. if (hasFirstLimited) {
  110. bonusCopyCounter++;
  111. if (bonusCopyCounter >= bonusInterval) {
  112. limitedCharsObtained++;
  113. bonusCopyCounter = 0;
  114. if (limitedCharsObtained >= targetCopies) break;
  115. // Continue to next pull without resetting normal pity
  116. continue;
  117. }
  118. }
  119.  
  120. // Check for 120 guarantee pity
  121. guaranteePityCounter++;
  122. if (guaranteePityCounter >= guaranteePityCount && !hasFirstLimited) {
  123. // Get guaranteed limited at 120
  124. limitedCharsObtained++;
  125. hasFirstLimited = true;
  126. guaranteePityCounter = 0;
  127. normalPity = 0; // Reset normal pity
  128. bonusCopyCounter = 0; // Start bonus copy counter
  129. if (limitedCharsObtained >= targetCopies) break;
  130. continue;
  131. }
  132.  
  133. // Normal pull simulation
  134. const result = simulateGachaPull(baseProb, softStart, increment, hard, normalPity);
  135. if (result.success) {
  136. // Got a 6* - check if it's limited or standard
  137. if (Math.random() < limitedChance) {
  138. // Got limited!
  139. limitedCharsObtained++;
  140. if (!hasFirstLimited) {
  141. hasFirstLimited = true;
  142. guaranteePityCounter = 0; // Reset guarantee pity after first limited
  143. bonusCopyCounter = 0; // Start bonus copy counter
  144. }
  145. if (limitedCharsObtained >= targetCopies) break;
  146. }
  147. // If standard, we don't count it but still reset pity
  148. normalPity = 0;
  149. } else {
  150. normalPity = result.newPity;
  151. }
  152. }
  153.  
  154. return totalPulls;
  155. };
  156.  
  157. const runMCMC = () => {
  158. setIsSimulating(true);
  159.  
  160. // Use setTimeout to prevent UI blocking
  161. setTimeout(() => {
  162. const characterCounts = [];
  163. const pullsFor1Copy = [];
  164. const pullsFor6Copies = [];
  165.  
  166. // Run simulations
  167. for (let i = 0; i < numSimulations; i++) {
  168. const count = simulateNPulls(
  169. baseProbability,
  170. softPityStart,
  171. softPityIncrement,
  172. hardPity,
  173. limitedRate,
  174. guaranteePity,
  175. bonusCopyInterval,
  176. totalPulls
  177. );
  178. characterCounts.push(count);
  179.  
  180. // Simulate pulls needed for 1 copy
  181. const pulls1 = simulatePullsForCopies(
  182. baseProbability,
  183. softPityStart,
  184. softPityIncrement,
  185. hardPity,
  186. limitedRate,
  187. guaranteePity,
  188. bonusCopyInterval,
  189. 1
  190. );
  191. pullsFor1Copy.push(pulls1);
  192.  
  193. // Simulate pulls needed for 6 copies
  194. const pulls6 = simulatePullsForCopies(
  195. baseProbability,
  196. softPityStart,
  197. softPityIncrement,
  198. hardPity,
  199. limitedRate,
  200. guaranteePity,
  201. bonusCopyInterval,
  202. 6
  203. );
  204. pullsFor6Copies.push(pulls6);
  205. }
  206.  
  207. // Calculate statistics
  208. const mean = characterCounts.reduce((a, b) => a + b, 0) / characterCounts.length;
  209. const sortedCounts = [...characterCounts].sort((a, b) => a - b);
  210. const median = sortedCounts[Math.floor(sortedCounts.length / 2)];
  211.  
  212. // Calculate average pulls for 1 copy and 6 copies
  213. const avgPullsFor1Copy = pullsFor1Copy.reduce((a, b) => a + b, 0) / pullsFor1Copy.length;
  214. const avgPullsFor6Copies = pullsFor6Copies.reduce((a, b) => a + b, 0) / pullsFor6Copies.length;
  215.  
  216. // Calculate percentiles
  217. const p25 = sortedCounts[Math.floor(sortedCounts.length * 0.25)];
  218. const p75 = sortedCounts[Math.floor(sortedCounts.length * 0.75)];
  219. const p90 = sortedCounts[Math.floor(sortedCounts.length * 0.90)];
  220. const p95 = sortedCounts[Math.floor(sortedCounts.length * 0.95)];
  221. const p10 = sortedCounts[Math.floor(sortedCounts.length * 0.10)];
  222. const p5 = sortedCounts[Math.floor(sortedCounts.length * 0.05)];
  223.  
  224. // Calculate variance and standard deviation
  225. const variance = characterCounts.reduce((sum, count) => sum + Math.pow(count - mean, 2), 0) / characterCounts.length;
  226. const stdDev = Math.sqrt(variance);
  227.  
  228. // Create histogram
  229. const histogram = {};
  230. characterCounts.forEach(count => {
  231. histogram[count] = (histogram[count] || 0) + 1;
  232. });
  233.  
  234. const chartData = Object.entries(histogram)
  235. .map(([count, frequency]) => ({
  236. characters: parseInt(count),
  237. probability: (frequency / numSimulations) * 100,
  238. count: frequency
  239. }))
  240. .sort((a, b) => a.characters - b.characters);
  241.  
  242. // Calculate cumulative distribution
  243. const cumulativeData = [];
  244. let cumulative = 0;
  245. chartData.forEach(point => {
  246. cumulative += point.probability;
  247. cumulativeData.push({
  248. characters: point.characters,
  249. cumulative: cumulative
  250. });
  251. });
  252.  
  253. // Calculate probability of getting 0 limited chars
  254. const zeroChars = (histogram[0] || 0) / numSimulations * 100;
  255.  
  256. setResults({
  257. mean,
  258. median,
  259. stdDev,
  260. p5,
  261. p10,
  262. p25,
  263. p75,
  264. p90,
  265. p95,
  266. min: Math.min(...characterCounts),
  267. max: Math.max(...characterCounts),
  268. chartData,
  269. cumulativeData,
  270. mode: parseInt(Object.entries(histogram).sort((a, b) => b[1] - a[1])[0][0]),
  271. zeroChars,
  272. avgPullsFor1Copy,
  273. avgPullsFor6Copies
  274. });
  275.  
  276. setIsSimulating(false);
  277. }, 100);
  278. };
  279.  
  280. return (
  281. <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">
  282. <h1 className="text-3xl font-bold text-purple-900 mb-6">Limited 6★ Character Distribution (MCMC)</h1>
  283.  
  284. <div className="bg-white p-6 rounded-lg shadow mb-6">
  285. <h2 className="text-xl font-semibold text-gray-800 mb-4">Parameters</h2>
  286.  
  287. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  288. <div>
  289. <label className="block text-sm font-medium text-gray-700 mb-2">
  290. Total Pulls (N)
  291. </label>
  292. <input
  293. type="number"
  294. min="1"
  295. step="10"
  296. value={totalPulls}
  297. onChange={(e) => setTotalPulls(parseInt(e.target.value))}
  298. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  299. />
  300. </div>
  301.  
  302. <div>
  303. <label className="block text-sm font-medium text-gray-700 mb-2">
  304. Base Probability (e.g., 0.008 = 0.8%)
  305. </label>
  306. <input
  307. type="number"
  308. step="0.001"
  309. min="0"
  310. max="1"
  311. value={baseProbability}
  312. onChange={(e) => setBaseProbability(parseFloat(e.target.value))}
  313. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  314. />
  315. </div>
  316.  
  317. <div>
  318. <label className="block text-sm font-medium text-gray-700 mb-2">
  319. Soft Pity Start (pull number)
  320. </label>
  321. <input
  322. type="number"
  323. min="1"
  324. value={softPityStart}
  325. onChange={(e) => setSoftPityStart(parseInt(e.target.value))}
  326. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  327. />
  328. </div>
  329.  
  330. <div>
  331. <label className="block text-sm font-medium text-gray-700 mb-2">
  332. Soft Pity Increment per Pull (e.g., 0.05 = 5%)
  333. </label>
  334. <input
  335. type="number"
  336. step="0.01"
  337. min="0"
  338. max="1"
  339. value={softPityIncrement}
  340. onChange={(e) => setSoftPityIncrement(parseFloat(e.target.value))}
  341. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  342. />
  343. </div>
  344.  
  345. <div>
  346. <label className="block text-sm font-medium text-gray-700 mb-2">
  347. Hard Pity (guaranteed 6★)
  348. </label>
  349. <input
  350. type="number"
  351. min="1"
  352. value={hardPity}
  353. onChange={(e) => setHardPity(parseInt(e.target.value))}
  354. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  355. />
  356. </div>
  357.  
  358. <div>
  359. <label className="block text-sm font-medium text-gray-700 mb-2">
  360. Limited Rate (0.5 = 50%)
  361. </label>
  362. <input
  363. type="number"
  364. step="0.1"
  365. min="0"
  366. max="1"
  367. value={limitedRate}
  368. onChange={(e) => setLimitedRate(parseFloat(e.target.value))}
  369. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  370. />
  371. </div>
  372.  
  373. <div>
  374. <label className="block text-sm font-medium text-gray-700 mb-2">
  375. Guarantee Pity (first limited)
  376. </label>
  377. <input
  378. type="number"
  379. min="1"
  380. value={guaranteePity}
  381. onChange={(e) => setGuaranteePity(parseInt(e.target.value))}
  382. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  383. />
  384. </div>
  385.  
  386. <div>
  387. <label className="block text-sm font-medium text-gray-700 mb-2">
  388. Bonus Copy Interval (after first limited)
  389. </label>
  390. <input
  391. type="number"
  392. min="1"
  393. value={bonusCopyInterval}
  394. onChange={(e) => setBonusCopyInterval(parseInt(e.target.value))}
  395. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  396. />
  397. </div>
  398.  
  399. <div>
  400. <label className="block text-sm font-medium text-gray-700 mb-2">
  401. Number of Simulations
  402. </label>
  403. <input
  404. type="number"
  405. min="1000"
  406. step="1000"
  407. value={numSimulations}
  408. onChange={(e) => setNumSimulations(parseInt(e.target.value))}
  409. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
  410. />
  411. </div>
  412. </div>
  413.  
  414. <button
  415. onClick={runMCMC}
  416. disabled={isSimulating}
  417. 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"
  418. >
  419. {isSimulating ? 'Simulating...' : 'Run MCMC Simulation'}
  420. </button>
  421. </div>
  422.  
  423. {results && (
  424. <>
  425. <div className="bg-white p-6 rounded-lg shadow mb-6">
  426. <h2 className="text-xl font-semibold text-gray-800 mb-4">
  427. Statistics for {totalPulls} Pulls
  428. </h2>
  429. <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
  430. <div className="bg-blue-50 p-4 rounded">
  431. <div className="text-sm text-gray-600">Mean Limited 6★</div>
  432. <div className="text-2xl font-bold text-blue-600">{results.mean.toFixed(2)}</div>
  433. </div>
  434. <div className="bg-green-50 p-4 rounded">
  435. <div className="text-sm text-gray-600">Median</div>
  436. <div className="text-2xl font-bold text-green-600">{results.median}</div>
  437. </div>
  438. <div className="bg-indigo-50 p-4 rounded">
  439. <div className="text-sm text-gray-600">Mode (Most Common)</div>
  440. <div className="text-2xl font-bold text-indigo-600">{results.mode}</div>
  441. </div>
  442. <div className="bg-teal-50 p-4 rounded">
  443. <div className="text-sm text-gray-600">Std Deviation</div>
  444. <div className="text-2xl font-bold text-teal-600">{results.stdDev.toFixed(2)}</div>
  445. </div>
  446. <div className="bg-yellow-50 p-4 rounded">
  447. <div className="text-sm text-gray-600">5th Percentile</div>
  448. <div className="text-2xl font-bold text-yellow-600">{results.p5}</div>
  449. </div>
  450. <div className="bg-purple-50 p-4 rounded">
  451. <div className="text-sm text-gray-600">10th Percentile</div>
  452. <div className="text-2xl font-bold text-purple-600">{results.p10}</div>
  453. </div>
  454. <div className="bg-orange-50 p-4 rounded">
  455. <div className="text-sm text-gray-600">25th Percentile</div>
  456. <div className="text-2xl font-bold text-orange-600">{results.p25}</div>
  457. </div>
  458. <div className="bg-pink-50 p-4 rounded">
  459. <div className="text-sm text-gray-600">75th Percentile</div>
  460. <div className="text-2xl font-bold text-pink-600">{results.p75}</div>
  461. </div>
  462. <div className="bg-red-50 p-4 rounded">
  463. <div className="text-sm text-gray-600">90th Percentile</div>
  464. <div className="text-2xl font-bold text-red-600">{results.p90}</div>
  465. </div>
  466. <div className="bg-rose-50 p-4 rounded">
  467. <div className="text-sm text-gray-600">95th Percentile</div>
  468. <div className="text-2xl font-bold text-rose-600">{results.p95}</div>
  469. </div>
  470. <div className="bg-gray-50 p-4 rounded">
  471. <div className="text-sm text-gray-600">Minimum</div>
  472. <div className="text-2xl font-bold text-gray-600">{results.min}</div>
  473. </div>
  474. <div className="bg-gray-50 p-4 rounded">
  475. <div className="text-sm text-gray-600">Maximum</div>
  476. <div className="text-2xl font-bold text-gray-600">{results.max}</div>
  477. </div>
  478. </div>
  479.  
  480. {results.zeroChars > 0 && (
  481. <div className="mt-4 p-4 bg-red-100 rounded-lg border border-red-300">
  482. <div className="text-sm font-medium text-red-800">
  483. Probability of getting 0 limited characters: {results.zeroChars.toFixed(2)}%
  484. </div>
  485. </div>
  486. )}
  487.  
  488. <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
  489. <div className="p-4 bg-emerald-100 rounded-lg border border-emerald-300">
  490. <div className="text-sm font-medium text-emerald-800 mb-1">Average Pulls for 1 Copy</div>
  491. <div className="text-3xl font-bold text-emerald-900">{results.avgPullsFor1Copy.toFixed(1)}</div>
  492. </div>
  493. <div className="p-4 bg-violet-100 rounded-lg border border-violet-300">
  494. <div className="text-sm font-medium text-violet-800 mb-1">Average Pulls for 6 Copies (Max)</div>
  495. <div className="text-3xl font-bold text-violet-900">{results.avgPullsFor6Copies.toFixed(1)}</div>
  496. </div>
  497. </div>
  498. </div>
  499.  
  500. <div className="bg-white p-6 rounded-lg shadow mb-6">
  501. <h2 className="text-xl font-semibold text-gray-800 mb-4">
  502. Limited 6★ Count Histogram ({totalPulls} pulls)
  503. </h2>
  504. <ResponsiveContainer width="100%" height={400}>
  505. <BarChart data={results.chartData}>
  506. <CartesianGrid strokeDasharray="3 3" />
  507. <XAxis
  508. dataKey="characters"
  509. label={{ value: 'Number of Limited 6★ Obtained', position: 'insideBottom', offset: -5 }}
  510. />
  511. <YAxis
  512. label={{ value: 'Probability (%)', angle: -90, position: 'insideLeft' }}
  513. />
  514. <Tooltip
  515. formatter={(value, name) => {
  516. if (name === 'probability') return `${value.toFixed(3)}%`;
  517. return value;
  518. }}
  519. labelFormatter={(label) => `${label} limited characters`}
  520. />
  521. <Bar dataKey="probability" fill="#8b5cf6" name="Probability" />
  522. </BarChart>
  523. </ResponsiveContainer>
  524. </div>
  525.  
  526. <div className="bg-white p-6 rounded-lg shadow">
  527. <h2 className="text-xl font-semibold text-gray-800 mb-4">Cumulative Distribution</h2>
  528. <ResponsiveContainer width="100%" height={300}>
  529. <LineChart data={results.cumulativeData}>
  530. <CartesianGrid strokeDasharray="3 3" />
  531. <XAxis
  532. dataKey="characters"
  533. label={{ value: 'Number of Limited 6★', position: 'insideBottom', offset: -5 }}
  534. />
  535. <YAxis
  536. label={{ value: 'Cumulative Probability (%)', angle: -90, position: 'insideLeft' }}
  537. />
  538. <Tooltip
  539. formatter={(value) => `${value.toFixed(2)}%`}
  540. labelFormatter={(label) => `≤ ${label} limited chars`}
  541. />
  542. <Legend />
  543. <Line
  544. type="monotone"
  545. dataKey="cumulative"
  546. stroke="#8b5cf6"
  547. strokeWidth={2}
  548. name="Cumulative Probability (%)"
  549. />
  550. </LineChart>
  551. </ResponsiveContainer>
  552. </div>
  553. </>
  554. )}
  555.  
  556. <div className="mt-6 p-4 bg-blue-50 rounded-lg">
  557. <h3 className="font-semibold text-blue-900 mb-2">Pity System Explained:</h3>
  558. <ul className="text-sm text-blue-800 space-y-1 list-disc list-inside">
  559. <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>
  560. <li><strong>Limited vs Standard:</strong> Each 6★ has {(limitedRate * 100).toFixed(0)}% chance to be limited character</li>
  561. <li><strong>First Copy Guarantee:</strong> If no limited obtained by pull {guaranteePity}, guaranteed limited at {guaranteePity}</li>
  562. <li><strong>Bonus Copies:</strong> After first limited, get free copy every {bonusCopyInterval} pulls (doesn't reset pity)</li>
  563. </ul>
  564. </div>
  565. </div>
  566. );
  567. };
  568.  
  569. export default GachaMCMC;
Advertisement
Add Comment
Please, Sign In to add comment