So I was annoyed that my opponents keep getting out a Gardevoir really really fast in this game and it struck me as odd since there aren't really any search cards compared to the traditional TCG and there's about no way to draw into these cards naturally.
First thought was to make a giant Excel spreadsheet using combinatorials to calculate the exact probabilities since the game is so small and there's relatively few card interactions you can have in each game. Several hours later after realizing there are too many branching factors ("how does the probability of drawing X card change when drawing X card, I.e. pokeball"), I gave up.
Actually no, I stuck it all into chatgpt and it recommended a Monte Carlo search algorithm to "simulate" the relatively simple mechanics of the game. To my surprise I was even able to pretty much generate the full simulation code with a series of prompts.
In this simulation I run a rudimentary deck: 2 Mewtwo, 2 ralts, 2 sigilyph, 2 kirlia, 2 Gardevoir, 2 pokeball, 2 professor, 2 slab, 2 x speed, and 2 other random cards. I find that with all of these cards being used at once, I get a success rate (of getting t3 gardevoir) as high as 54.446%.
This guess seems to generally line up with some of my online matches, as well as the rough combinatorials I used as an estimate beforehand. Feel free to tweak the code if you want to experiment. normally I'd be really proud of something like this, but given that I was able to do this in less than an hour using just a few o1 model chatgpt prompts is absolutely crazy.
First thought was to make a giant Excel spreadsheet using combinatorials to calculate the exact probabilities since the game is so small and there's relatively few card interactions you can have in each game. Several hours later after realizing there are too many branching factors ("how does the probability of drawing X card change when drawing X card, I.e. pokeball"), I gave up.
Actually no, I stuck it all into chatgpt and it recommended a Monte Carlo search algorithm to "simulate" the relatively simple mechanics of the game. To my surprise I was even able to pretty much generate the full simulation code with a series of prompts.
Python:
import random
NUM_SIMULATIONS = 100_000
# -----------------------------
# Game Configuration & Helpers
# -----------------------------
def build_deck():
"""
Returns a list of all cards in the deck (20 cards total, because Speed=2).
"""
deck = []
deck += ["BasicCat"] * 2
deck += ["BasicKid"] * 2
deck += ["BasicSigil"] * 2
deck += ["Stage1"] * 2
deck += ["Stage2"] * 2
deck += ["Ball"] * 2
deck += ["Professor"] * 2
deck += ["Slab"] * 2
deck += ["Speed"] * 2 # 2 copies of Speed
deck += ["Trash3"] * 1
deck += ["Trash4"] * 1
return deck
def retreat_cost(card_name):
"""
Returns the retreat cost (in energy) for the given card.
"""
if card_name == "BasicCat":
return 2
elif card_name == "BasicKid":
return 1
elif card_name == "BasicSigil":
return 1
return 99 # fallback (shouldn't happen in this scenario)
def draw_card(deck):
"""Draw one card from the top of the deck (if any left)."""
if deck:
return deck.pop(0)
return None
def play_ball(deck, hand):
"""
Ball: Grab (at random) one of {BasicCat, BasicKid, BasicSigil} from the deck, if any remain.
"""
basic_targets = [c for c in deck if c in ["BasicCat","BasicKid","BasicSigil"]]
if not basic_targets:
return
chosen = random.choice(basic_targets)
deck.remove(chosen)
hand.append(chosen)
def play_slab(deck, hand):
"""
Slab: Look at the top card of the deck.
If it's in [BasicCat,BasicKid,BasicSigil,Stage1,Stage2], put it in your hand.
Otherwise, put that top card on the bottom of the deck.
"""
if len(deck) == 0:
return
top_card = deck[0]
if top_card in ["BasicCat","BasicKid","BasicSigil","Stage1","Stage2"]:
# take it into hand
hand.append(draw_card(deck))
else:
# move top card to bottom
deck.append(draw_card(deck))
def play_professor(deck, hand):
"""
Professor: Draw 2 cards from the deck. (Only 1 per turn allowed.)
"""
c1 = draw_card(deck)
if c1:
hand.append(c1)
c2 = draw_card(deck)
if c2:
hand.append(c2)
# -----------------------
# Simulation of One Game
# -----------------------
def simulate_one_game():
# 1) Build and shuffle the deck
deck = build_deck()
random.shuffle(deck)
# 2) Choose a random starter among [BasicCat, BasicKid, BasicSigil]
starter_options = ["BasicCat","BasicKid","BasicSigil"]
start_card = random.choice(starter_options)
deck.remove(start_card)
# 3) Draw 4 more cards for opening hand
hand = [draw_card(deck) for _ in range(4)]
# Active
active = start_card
active_energy = 0 # how many energies attached to the active
# Bench: a list of tuples (pokemon_name, energy_attached)
bench = []
# Put any extra basics on bench
for c in hand[:]: # copy because we'll remove from hand if we bench them
if c in ["BasicCat","BasicKid","BasicSigil"]:
if len(bench) < 3:
bench.append((c, 0)) # 0 energy on bench initially
hand.remove(c)
# Turn order: 50% chance to go first
# If you go first, you CANNOT attach an energy on Turn 1.
go_first = (random.random() < 0.5)
# Track if we can still attach 1 energy this turn (resets each turn)
can_attach_energy_this_turn = True
# -----------------------
# Helper Functions
# -----------------------
def attach_energy_to_active():
"""
Attach one energy to the active Pokémon (if we can).
"""
nonlocal active_energy, can_attach_energy_this_turn
if can_attach_energy_this_turn:
active_energy += 1
can_attach_energy_this_turn = False
def attach_energy_to_bench_sigil():
"""
Attach one energy to the first bench Sigil that doesn't have energy
(or to any Sigil, if we want). For simplicity, we pick the first
Sigil that has 0 energy. If none, do nothing.
"""
nonlocal bench, can_attach_energy_this_turn
if not can_attach_energy_this_turn:
return False
for i, (b_poke, b_energy) in enumerate(bench):
if b_poke == "BasicSigil":
# Attach
bench[i] = (b_poke, b_energy + 1)
can_attach_energy_this_turn = False
return True
return False
def bench_has_sigil_with_energy_or_can_attach():
"""
Return True if there's a benched Sigil that already has >=1 energy,
OR if we can attach an energy to a Sigil that is currently at 0
(assuming we still have our once-per-turn energy available).
"""
# Already has >= 1 energy?
for (b_poke, b_energy) in bench:
if b_poke == "BasicSigil" and b_energy >= 1:
return True
# Or can attach to a 0-energy Sigil right now?
if can_attach_energy_this_turn:
for (b_poke, b_energy) in bench:
if b_poke == "BasicSigil" and b_energy == 0:
return True
return False
def play_items_and_supporters(deck, hand, turn):
"""
Naive approach for Items/Supporters:
1) Play all Balls
2) Play all Slabs
3) Play exactly one Professor if present
Then decide on Speed usage *only if*:
"Playing Speed will allow the active to switch to a Benched Sigil,
AND that Benched Sigil either already has 1 energy or can still get one."
Returns: speed_count (int) = number of Speed cards actually used.
"""
nonlocal active, active_energy, bench
# 1) Play all Balls
to_play = hand[:]
for c in to_play:
if c == "Ball":
hand.remove(c)
play_ball(deck, hand)
# 2) Play all Slabs
to_play = hand[:]
for c in to_play:
if c == "Slab":
hand.remove(c)
play_slab(deck, hand)
# 3) Play one Professor (if any in hand)
if "Professor" in hand:
hand.remove("Professor")
play_professor(deck, hand)
# Now decide about Speed usage
# Step A: If active == BasicSigil, no point in using Speed to retreat into Sigil :-)
# The user rule states: "Only play Speed that turn if playing Speed
# will allow the active to switch to BasicSigil, and BasicSigil has or can get energy."
# If the active is already Sigil, we don't need to do that, so we skip Speed usage.
if active == "BasicSigil":
return 0
# Step B: Check if there's a benched Sigil that either
# (a) already has energy or (b) can get it this turn
if not bench_has_sigil_with_energy_or_can_attach():
# If no bench Sigil that can be energized, we do NOT use speed
return 0
# Step C: See how many Speed cards are in hand
speed_in_hand = sum(1 for c in hand if c == "Speed")
if speed_in_hand == 0:
return 0
# Step D: Determine how many Speed are needed to reduce cost so we can retreat
cost = retreat_cost(active)
# If we have cost <= active_energy already, we don't *need* Speed
# from the standpoint of enabling retreat. But let's interpret
# the new rule as "Only play Speed if we *need* it to retreat into Sigil."
# If cost <= active_energy, no need to use Speed.
if cost <= active_energy:
return 0
# We do a simple approach: use the minimal i in [1..speed_in_hand]
# such that cost - i <= active_energy
# If none works, we can't retreat even with all Speed, so we do not use them.
used_speed = 0
for i in range(1, speed_in_hand + 1):
if cost - i <= active_energy:
used_speed = i
break
if used_speed == 0:
return 0 # can't retreat even with all Speed
# We found that using 'used_speed' Speed cards is enough
# So remove exactly that many Speed from our hand
speeds_removed = 0
new_hand = []
for c in hand:
if c == "Speed" and speeds_removed < used_speed:
speeds_removed += 1
else:
new_hand.append(c)
hand[:] = new_hand # update hand
return used_speed
def do_retreat_if_applicable(speed_count):
"""
Retreat logic:
If the active is NOT BasicSigil, try to retreat it if we can pay
(retreat_cost - speed_count).
Then pick a bench target:
- Prefer a bench BasicSigil if available
- Else pick the first bench
Before or after we check cost, if the Sigil on the bench has 0 energy
but we still can attach, we do so to satisfy "Sigil has at least 1 energy" for usage.
"""
nonlocal active, active_energy, bench, can_attach_energy_this_turn
if active == "BasicSigil":
return # do not retreat if active is Sigil
cost = retreat_cost(active)
cost = max(0, cost - speed_count) # reduce by speed_count
# Check if we have enough energy on the active
if active_energy < cost:
return # can't retreat anyway
# If we *can* retreat, let's see if we have a bench Sigil
# Because the rule states we only used Speed if we wanted to switch to Sigil
# but let's still implement our standard "prefer bench Sigil" approach.
sigil_index = None
for i, (b_poke, b_energy) in enumerate(bench):
if b_poke == "BasicSigil":
sigil_index = i
break
if sigil_index is None:
# no Sigil on bench => we do normal retreat logic (to first bench) if we want
# (But per the new rule, we *only* used Speed if we wanted to switch to Sigil.)
# For consistency, we'll do the old logic: retreat to bench[0] if there is one.
if len(bench) == 0:
return
# else retreat to bench[0]
active_energy -= cost
new_active_poke, new_active_energy = bench[0]
bench[0] = (active, active_energy)
active = new_active_poke
active_energy = new_active_energy
return
# We do have a bench Sigil.
# If it has 0 energy but we still can attach, do that now:
b_poke, b_energy = bench[sigil_index]
if b_energy == 0 and can_attach_energy_this_turn:
# attach to that bench Sigil
bench[sigil_index] = (b_poke, b_energy + 1)
can_attach_energy_this_turn = False
# Now retreat (pay cost)
active_energy -= cost
# swap
new_active_poke, new_active_energy = bench[sigil_index]
bench[sigil_index] = (active, active_energy)
active = new_active_poke
active_energy = new_active_energy
# -------------------
# TURN 1
# -------------------
# Condition #1 (end of T1): must have BasicKid on field (active or bench).
# Draw 1 card
c = draw_card(deck)
if c:
hand.append(c)
# Reset can_attach_energy_this_turn for Turn 1
can_attach_energy_this_turn = (not go_first)
# If active is BasicSigil and we can attach on T1 (go_first=False), do so and use skill
if active == "BasicSigil" and can_attach_energy_this_turn:
attach_energy_to_active() # attaches to the Active Sigil
# Use Sigil's skill: draw 1
c = draw_card(deck)
if c:
hand.append(c)
# Play items/supporters (including *conditional* Speed usage)
speed_count_this_turn = play_items_and_supporters(deck, hand, turn=1)
# If we haven't yet attached energy this turn, do a naive attempt:
# We *could* attach to the Active or a Bench Sigil. Let's keep it simple:
# if there's a 0-energy bench Sigil, attach to it first; else attach to active.
if can_attach_energy_this_turn:
attached = attach_energy_to_bench_sigil()
if not attached:
attach_energy_to_active()
# Bench any newly acquired Basic from hand
for c in hand[:]:
if c in ["BasicCat","BasicKid","BasicSigil"] and len(bench) < 3:
bench.append((c, 0))
hand.remove(c)
# Attempt retreat if active != BasicSigil
do_retreat_if_applicable(speed_count_this_turn)
# Check Condition #1
field_pokes = [active] + [bp for (bp,_) in bench]
if "BasicKid" not in field_pokes:
return False # fail immediately
# -------------------
# TURN 2
# -------------------
# Condition #2: must have Stage1 in hand by end of T2
# Draw 1
c = draw_card(deck)
if c:
hand.append(c)
# Reset energy attach for Turn 2
can_attach_energy_this_turn = True
# Attach to active Sigil if it's active and has 0, or else attach to bench Sigil if any, else to active
# Just for demonstration, we do the same logic:
if active == "BasicSigil" and active_energy == 0 and can_attach_energy_this_turn:
attach_energy_to_active()
elif can_attach_energy_this_turn:
attached = attach_energy_to_bench_sigil()
if not attached:
attach_energy_to_active()
# If active is BasicSigil (and it now presumably has energy), use skill
if active == "BasicSigil":
c = draw_card(deck)
if c:
hand.append(c)
speed_count_this_turn = play_items_and_supporters(deck, hand, turn=2)
# If we still haven't used energy (rare if above logic didn't attach), attach it
if can_attach_energy_this_turn:
attached = attach_energy_to_bench_sigil()
if not attached:
attach_energy_to_active()
# Bench any Basic from hand
for c in hand[:]:
if c in ["BasicCat","BasicKid","BasicSigil"] and len(bench) < 3:
bench.append((c, 0))
hand.remove(c)
do_retreat_if_applicable(speed_count_this_turn)
# Check Condition #2
if "Stage1" not in hand:
return False
# -------------------
# TURN 3
# -------------------
# Condition #3: must have Stage2 in hand by end of T3
# Draw 1
c = draw_card(deck)
if c:
hand.append(c)
can_attach_energy_this_turn = True
# Attach logic (similar approach)
if active == "BasicSigil" and active_energy == 0 and can_attach_energy_this_turn:
attach_energy_to_active()
elif can_attach_energy_this_turn:
attached = attach_energy_to_bench_sigil()
if not attached:
attach_energy_to_active()
# If active is Sigil with an energy, use skill
if active == "BasicSigil":
c = draw_card(deck)
if c:
hand.append(c)
speed_count_this_turn = play_items_and_supporters(deck, hand, turn=3)
# If we still can attach energy, do so
if can_attach_energy_this_turn:
attached = attach_energy_to_bench_sigil()
if not attached:
attach_energy_to_active()
# Bench any Basic from hand
for c in hand[:]:
if c in ["BasicCat","BasicKid","BasicSigil"] and len(bench) < 3:
bench.append((c, 0))
hand.remove(c)
do_retreat_if_applicable(speed_count_this_turn)
# Check Condition #3
if "Stage2" not in hand:
return False
# If we get here, we met all conditions
return True
# -----------------------
# Main: run simulations
# -----------------------
def main():
wins = 0
for _ in range(NUM_SIMULATIONS):
if simulate_one_game():
wins += 1
win_rate = wins / NUM_SIMULATIONS
print(f"Out of {NUM_SIMULATIONS} simulations, you won {wins} times.")
print(f"Win percentage: {win_rate*100:.2f}%")
if __name__ == "__main__":
main()
In this simulation I run a rudimentary deck: 2 Mewtwo, 2 ralts, 2 sigilyph, 2 kirlia, 2 Gardevoir, 2 pokeball, 2 professor, 2 slab, 2 x speed, and 2 other random cards. I find that with all of these cards being used at once, I get a success rate (of getting t3 gardevoir) as high as 54.446%.
This guess seems to generally line up with some of my online matches, as well as the rough combinatorials I used as an estimate beforehand. Feel free to tweak the code if you want to experiment. normally I'd be really proud of something like this, but given that I was able to do this in less than an hour using just a few o1 model chatgpt prompts is absolutely crazy.