Every Genshin Impact player eventually asks the same question after a rough banner: how many wishes will it actually take to get the character I want, and how much is that going to cost in real money? HoYoverse publishes the raw pity numbers, but nobody hands you a calculator. This tutorial builds one from scratch, using only officially documented rates, and shows the math behind the expected-value figure so you can verify every number yourself instead of trusting a random spreadsheet you found on Reddit.

By the end you’ll have a working Python genshin wish calculator that models the pity system pull by pull, a pity-cost table priced in both Primogems and US dollars, and a full expected-value derivation you can adapt for any 50/50-style gacha banner. This is a code-along tutorial, not a spreadsheet download, so budget the full 90 minutes if you want to type along.

This piece sits in our provably fair coverage, where we treat gacha odds the same way we’d treat any other disclosed probability system: verify the published rate, do the arithmetic in the open, and flag anywhere the math has to lean on an assumption instead of a confirmed number. Genshin Impact is a useful test case because HoYoverse actually publishes its base rates and pity thresholds, unlike plenty of loot-box systems that disclose nothing at all.

Prerequisites: what you need before starting

You don’t need a gaming rig for this. The calculator runs as a small command-line script, so any laptop from the last decade will handle it. Here’s what to install first.

  • Python 3.10 or newer — check with python3 --version. Anything from 3.9 up will run the code in this guide, but 3.10+ gives you the match statement used in one optional section.
  • A text editor — VS Code, Sublime, or even nano. No IDE plugins required.
  • pip — comes bundled with Python 3.4+. You’ll install one small dependency (matplotlib) for the optional chart in step 9.
  • Basic command-line comfort — you should be able to run a Python file and read a stack trace. No prior probability background needed; the math is explained inline.
  • 15 minutes with your own wish history (optional) — if you want to plug in your real account data, export it via the in-game wish history page before you start.

Nothing here requires modifying game files, using third-party injectors, or touching your account credentials. This calculator only consumes public rate data and, optionally, the wish history export HoYoverse already provides in the client. If you’d rather skip the coding entirely and just want the final numbers, jump straight to the pity-cost table further down, but you’ll get more out of the guide by typing along, since the EV derivation in Step 7 is what actually explains why those numbers are what they are, rather than just asserting them.

Step 1: Understand the official pity numbers you’re coding against

Before writing a line of code, get the actual mechanics straight, because a calculator built on wrong assumptions is worse than no calculator. Genshin Impact’s featured character event banner works like this, per HoYoverse’s own rate-disclosure documentation:

  • The base drop rate for any 5-star item is 0.6% per pull for pulls 1 through 73.
  • Soft pity begins at pull 74 — the 5-star rate climbs sharply with each additional pull.
  • Hard pity hits at pull 90 — you are guaranteed a 5-star by then, no exceptions.
  • The event banner runs on a 50/50: the first 5-star you pull has a 50% chance of being the featured character and a 50% chance of being a random standard-banner 5-star.
  • If you lose the 50/50, the next 5-star you pull is guaranteed to be the featured character (the “guarantee” mechanic).
  • Pity carries over between banners within the same category — it never resets when a banner rotates, only when you actually pull a 5-star.

That gives us the worst-case scenario in plain terms: hit hard pity at pull 90, lose the 50/50, then hit hard pity again at pull 90 on the follow-up. That’s 180 pulls as an absolute ceiling to guarantee the featured unit, confirmed by HoYoverse’s own rate disclosure article on HoYoLab. Everything the calculator does from here is built on those six bullet points, nothing more.

Step 2: Set up the project folder

Create a clean working directory so the files don’t get lost in your Downloads folder.

mkdir genshin-pity-calculator
cd genshin-pity-calculator
python3 -m venv venv
source venv/bin/activate   # on Windows: venv\Scripts\activate
pip install matplotlib

The virtual environment isn’t strictly required for a script this small, but it keeps your global Python installation clean and matches how you’d structure a larger project. Create one file, pity.py, and keep it open for the next several steps.

Step 3: Encode the base rate constants

Start with the numbers from Step 1 as named constants at the top of the file. Hardcoding these as magic numbers later would make the code unreadable and hard to audit.

# pity.py
BASE_RATE = 0.006          # 0.6% flat 5-star rate, pulls 1-73
SOFT_PITY_START = 74       # rate begins climbing here
HARD_PITY = 90              # guaranteed 5-star by this pull
PRIMOGEMS_PER_PULL = 160    # 1 Intertwined Fate costs 160 Primogems
FIFTY_FIFTY = 0.5           # chance the first 5-star is the featured unit

