<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pack Rarity Simulator</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
padding: 20px;
line-height: 1.4;
}
h1 { color: #f39c12; margin-bottom: 10px; }
h2 { color: #3498db; margin: 15px 0 10px; font-size: 1.1em; border-bottom: 1px solid #333; padding-bottom: 5px; }
h3 { color: #9b59b6; margin: 10px 0 5px; font-size: 0.95em; }
.container { display: grid; grid-template-columns: 320px 1fr; gap: 20px; max-width: 1200px; margin: 0 auto; }
.panel { background: #16213e; padding: 15px; border-radius: 8px; }
.input-row { display: flex; align-items: center; margin: 5px 0; gap: 8px; }
.input-row label { flex: 1; font-size: 0.85em; color: #aaa; }
.input-row input[type="number"] { width: 80px; padding: 4px 6px; border: 1px solid #444; border-radius: 4px; background: #0f0f23; color: #eee; text-align: right; }
.template-editor { margin: 10px 0; }
.template-slots { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }
.template-slot { padding: 6px 12px; border-radius: 4px; font-size: 0.85em; font-weight: bold; cursor: pointer; border: 2px solid transparent; }
.template-slot:hover { opacity: 0.8; }
.slot-common { background: #555; color: #eee; }
.slot-rare { background: #3498db; color: #fff; }
.slot-epic { background: #9b59b6; color: #fff; }
.slot-legendary { background: #f39c12; color: #fff; }
.template-buttons { display: flex; gap: 6px; margin-top: 8px; }
.template-buttons button { padding: 4px 10px; border: 1px solid #444; border-radius: 4px; background: #0f0f23; color: #aaa; cursor: pointer; font-size: 0.8em; }
.template-buttons button:hover { border-color: #3498db; color: #eee; }
.presets { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
.preset-btn { padding: 5px 10px; border: 1px solid #555; border-radius: 4px; background: #0f0f23; color: #aaa; cursor: pointer; font-size: 0.8em; }
.preset-btn:hover { border-color: #3498db; color: #eee; }
.preset-btn.active { border-color: #f39c12; background: #2a2a4e; color: #eee; }
.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.rarity-bar { margin: 8px 0; }
.rarity-label { display: flex; justify-content: space-between; font-size: 0.85em; margin-bottom: 3px; }
.rarity-label .name { font-weight: bold; }
.rarity-label .pct { color: #2ecc71; font-family: monospace; }
.bar-bg { height: 24px; background: #0f0f23; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; display: flex; align-items: center; padding-left: 8px; font-size: 0.75em; font-weight: bold; }
.bar-common { background: #555; }
.bar-rare { background: #3498db; }
.bar-epic { background: #9b59b6; }
.bar-legendary { background: #f39c12; }
.stats-section { margin-top: 15px; }
.stat-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #333; font-size: 0.85em; }
.stat-row:last-child { border-bottom: none; }
.stat-label { color: #888; }
.stat-value { color: #2ecc71; font-family: monospace; }
.pack-breakdown { margin-top: 15px; }
.pack-type-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 0.8em; color: #aaa; }
.pack-type-row .count { font-family: monospace; color: #eee; }
.note { font-size: 0.75em; color: #666; margin-top: 10px; font-style: italic; }
</style>
</head>
<body>
<h1>Pack Rarity Simulator</h1>
<div class="container">
<div class="panel">
<h2>Configuration</h2>
<h3>Upgrade Probabilities</h3>
<div class="input-row">
<label>Common → Rare</label>
<input type="number" id="prob-c2r" value="10" min="0" max="100" step="1">
<span style="color:#888; font-size:0.85em">%</span>
</div>
<div class="input-row">
<label>Rare → Epic</label>
<input type="number" id="prob-r2e" value="10" min="0" max="100" step="1">
<span style="color:#888; font-size:0.85em">%</span>
</div>
<div class="input-row">
<label>Epic → Legendary</label>
<input type="number" id="prob-e2l" value="10" min="0" max="100" step="1">
<span style="color:#888; font-size:0.85em">%</span>
</div>
<div class="presets">
<button class="preset-btn" data-preset="conservative">Conservative</button>
<button class="preset-btn active" data-preset="default">Default</button>
<button class="preset-btn" data-preset="generous">Generous</button>
<button class="preset-btn" data-preset="hearthstone">Hearthstone-ish</button>
</div>
<h3>Pack Template</h3>
<div class="template-editor">
<div class="template-slots" id="template-slots"></div>
<div class="template-buttons">
<button id="add-common">+ Common</button>
<button id="add-rare">+ Rare</button>
<button id="add-epic">+ Epic</button>
<button id="add-legendary">+ Legendary</button>
<button id="remove-last">Remove</button>
</div>
<div class="presets" style="margin-top: 8px;">
<button class="preset-btn" data-template="basic">Basic (4C 1R)</button>
<button class="preset-btn" data-template="premium">Premium (3C 2R)</button>
<button class="preset-btn" data-template="epic">Epic (2C 2R 1E)</button>
</div>
</div>
<h3>Simulation</h3>
<div class="input-row">
<label>Packs to open</label>
<input type="number" id="num-packs" value="10000" min="100" max="100000" step="100">
</div>
<p class="note">Results update automatically as you change inputs.</p>
</div>
<div class="panel">
<h2>Results</h2>
<div class="results-grid">
<div>
<h3>Per-Item Rarity Distribution</h3>
<div id="item-bars"></div>
<div class="stats-section">
<h3>Per-Item Stats</h3>
<div id="item-stats"></div>
</div>
</div>
<div>
<h3>Per-Pack Stats</h3>
<div id="pack-stats"></div>
<div class="stats-section">
<h3>Pack Composition Breakdown</h3>
<p style="font-size:0.75em; color:#666; margin-bottom:8px;">most common pack types</p>
<div id="pack-breakdown"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const RARITIES = ['common', 'rare', 'epic', 'legendary'];
const RARITY_COLORS = { common: '#555', rare: '#3498db', epic: '#9b59b6', legendary: '#f39c12' };
const PRESETS = {
conservative: { c2r: 5, r2e: 5, e2l: 3 },
default: { c2r: 10, r2e: 10, e2l: 10 },
generous: { c2r: 25, r2e: 15, e2l: 10 },
'hearthstone-ish': { c2r: 22, r2e: 18, e2l: 20 },
};
const TEMPLATE_PRESETS = {
basic: ['common', 'common', 'common', 'common', 'rare'],
premium: ['common', 'common', 'common', 'rare', 'rare'],
epic: ['common', 'common', 'rare', 'rare', 'epic'],
};
let template = ['common', 'common', 'common', 'common', 'rare'];
function getProbs() {
return {
c2r: parseFloat(document.getElementById('prob-c2r').value) / 100,
r2e: parseFloat(document.getElementById('prob-r2e').value) / 100,
e2l: parseFloat(document.getElementById('prob-e2l').value) / 100,
};
}
function upgradeRarity(rarity, probs) {
while (true) {
let roll = Math.random();
if (rarity === 'common' && roll < probs.c2r) { rarity = 'rare'; }
else if (rarity === 'rare' && roll < probs.r2e) { rarity = 'epic'; }
else if (rarity === 'epic' && roll < probs.e2l) { rarity = 'legendary'; }
else break;
}
return rarity;
}
function simulate() {
const probs = getProbs();
const numPacks = parseInt(document.getElementById('num-packs').value);
const itemsPerPack = template.length;
const totalItems = numPacks * itemsPerPack;
const itemCounts = { common: 0, rare: 0, epic: 0, legendary: 0 };
const packTypes = {};
let packsWithEpicPlus = 0;
let packsWithLegendary = 0;
let allCommonPacks = 0;
for (let p = 0; p < numPacks; p++) {
const packResult = { common: 0, rare: 0, epic: 0, legendary: 0 };
for (let i = 0; i < itemsPerPack; i++) {
const finalRarity = upgradeRarity(template[i], probs);
itemCounts[finalRarity]++;
packResult[finalRarity]++;
}
const key = `${packResult.common}C ${packResult.rare}R ${packResult.epic}E ${packResult.legendary}L`;
packTypes[key] = (packTypes[key] || 0) + 1;
if (packResult.epic > 0 || packResult.legendary > 0) packsWithEpicPlus++;
if (packResult.legendary > 0) packsWithLegendary++;
if (packResult.rare === 0 && packResult.epic === 0 && packResult.legendary === 0) allCommonPacks++;
}
return { itemCounts, totalItems, numPacks, packTypes, packsWithEpicPlus, packsWithLegendary, allCommonPacks };
}
function renderResults() {
const result = simulate();
const barsEl = document.getElementById('item-bars');
barsEl.innerHTML = '';
for (const rarity of RARITIES) {
const count = result.itemCounts[rarity];
const pct = (count / result.totalItems * 100);
const div = document.createElement('div');
div.className = 'rarity-bar';
div.innerHTML = `
<div class="rarity-label">
<span class="name" style="color:${RARITY_COLORS[rarity]}">${rarity}</span>
<span class="pct">${pct.toFixed(2)}% (${count.toLocaleString()})</span>
</div>
<div class="bar-bg">
<div class="bar-fill bar-${rarity}" style="width:${Math.max(pct, 0.5)}%"></div>
</div>
`;
barsEl.appendChild(div);
}
const itemStatsEl = document.getElementById('item-stats');
const avgRare = result.numPacks / Math.max(result.itemCounts.rare, 1);
const avgEpic = result.numPacks / Math.max(result.itemCounts.epic, 1);
const avgLegendary = result.numPacks / Math.max(result.itemCounts.legendary, 1);
itemStatsEl.innerHTML = `
<div class="stat-row"><span class="stat-label">Avg packs per rare item</span><span class="stat-value">${avgRare.toFixed(1)}</span></div>
<div class="stat-row"><span class="stat-label">Avg packs per epic item</span><span class="stat-value">${avgEpic.toFixed(1)}</span></div>
<div class="stat-row"><span class="stat-label">Avg packs per legendary item</span><span class="stat-value">${avgLegendary.toFixed(1)}</span></div>
`;
const packStatsEl = document.getElementById('pack-stats');
const epicPlusPct = (result.packsWithEpicPlus / result.numPacks * 100);
const legendaryPct = (result.packsWithLegendary / result.numPacks * 100);
const allCommonPct = (result.allCommonPacks / result.numPacks * 100);
const packsPerEpicPack = result.packsWithEpicPlus > 0 ? (result.numPacks / result.packsWithEpicPlus) : Infinity;
const packsPerLegendaryPack = result.packsWithLegendary > 0 ? (result.numPacks / result.packsWithLegendary) : Infinity;
packStatsEl.innerHTML = `
<div class="stat-row"><span class="stat-label">Packs with epic+</span><span class="stat-value">${epicPlusPct.toFixed(1)}%</span></div>
<div class="stat-row"><span class="stat-label">Packs with legendary</span><span class="stat-value">${legendaryPct.toFixed(1)}%</span></div>
<div class="stat-row"><span class="stat-label">All-common packs</span><span class="stat-value">${allCommonPct.toFixed(1)}%</span></div>
<div class="stat-row"><span class="stat-label">Avg packs until epic+ pack</span><span class="stat-value">${packsPerEpicPack === Infinity ? '∞' : packsPerEpicPack.toFixed(1)}</span></div>
<div class="stat-row"><span class="stat-label">Avg packs until legendary pack</span><span class="stat-value">${packsPerLegendaryPack === Infinity ? '∞' : packsPerLegendaryPack.toFixed(1)}</span></div>
`;
const breakdownEl = document.getElementById('pack-breakdown');
const sorted = Object.entries(result.packTypes).sort((a, b) => b[1] - a[1]).slice(0, 10);
breakdownEl.innerHTML = sorted.map(([type, count]) => {
const pct = (count / result.numPacks * 100).toFixed(1);
return `<div class="pack-type-row"><span>${type}</span><span class="count">${pct}% (${count.toLocaleString()})</span></div>`;
}).join('');
}
function renderTemplate() {
const slotsEl = document.getElementById('template-slots');
slotsEl.innerHTML = '';
template.forEach((rarity, i) => {
const span = document.createElement('span');
span.className = `template-slot slot-${rarity}`;
span.textContent = rarity[0].toUpperCase();
span.title = `${rarity} — click to cycle`;
span.addEventListener('click', () => {
const idx = RARITIES.indexOf(template[i]);
template[i] = RARITIES[(idx + 1) % RARITIES.length];
renderTemplate();
renderResults();
});
slotsEl.appendChild(span);
});
}
document.getElementById('add-common').addEventListener('click', () => { template.push('common'); renderTemplate(); renderResults(); });
document.getElementById('add-rare').addEventListener('click', () => { template.push('rare'); renderTemplate(); renderResults(); });
document.getElementById('add-epic').addEventListener('click', () => { template.push('epic'); renderTemplate(); renderResults(); });
document.getElementById('add-legendary').addEventListener('click', () => { template.push('legendary'); renderTemplate(); renderResults(); });
document.getElementById('remove-last').addEventListener('click', () => { if (template.length > 1) { template.pop(); renderTemplate(); renderResults(); } });
document.querySelectorAll('[data-template]').forEach(btn => {
btn.addEventListener('click', () => {
template = [...TEMPLATE_PRESETS[btn.dataset.template]];
renderTemplate();
renderResults();
});
});
document.querySelectorAll('[data-preset]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const p = PRESETS[btn.dataset.preset];
document.getElementById('prob-c2r').value = p.c2r;
document.getElementById('prob-r2e').value = p.r2e;
document.getElementById('prob-e2l').value = p.e2l;
renderResults();
});
});
document.querySelectorAll('input').forEach(el => {
el.addEventListener('change', renderResults);
el.addEventListener('input', renderResults);
});
renderTemplate();
renderResults();
</script>
</body>
</html>