Thanks! I looked through the code. I decided to check out ChatGPT 5 coding and math skills out for the first time, and it believes it found the error. It seems plausible based on its reasoning.
My prompt:
"I have a website for keeping a database of oneâs pokemon cards from the game PokĂ©mon Trading Card Game Pocket. One of the functions of the website is to access the probability of getting a new card from a specific pack based on your current cards from that set the pack is part of.
A user noticed that the probabilities seems to be too high when the collection of cards for that set becomes significant. It seems the calculation is biased towards too high a probability. Can you check the code and math from this github link: tcg-pocket-collection-tracker/frontend/src/lib/CardsDB.ts at main · marcelpanse/tcg-pocket-collection-tracker · GitHub and analyze the code relation to probabilities for getting a new card one doesnât already own a card of."
It gave the following comprehensive answer after thinking and analysing for almost 2 minutes (link for proper display of math etc: https://chatgpt.com/c/68cecb7b-994c-8329-8dd2-0c4c6b484973)
Short answer: yes â thereâs a real âtoo-highâ bias in the way the probability is computed right now, and it shows up more and more as a userâs collection grows.
Hereâs what I could verify and infer from the repo and the thread where the maintainer points to the relevant function:
- The probability code lives in
pullRate(...) in frontend/src/lib/CardsDB.ts (commit 0e2f93eâŠ). The function first builds the set of cards in the pack and filters out âownedâ ones to get missingCards. Then it computes the âchance to get a new card.â (TCG Pocket Community)
- The code clearly aggregates the candidate pool at the pack level (
cardsInPack = expansion.cards.filter(c => c.pack === pack.name || c.pack === 'everypack')) and then narrows to missingCards (with optional rarityFilter). Whatâs not done (and what the pack-opening UI of the game actually uses) is slot-by-slot weighting by the packâs rarity distribution for each of the 5 slots. The forum note by the author also hints that the real math differs âbecause there are different pull rates for each slot,â which is exactly the missing piece. (TCG Pocket Community)
Where the bias comes from
- Using a single blended âmissing/totalâ for the whole pack pool
If you take p = missingCards.length / cardsInPack.length and then do something like 1 - (1 - p)^5, youâll overstate the true chance when most of the cards youâre missing are rarer than the ones you already own. As your collection grows, commons/uncommons are mostly completed, and the remaining missing cards are concentrated in lower-probability rarities â but the blended p still treats every card as if it had equal weight in every slot. That pushes the displayed probability upward relative to the truth.
- Not accounting for per-slot rarity menus
In PokĂ©mon TCG Pocket, each of the 5 slots draws from different rarity menus with different odds (e.g., slots 1â3 are âlow rarities,â 4 has a mid/rare menu, 5 has the ârare/crownâ menu, etc.). The correct per-slot probability of drawing any new card is:
ps(new)=ârâRsPrâĄ(rarity=r in slot s)Ă#missing in (râ©pack)#total in (râ©pack)p_s(\text{new})=\sum_{r\in R_s} \Pr(\text{rarity}=r\text{ in slot } s)\times \frac{#\text{missing in }(r\cap \text{pack})}{#\text{total in }(r\cap \text{pack})}
Then the pack probability is:
PrâĄ(at least one new)=1ââs=15(1âps(new))\Pr(\text{at least one new}) = 1 - \prod_{s=1}^{5}\bigl(1 - p_s(\text{new})\bigr)
If you instead swap in a single missing/total for all slots, you inflate the result precisely when your missing set is skewed toward rarer slots. (This matches the user reports that the number âlooks too high once my collection is large.â) The maintainerâs comment in the forum acknowledges slot pull-rates matter; they arenât actually folded into the displayed number right now. (TCG Pocket Community)
What to change (concrete fix)
Inside pullRate(...), after youâve built cardsInPack and missingCards:
- Build rarity-indexed pools for the current pack, e.g.:
const poolByRarity = groupBy(cardsInPack, c => c.rarity);
const missingByRarity = groupBy(missingCards, c => c.rarity);
- For the
pack youâre evaluating, use its slot definitions (however you model them â typically pack.slots with something like [{ rarity: '1âŠ', p: 0.XX }, âŠ] or a map of rarityâweight per slot). For each slot s:
const pNewSlot =
sum_over_rarities_in_slot( slotWeight[s][r] *
( (missingByRarity[r]?.length ?? 0) / (poolByRarity[r]?.length ?? 0) )
);
- Combine slots with the complement rule:
const pAtLeastOneNew = 1 - pNewSlotArray.reduce((acc, p) => acc * (1 - p), 1);
- Keep your
rarityFilter and numberFilter logic exactly as you already do, but apply them before building the rarity pools so they naturally propagate into the fractions.
This change alone removes the âtoo-highâ bias.
Quick self-check you can run
- Take a pack where slots 1â3 only pull from 1â3âŠ, slot 4 from 4âŠ/â
, slot 5 from â
/Crown. Make a test account that owns nearly all low-rarity cards but is still missing many â
/Crown.
- The blended method will show a high
p (most of the packâs pool is âownedâ but a non-trivial slice is âmissing,â so missing/total looks healthy and gets multiplied across 5 slots).
- The slot-aware method will show a materially lower
p because slots 1â3 have almost zero chance of new, so nearly the whole chance must come from slots 4â5 only.
Youâll see the gap widen the more âtop-heavyâ the missing collection is.
If you want, paste your pack/slot schema (how you store per-slot rarity odds) and Iâll drop a ready-to-merge patch for pullRate(...) that uses the exact structure you have. In any case, the math above is what you want to implement. (TCG Pocket Community)
Not sure if itâs correct or not, but putting it out there for someone to check. I might look again at it later.