Note what’s deliberately missing: an exact per-pull escalation curve for pulls 74 through 89. HoYoverse has never published the precise soft-pity ramp formula, only the two anchor points (starts at 74, guaranteed by 90). Community datamines estimate a curve, but since that specific escalation isn’t officially confirmed, this calculator uses a documented, conservative substitute in the next step instead of guessing at unpublished numbers.

Step 4: Build the conservative pull-simulation model

Here’s the trick that keeps this calculator honest: instead of inventing a soft-pity curve, we model pulls 1 through 89 at the flat, officially-confirmed 0.6% rate, with pull 90 forced to a guaranteed hit. Because the real soft-pity mechanic pushes your true odds above 0.6% starting at pull 74, this flat-rate model always predicts more pulls than you’ll need in practice. That means every number this calculator outputs is a safe, conservative upper bound, never an optimistic underestimate.

import random

def simulate_single_pull_run(rng=random):
    """Simulate pulls until a 5-star drops. Returns the pull count."""
    for pull_number in range(1, HARD_PITY + 1):
        if pull_number == HARD_PITY:
            return pull_number  # hard pity guarantee
        if rng.random() < BASE_RATE:
            return pull_number
    return HARD_PITY

Run this a few times interactively and you'll see it return numbers anywhere from 1 to 90, with most runs clustering in the 40-90 range. That's expected: a 0.6% flat rate is genuinely rare, and the guarantee at 90 is what's saving you from a run of catastrophically bad luck.

Step 5: Layer the 50/50 and guarantee mechanic on top

Getting a 5-star isn't the goal — getting the featured 5-star is. Wrap the single-pull simulator in a second function that handles the 50/50 coin flip and the guarantee-on-loss rule.

def simulate_to_featured_character(rng=random):
    """Simulate full pulls needed to secure the featured 5-star."""
    total_pulls = simulate_single_pull_run(rng)
    won_fifty_fifty = rng.random() < FIFTY_FIFTY

    if won_fifty_fifty:
        return total_pulls

    # Lost the 50/50 — next 5-star is guaranteed featured
    total_pulls += simulate_single_pull_run(rng)
    return total_pulls

This function alone answers the question most players actually care about. Call it a few thousand times and average the results, and you have an empirical expected value that should land close to the analytical one we derive by hand in Step 7.

Step 6: Run a Monte Carlo batch and print the distribution

One simulated player tells you nothing useful. Run 50,000 of them and you get a distribution you can actually trust.

def run_batch(trials=50_000, seed=42):
    rng = random.Random(seed)
    results = [simulate_to_featured_character(rng) for _ in range(trials)]
    average = sum(results) / len(results)
    worst = max(results)
    best = min(results)
    return {
        "trials": trials,
        "average_pulls": round(average, 2),
        "worst_case_pulls": worst,
        "best_case_pulls": best,
    }

if __name__ == "__main__":
    stats = run_batch()
    print(f"Trials: {stats['trials']:,}")
    print(f"Average pulls to featured 5-star: {stats['average_pulls']}")
    print(f"Best case observed: {stats['best_case_pulls']} pulls")
    print(f"Worst case observed: {stats['worst_case_pulls']} pulls")

Save and run it with python3 pity.py. A fixed seed (42) is used here so your output is reproducible — remove the seed argument if you want fresh randomness on every run.

Output example: what your terminal should show

$ python3 pity.py
Trials: 50,000
Average pulls to featured 5-star: 104.61
Best case observed: 1 pulls
Worst case observed: 180 pulls

Your exact average will vary slightly by seed, but it should land close to 104-105 pulls across a 50,000-trial run. That number is the simulation confirming the analytical derivation we're about to walk through by hand, which is the real point of building both: if the simulated average and the hand-calculated expected value disagree by more than a percent or two, one of them has a bug.

Step 7: The expected-value math, worked step by step

This is the section that matters if you want to understand the number instead of just trusting a script. We're calculating the expected number of pulls to secure the featured character, using only the officially confirmed 0.6% base rate and the 90-pull hard pity as inputs.

Step 7a — expected pulls to ANY 5-star. For a stopping process like this, the expected value equals the sum of survival probabilities: E[N] = Σ P(N > k) for k = 0 to 89. Under our flat-rate model, P(N > k) = (1 − 0.006)^k for k up to 89, since the process is forced to stop at pull 90.

E[N] = Σ (0.994)^k   for k = 0 to 89
     = [1 − (0.994)^90] / (1 − 0.994)
     = [1 − 0.5818] / 0.006
     ≈ 69.7 pulls

Step 7b — expected pulls to the FEATURED 5-star, accounting for the 50/50. Half the time you win the coin flip and you're done in ~69.7 pulls. The other half, you lose, then need a second independent draw (pity resets after any 5-star) to hit the guaranteed featured pull — another ~69.7 pulls on top of the first.

E[featured] = 0.5 × E[N] + 0.5 × (E[N] + E[N])
            = 0.5 × 69.7 + 0.5 × 139.4
            = 1.5 × E[N]
            = 1.5 × 69.7
            ≈ 104.6 pulls (rounds to 105)

That 1.5× multiplier is the key insight: because losing the 50/50 costs you a full second pity cycle, the expected cost of a featured banner character is 50% higher than the expected cost of just any 5-star. This is the same reason experienced players talk about "50/50 tax" — it isn't a fee HoYoverse charges, it's simply what the math produces when a coin flip gates a guaranteed outcome.

Convert to Primogems by multiplying by the 160-per-pull cost: 105 pulls × 160 = 16,800 Primogems expected. Since Genesis Crystals convert to Primogems at a fixed 1:1 ratio, that's also 16,800 Genesis Crystals if you're paying with cash currency directly.

Step 8: Add the currency-cost function to the calculator

Now wire the pull counts into a Primogem/Genesis Crystal cost, and add USD pricing using the real Genesis Crystal pack rates.

# Official US Genesis Crystal pack tiers (non-first-time), 1 crystal = 1 Primogem
GENESIS_CRYSTAL_PACKS = [
    (0.99, 60),
    (4.99, 330),
    (14.99, 1090),
    (29.99, 2240),
    (49.99, 3880),
    (99.99, 8080),   # best per-crystal rate
]

def cheapest_usd_for_primogems(primogem_needed):
    usd_per_crystal, crystals_in_pack = GENESIS_CRYSTAL_PACKS[-1]
    rate = usd_per_crystal / crystals_in_pack
    return round(primogem_needed * rate, 2)

def pulls_to_cost(pulls):
    primogems = pulls * PRIMOGEMS_PER_PULL
    usd = cheapest_usd_for_primogems(primogems)
    return primogems, usd

if __name__ == "__main__":
    for label, pulls in [
        ("Expected (EV)", 105),
        ("Worst case (hard pity twice)", 180),
    ]:
        primo, usd = pulls_to_cost(pulls)
        print(f"{label}: {pulls} pulls = {primo:,} Primogems ≈ ${usd}")

The cheapest_usd_for_primogems function deliberately uses the largest $99.99 pack's per-crystal rate, because that's the best value tier available on repeat purchases. It ignores the one-time first-purchase bonus, which we'll cover separately in Step 12, since it isn't a sustainable rate you can rely on for a second character.

It's worth seeing the full pack ladder laid out before we move on, because the per-crystal rate isn't flat across tiers — buying small keeps you flexible but costs more per pull, while buying the top tier gets you the best rate but demands the biggest single charge. This is the same official US pricing published on HoYoverse's own Genesis Crystal storefront.

USD PriceBase CrystalsLoyalty BonusTotal CrystalsEffective $/Crystal
$0.9960060$0.0165
$4.9930030330$0.0151
$14.999801101,090$0.0138
$29.991,9802602,240$0.0134
$49.993,2806003,880$0.0129
$99.996,4801,6008,080$0.0124

Notice the pattern: every step up the ladder shaves a fraction of a cent off the per-crystal rate, but the gap between the cheapest and most expensive tier is smaller than most players assume, only about 25% between the $0.99 pack and the $99.99 pack. That's why the calculator's default assumption (top-tier pricing) is a reasonable approximation even for players who actually buy a mix of pack sizes rather than always maxing out the cart.

The pity-cost table: pulls, Primogems, and real dollars

Putting the simulation, the hand-derived expected value, and the official Genesis Crystal pricing together produces the full cost table below. This is the core reference for the whole article — everything above was building toward these five rows.

ScenarioPullsPrimogems / Genesis CrystalsCost at best cash rate ($99.99 pack)
Average pulls to ANY 5-star (base-rate model)7011,200$138.60
50/50 WIN — featured secured on first 5-star7011,200$138.60
50/50 LOSS, then guarantee — featured secured on second 5-star14022,400$277.20
Expected value (weighted average across both branches)10516,800$207.90
Absolute worst case — hard pity twice (100% guaranteed)18028,800$356.40

Two things to note about this table. First, the "expected value" row (105 pulls, $207.90) is the number to budget around — it's the probability-weighted average of every possible outcome, so half of all players will spend less and half will spend more. Second, the worst-case row ($356.40) is a hard ceiling: no matter how unlucky you are, 180 pulls guarantees the character, full stop, because of hard pity.

Step 9: Visualize the pull distribution (optional but useful)

A histogram makes the shape of the distribution click in a way the raw average doesn't. This step uses the matplotlib dependency installed back in Step 2.

import matplotlib.pyplot as plt

def plot_distribution(trials=50_000, seed=42):
    rng = random.Random(seed)
    results = [simulate_to_featured_character(rng) for _ in range(trials)]
    plt.hist(results, bins=range(1, 182), color="#4a90d9")
    plt.axvline(105, color="red", linestyle="--", label="Expected value (105)")
    plt.xlabel("Pulls to secure featured 5-star")
    plt.ylabel("Number of simulated players")
    plt.title("Genshin Impact Featured Banner: Pull Distribution (n=50,000)")
    plt.legend()
    plt.savefig("pity_distribution.png", dpi=150)
    print("Saved chart to pity_distribution.png")

plot_distribution()

Run this and open the resulting PNG. You'll see a long left tail (lucky players hitting the character in under 40 pulls) and a hard cliff at pull 180, where the distribution simply stops because hard pity forces a resolution. That cliff is the visual proof that this system, unlike a true unbounded random draw, has a mathematically guaranteed ceiling.

Step 10: Add a simple CLI so anyone can run it without reading code

Wrap everything in an argument parser so the calculator is actually usable by someone who doesn't want to edit Python source.

import argparse

def main():
    parser = argparse.ArgumentParser(description="Genshin Impact pity/EV calculator")
    parser.add_argument("--trials", type=int, default=50_000)
    parser.add_argument("--seed", type=int, default=None)
    args = parser.parse_args()

    rng = random.Random(args.seed)
    results = [simulate_to_featured_character(rng) for _ in range(args.trials)]
    avg_pulls = sum(results) / len(results)
    primo, usd = pulls_to_cost(round(avg_pulls))

    print(f"Trials: {args.trials:,}")
    print(f"Average pulls: {avg_pulls:.2f}")
    print(f"Average cost: {primo:,} Primogems (~${usd})")
    print(f"Worst case: 180 pulls (28,800 Primogems, ~$356.40)")

if __name__ == "__main__":
    main()

Run python3 pity.py --trials 100000 to double the sample size, or add --seed 7 to reproduce a specific run for debugging. This is the version worth keeping around if you plan to reuse the tool for future banners.

Step 11: Factor in Welkin Moon for a realistic monthly budget

The Genesis Crystal packs aren't the cheapest way to fund pulls — the Blessing of the Welkin Moon monthly pass is. At $4.99, Welkin grants 300 Primogems immediately plus 90 Primogems per day for 30 days, for a total of 3,000 Primogems per month (18.75 pulls' worth). Per Primogem, that works out to roughly $0.0017, compared to about $0.0124 per Primogem-equivalent crystal on the largest one-off pack — Welkin is over 7x more cost-efficient per pull.

Add this as a comparison function so your calculator reflects how players actually spend, rather than assuming everyone buys the $99.99 pack in bulk.

WELKIN_USD = 4.99
WELKIN_PRIMOGEMS_PER_MONTH = 3000  # 300 immediate + 90/day x 30 days

def months_of_welkin_needed(primogem_target):
    return primogem_target / WELKIN_PRIMOGEMS_PER_MONTH

target = 16800  # expected-value Primogem cost from the table above
months = months_of_welkin_needed(target)
print(f"Welkin alone: {months:.1f} months (${months * WELKIN_USD:.2f}) to reach {target:,} Primogems")

That prints roughly 5.6 months and about $27.94 in Welkin subscriptions to passively accumulate the expected-value cost of a featured character — dramatically cheaper than direct crystal purchases, but only realistic if you have that much time before the banner rotates out.

Step 12: Handle the first-time purchase bonus correctly

Every Genesis Crystal pack tier doubles its base crystal amount the very first time you buy it, a one-time-per-tier bonus. The largest pack, for example, jumps from 8,080 crystals (with the standard loyalty bonus) to 12,960 crystals on your first purchase — the doubling applies to the base 6,480 amount, not the already-bonused total. That drops the effective rate to roughly $0.0077 per crystal, about 38% cheaper than the standard repeat rate.

The catch: this bonus fires once per account per pack tier, forever. It's real money saved on your very first top-up, but it isn't a rate you can build a repeatable EV model around, which is exactly why Step 8's calculator function ignores it by default. If you're a new player who hasn't spent yet, do the math on stacking all six first-time bonuses before your first pull session — that's a one-time discount worth claiming deliberately rather than accidentally.

Common pitfalls when building or using this calculator

  • Forgetting pity carries across banners. If you're at pull 60 with no 5-star when a banner rotates, you're still at pull 60 on the new banner, not pull 0. Feeding a calculator "pulls remaining until next banner" instead of your running total will throw off every downstream number.
  • Confusing character pity with weapon pity. The weapon banner uses an 80-pull hard pity and a separate Epitomized Path fate-point system, not the 90-pull 50/50 modeled here. Don't reuse this script's constants for weapon banner math without changing them.
  • Assuming the flat 0.6% model is the "real" curve. It isn't — it's a deliberately conservative stand-in for an unpublished soft-pity ramp. Treat every output as an upper bound on cost, not a precise prediction.
  • Using the first-time bonus rate as your baseline. It only applies once per pack tier ever. Basing a "cost per character" projection on that rate will make every subsequent character look far cheaper than it actually is.
  • Ignoring the 50/50 multiplier. A lot of casual math online just multiplies the average-pulls-to-any-5-star (≈70) by the Primogem cost, skipping the 1.5x factor from Step 7b entirely. That understates the true expected cost by a third.
  • Not fixing a random seed during testing. Without seed=, every debug run gives different numbers, making it hard to tell whether a code change fixed a bug or just got lucky.
  • Treating simulated results from small trial counts as reliable. Below about 5,000 trials, the average pull count can swing by several points run to run. Use 50,000+ trials before trusting the output.
  • Mixing up Genesis Crystals and Primogems in code. They convert 1:1, so it's easy to accidentally double-count currency if a function takes one and a caller passes the other without checking units.

Troubleshooting

  • Script throws "ModuleNotFoundError: No module named 'matplotlib'". You skipped the pip install in Step 2, or you're running outside the virtual environment. Activate the venv and rerun pip install matplotlib.
  • Average pulls printed is way above 105 (like 130+). Check that simulate_single_pull_run actually returns at pull 90 rather than looping past it — an off-by-one in the range will silently break the hard pity cap.
  • Average pulls printed is suspiciously low (under 90). You likely forgot the second draw on a 50/50 loss in simulate_to_featured_character, so the script is only modeling "any 5-star," not the featured one.
  • Histogram in Step 9 shows a smooth bell curve with no cliff at 180. The hard pity cap isn't being enforced in the plotting function — verify you're calling the same simulator function used in Step 6, not a copy that dropped the guarantee logic.
  • CLI argument --seed doesn't reproduce the same output twice. Make sure you're passing the same rng instance into every function call rather than mixing calls to the global random module and a seeded random.Random() object.
  • USD output looks too high compared to what you actually spent. You're likely comparing against a run that included the first-time bonus. Re-check which pack tier and bonus state you're pricing against — Step 12 changes the effective rate significantly.
  • Script runs but takes several seconds at 50,000+ trials. Pure-Python loops are slow for this. If you need 500,000+ trials, vectorize with NumPy's random.random(size=n) instead of a Python for loop.
  • Numbers don't match a friend's spreadsheet. Ask what soft-pity curve they used. Most community spreadsheets use a datamined, unofficial escalation curve rather than this article's conservative flat-rate substitute, so a small gap (a few percent) is expected and doesn't mean either is "wrong."

Advanced tips for extending the calculator

Once the base version works, a few extensions make it genuinely useful for planning real spending instead of just satisfying curiosity.

  1. Feed it your current pity count. Change simulate_single_pull_run to accept a starting_pull parameter so the loop begins wherever you currently sit instead of assuming pull 1. This turns the tool from "average cost for a random player" into "my specific cost from here."
  2. Model captured constellations or refinements separately. If you're pulling for duplicates (constellations) after already owning the character, there's no 50/50 to worry about — every 5-star is a win, so you can drop the guarantee branch entirely and just use simulate_single_pull_run directly.
  3. Cross-check against your real wish history. Export your account's wish history log and compute your personal empirical average across every banner you've pulled on. Comparing your real average against the theoretical ~70-pull figure is a good sanity check on whether you've been unusually lucky or unlucky.
  4. Batch-price multiple characters. Loop the EV calculation across a list of upcoming banners with a shared Primogem pool, and simulate which characters you can realistically afford before your currency runs out.

Complete working project

Here's the full script combining every step above into one file. Save this as pity.py and run it directly.

import random
import argparse

BASE_RATE = 0.006
HARD_PITY = 90
PRIMOGEMS_PER_PULL = 160
FIFTY_FIFTY = 0.5

GENESIS_CRYSTAL_PACKS = [
    (0.99, 60), (4.99, 330), (14.99, 1090),
    (29.99, 2240), (49.99, 3880), (99.99, 8080),
]

WELKIN_USD = 4.99
WELKIN_PRIMOGEMS_PER_MONTH = 3000


def simulate_single_pull_run(rng):
    for pull_number in range(1, HARD_PITY + 1):
        if pull_number == HARD_PITY:
            return pull_number
        if rng.random() < BASE_RATE:
            return pull_number
    return HARD_PITY


def simulate_to_featured_character(rng):
    total_pulls = simulate_single_pull_run(rng)
    if rng.random() < FIFTY_FIFTY:
        return total_pulls
    total_pulls += simulate_single_pull_run(rng)
    return total_pulls


def cheapest_usd_for_primogems(primogem_needed):
    usd_per_crystal, crystals_in_pack = GENESIS_CRYSTAL_PACKS[-1]
    rate = usd_per_crystal / crystals_in_pack
    return round(primogem_needed * rate, 2)


def pulls_to_cost(pulls):
    primogems = pulls * PRIMOGEMS_PER_PULL
    return primogems, cheapest_usd_for_primogems(primogems)


def months_of_welkin_needed(primogem_target):
    return primogem_target / WELKIN_PRIMOGEMS_PER_MONTH


def main():
    parser = argparse.ArgumentParser(description="Genshin Impact pity/EV calculator")
    parser.add_argument("--trials", type=int, default=50_000)
    parser.add_argument("--seed", type=int, default=None)
    args = parser.parse_args()

    rng = random.Random(args.seed)
    results = [simulate_to_featured_character(rng) for _ in range(args.trials)]
    avg_pulls = sum(results) / len(results)
    primo, usd = pulls_to_cost(round(avg_pulls))
    welkin_months = months_of_welkin_needed(primo)

    print(f"Trials: {args.trials:,}")
    print(f"Average pulls: {avg_pulls:.2f}")
    print(f"Average cost: {primo:,} Primogems (~${usd} via crystal packs)")
    print(f"Same cost via Welkin only: ~{welkin_months:.1f} months (~${welkin_months * WELKIN_USD:.2f})")
    print(f"Worst case: 180 pulls (28,800 Primogems, ~$356.40)")


if __name__ == "__main__":
    main()

That's the complete tool: pity simulation, expected-value math baked in as a cross-check, real US pricing, and a Welkin comparison, all in under 60 lines. Test it by running it twice with the same seed (python3 pity.py --seed 1) and confirming you get identical output both times, then run it without a seed a few times and watch the average pull count settle close to 105 as long as you keep the trial count at 50,000 or above. If those two checks pass, the calculator is behaving correctly and you can trust the numbers it produces for your own planning.

How this compares to other gacha calculators worth checking

You don't have to build your own — a few community tools already do a version of this. Paimon.moe's wish counter tracks your real pull history against your account. Lootcalc's wish-pity calculator models forward planning from your current pity count. Miniwebtool's gacha pity calculator offers a generic version for other gacha titles using similar mechanics. What none of them show you is the derivation this article walks through, which is the actual value of building your own: once you understand the 1.5x multiplier and the hard-pity ceiling, you can sanity-check any tool's output instead of taking it on faith.

The same core method — flat base rate, hard pity cap, coin-flip guarantee multiplier — applies with different constants to plenty of other live-service gacha systems, which is worth knowing given that regulators are paying closer attention to loot box and gacha odds disclosure generally (see our coverage of the EU's loot box PEGI 16 crackdown for the regulatory side of this).

Why published rates matter more than they used to

None of the math in this article would be possible if HoYoverse hadn't published the base rate, the pity thresholds, and the guarantee mechanic in the first place. That's not universal in the gacha genre. Plenty of live-service games disclose a vague "increased chance" near a soft-pity threshold without ever naming the starting pull or the exact base percentage, which makes an honest EV calculation impossible for players and leaves regulators to fill the gap with blanket rules instead of case-by-case audits.

That gap is exactly what's driving policy in this space right now. Regulators across multiple markets have been tightening disclosure requirements for randomized in-game purchases, treating loot boxes and gacha pulls as adjacent problems even when the mechanics differ in the details. For the regulatory side of that story, including the specific age-rating and disclosure changes rolling out in 2026, see our coverage of the EU's loot box PEGI 16 push linked below.

The practical takeaway for building tools like this one: always build against the anchor points a publisher has actually confirmed in writing, and be explicit anywhere you're filling a gap with an assumption. The calculator in this guide does that by using a conservative flat rate instead of an unofficial datamined curve, and by labeling the first-time purchase bonus as a one-time exception rather than baking it into the default EV. That's the difference between a calculator you can trust and one that just looks convincing.

Frequently asked questions

Based on the officially confirmed 0.6% base rate and 90-pull hard pity, the expected value works out to roughly 105 pulls, accounting for the 50/50 mechanic. That's 16,800 Primogems, or about $207.90 at the best available cash rate.

180 pulls. That's hard pity (90 pulls) hit twice in a row in the worst case: once to draw a non-featured 5-star and lose the 50/50, and once more to draw the guaranteed featured character. This is a hard ceiling, not a probability — you cannot need more than 180 pulls.

How much does 180 pulls cost in real money?

180 pulls costs 28,800 Primogems (or Genesis Crystals at a 1:1 conversion), which comes out to about $356.40 at the best available per-crystal rate, using the $99.99 / 8,080-crystal Genesis Crystal pack.

Is the Welkin Moon pass worth buying over Genesis Crystal packs?

Yes, by a wide margin on a per-Primogem basis. Welkin costs $4.99 for 3,000 Primogems across 30 days (300 immediate plus 90/day), working out to about $0.0017 per Primogem versus roughly $0.0124 per Primogem-equivalent on the largest one-off crystal pack. The tradeoff is time: Welkin pays out slowly, so it isn't useful if you need Primogems before a banner ends.

Does pity carry over between different character banners?

Yes. Pity is tracked per banner category (character event banners share one pity counter), and it persists across banner rotations. It only resets when you actually pull a 5-star item. Your pull count doesn't reset just because a new character banner started.

Why does the calculator use a flat 0.6% rate instead of the real soft-pity curve?

Because HoYoverse has never officially published the exact per-pull escalation formula for pulls 74 through 89, only the two confirmed anchor points (soft pity starts at 74, hard pity guarantees a hit by 90). Using the flat rate for that whole range is a deliberately conservative approximation that slightly overstates the expected pull count, rather than guessing at unpublished numbers.

Is this calculator accurate for the weapon banner too?

No, not without changes. The weapon banner uses an 80-pull hard pity and the Epitomized Path fate-point system instead of a 50/50 coin flip, which is a different mechanic entirely. You'd need to swap the constants and rewrite the guarantee logic to model it accurately.

What does the first-time purchase bonus actually save you?

On the largest pack, the first-time bonus roughly doubles the base crystal amount, taking the $99.99 pack from 8,080 crystals (standard rate) to 12,960 crystals on your very first purchase of that tier — about 38% more crystals for the same price. It only applies once per pack tier per account, so it isn't repeatable.

Can I use this same method for Honkai Star Rail or other HoYoverse games?

The overall shape of the method (flat base rate, hard pity cap, 50/50-plus-guarantee, expected value as a survival-probability sum) generalizes to any gacha system built the same way. But the specific constants in this article, 0.6% base rate, pull 74 soft pity, pull 90 hard pity, and 160 currency per pull, are Genshin Impact's confirmed numbers only. Don't reuse them for a different game without verifying that game's own published rates first, since even closely related titles from the same publisher can use different thresholds.