import numpy as np
import pandas as pd
from collections import Counter
from concurrent.futures import ProcessPoolExecutor, as_completed
from itertools import combinations
from tqdm import tqdm

# VERSION 1.0
MovesetData = pd.read_csv("FactoryPokemon.csv")

# Keeping all contants here to keep everything in one file.
LEGAL_SETS = {"Grimer 1","Krabby 1","Exeggcute 1","Eevee 1","Drowzee 1","Voltorb 1","Chinchou 1","Teddiursa 1","Delibird 1","Houndour 1","Phanpy 1","Spoink 1","Aron 1","Luvdisc 1","Tentacool 1","Cacnea 1","Koffing 1","Staryu 1","Skiploom 1","Nuzleaf 1","Lombre 1","Vibrava 1","Rhyhorn 1","Clamperl 1","Pidgeotto 1","Growlithe 1","Farfetch'd 1","Omanyte 1","Kabuto 1","Lileep 1","Anorith 1","Aipom 1","Elekid 1","Loudred 1","Spinda 1","Nidorina 1","Nidorino 1","Flaaffy 1","Magby 1","Nosepass 1","Corsola 1","Mawile 1","Butterfree 1","Beedrill 1","Poliwhirl 1","Onix 1","Beautifly 1","Dustox 1","Ledian 1","Ariados 1","Yanma 1","Delcatty 1","Sableye 1","Lickitung 1","Weepinbell 1","Graveler 1","Gloom 1","Porygon 1","Kadabra 1","Wailmer 1","Roselia 1","Volbeat 1","Illumise 1","Ivysaur 1","Charmeleon 1","Wartortle 1","Parasect 1","Machoke 1","Haunter 1","Bayleef 1","Quilava 1","Croconaw 1","Togetic 1","Murkrow 1","Wobbuffet 1","Plusle 1","Minun 1","Grovyle 1","Combusken 1","Marshtomp 1","Ponyta 1","Azumarill 1","Sudowoodo 1","Magcargo 1","Pupitar 1","Sealeo 1","Raticate 1","Masquerain 1","Furret 1","Dunsparce 1","Dragonair 1","Mightyena 1","Linoone 1","Castform 1","Shelgon 1","Metang 1","Wigglytuff 1","Sunflora 1","Chimecho 1","Gligar 1","Qwilfish 1","Sneasel 1","Pelipper 1","Swellow 1","Lairon 1","Tangela 1","Arbok 1","Persian 1","Seadra 1","Kecleon 1","Vigoroth 1","Lunatone 1","Solrock 1","Noctowl 1","Sandslash 1","Venomoth 1","Chansey 1","Seaking 1","Jumpluff 1","Piloswine 1","Golbat 1","Primeape 1","Hitmonlee 1","Hitmonchan 1","Girafarig 1","Hitmontop 1","Banette 1","Ninjask 1","Seviper 1","Zangoose 1","Camerupt 1","Sharpedo 1","Tropius 1","Magneton 1","Mantine 1","Stantler 1","Absol 1","Swalot 1","Crawdaunt 1","Pidgeot 1","Grumpig 1","Torkoal 1","Kingler 1","Cacturne 1","Bellossom 1","Octillery 1","Huntail 1","Gorebyss 1","Relicanth 1","Omastar 1","Kabutops 1","Poliwrath 1","Scyther 1","Pinsir 1","Politoed 1","Cloyster 1","Delcatty 2","Sableye 2","Lickitung 2","Weepinbell 2","Graveler 2","Gloom 2","Porygon 2","Kadabra 2","Wailmer 2","Roselia 2","Volbeat 2","Illumise 2","Ivysaur 2","Charmeleon 2","Wartortle 2","Parasect 2","Machoke 2","Haunter 2","Bayleef 2","Quilava 2","Croconaw 2","Togetic 2","Murkrow 2","Wobbuffet 2","Plusle 2","Minun 2","Grovyle 2","Combusken 2","Marshtomp 2","Ponyta 2","Azumarill 2","Sudowoodo 2","Magcargo 2","Pupitar 2","Sealeo 2","Raticate 2","Masquerain 2","Furret 2","Dunsparce 2","Dragonair 2","Mightyena 2","Linoone 2","Castform 2","Shelgon 2","Metang 2","Wigglytuff 2","Sunflora 2","Chimecho 2","Gligar 2","Qwilfish 2","Sneasel 2","Pelipper 2","Swellow 2","Lairon 2","Tangela 2","Arbok 2","Persian 2","Seadra 2","Kecleon 2","Vigoroth 2","Lunatone 2","Solrock 2","Noctowl 2","Sandslash 2","Venomoth 2","Chansey 2","Seaking 2","Jumpluff 2","Piloswine 2","Golbat 2","Primeape 2","Hitmonlee 2","Hitmonchan 2","Girafarig 2","Hitmontop 2","Banette 2","Ninjask 2","Seviper 2","Zangoose 2","Camerupt 2","Sharpedo 2","Tropius 2","Magneton 2","Mantine 2","Stantler 2","Absol 2","Swalot 2","Crawdaunt 2","Pidgeot 2","Grumpig 2","Torkoal 2","Kingler 2","Cacturne 2","Bellossom 2","Octillery 2","Huntail 2","Gorebyss 2","Relicanth 2","Omastar 2","Kabutops 2","Poliwrath 2","Scyther 2","Pinsir 2","Politoed 2","Cloyster 2","Dugtrio 1","Medicham 1","Misdreavus 1","Fearow 1","Granbull 1","Jynx 1","Dusclops 1","Dodrio 1","Mr. Mime 1","Lanturn 1","Breloom 1","Forretress 1","Whiscash 1","Xatu 1","Skarmory 1","Marowak 1","Quagsire 1","Clefable 1","Hariyama 1","Raichu 1","Dewgong 1","Manectric 1","Vileplume 1","Victreebel 1","Electrode 1","Exploud 1","Shiftry 1","Glalie 1","Ludicolo 1","Hypno 1","Golem 1","Rhydon 1","Alakazam 1","Weezing 1","Kangaskhan 1","Electabuzz 1","Tauros 1","Slowbro 1","Slowking 1","Miltank 1","Altaria 1","Nidoqueen 1","Nidoking 1","Magmar 1","Cradily 1","Armaldo 1","Golduck 1","Rapidash 1","Muk 1","Gengar 1","Ampharos 1","Scizor 1","Heracross 1","Ursaring 1","Houndoom 1","Donphan 1","Claydol 1","Wailord 1","Ninetales 1","Machamp 1","Shuckle 1","Steelix 1","Tentacruel 1","Aerodactyl 1","Porygon2 1","Gardevoir 1","Exeggutor 1","Starmie 1","Flygon 1","Venusaur 1","Vaporeon 1","Jolteon 1","Flareon 1","Meganium 1","Espeon 1","Umbreon 1","Blastoise 1","Feraligatr 1","Aggron 1","Blaziken 1","Walrein 1","Sceptile 1","Charizard 1","Typhlosion 1","Lapras 1","Crobat 1","Swampert 1","Gyarados 1","Snorlax 1","Kingdra 1","Blissey 1","Milotic 1","Arcanine 1","Salamence 1","Metagross 1","Slaking 1","Dugtrio 2","Sceptile 2","Aerodactyl 2","Gengar 2","Espeon 2","Electrode 2","Manectric 2","Fearow 2","Dodrio 2","Raichu 2","Ninetales 2","Flygon 2","Charizard 2","Heracross 2","Jolteon 2","Crobat 2","Houndoom 2","Arcanine 2","Electabuzz 2","Rapidash 2","Magmar 2","Typhlosion 2","Salamence 2","Alakazam 2","Starmie 2","Gyarados 2","Medicham 2","Glalie 2","Tauros 2","Golduck 2","Lapras 2","Breloom 2","Tentacruel 2","Miltank 2","Slaking 2","Jynx 2","Xatu 2","Donphan 2","Metagross 2","Mr. Mime 2","Kangaskhan 2","Misdreavus 2","Kingdra 2","Nidoking 2","Milotic 2","Shiftry 2","Altaria 2","Gardevoir 2","Venusaur 2","Meganium 2","Blaziken 2","Marowak 2","Nidoqueen 2","Claydol 2","Skarmory 2","Victreebel 2","Dewgong 2","Ludicolo 2","Exploud 2","Blastoise 2","Feraligatr 2","Hypno 2","Lanturn 2","Scizor 2","Umbreon 2","Vaporeon 2","Walrein 2","Clefable 2","Weezing 2","Porygon2 2","Whiscash 2","Wailord 2","Swampert 2","Flareon 2","Ampharos 2","Ursaring 2","Machamp 2","Exeggutor 2","Blissey 2","Vileplume 2","Muk 2","Aggron 2","Golem 2","Armaldo 2","Hariyama 2","Cradily 2","Forretress 2","Rhydon 2","Granbull 2","Steelix 2","Slowbro 2","Snorlax 2","Slowking 2","Quagsire 2","Dusclops 2","Shuckle 2","Dugtrio 3","Starmie 3","Misdreavus 3","Fearow 3","Granbull 3","Espeon 3","Dusclops 3","Dodrio 3","Xatu 3","Lanturn 3","Breloom 3","Forretress 3","Whiscash 3","Alakazam 3","Skarmory 3","Marowak 3","Quagsire 3","Clefable 3","Hariyama 3","Raichu 3","Dewgong 3","Manectric 3","Vileplume 3","Victreebel 3","Electrode 3","Exploud 3","Shiftry 3","Glalie 3","Ludicolo 3","Medicham 3","Golem 3","Rhydon 3","Gardevoir 3","Weezing 3","Kangaskhan 3","Electabuzz 3","Tauros 3","Jynx 3","Metagross 3","Miltank 3","Altaria 3","Nidoqueen 3","Nidoking 3","Magmar 3","Cradily 3","Armaldo 3","Golduck 3","Rapidash 3","Muk 3","Gengar 3","Ampharos 3","Scizor 3","Heracross 3","Ursaring 3","Houndoom 3","Donphan 3","Mr. Mime 3","Wailord 3","Ninetales 3","Machamp 3","Shuckle 3","Steelix 3","Tentacruel 3","Aerodactyl 3","Porygon2 3","Claydol 3","Hypno 3","Exeggutor 3","Flygon 3","Venusaur 3","Vaporeon 3","Jolteon 3","Flareon 3","Meganium 3","Slowbro 3","Umbreon 3","Blastoise 3","Feraligatr 3","Aggron 3","Blaziken 3","Walrein 3","Sceptile 3","Charizard 3","Typhlosion 3","Lapras 3","Crobat 3","Swampert 3","Gyarados 3","Snorlax 3","Kingdra 3","Blissey 3","Milotic 3","Arcanine 3","Salamence 3","Slowking 3","Slaking 3","Jolteon 4","Crobat 4","Dugtrio 4","Sceptile 4","Starmie 4","Gengar 4","Espeon 4","Electrode 4","Manectric 4","Dodrio 4","Ninetales 4","Misdreavus 4","Aerodactyl 4","Houndoom 4","Arcanine 4","Rapidash 4","Kangaskhan 4","Flygon 4","Charizard 4","Typhlosion 4","Alakazam 4","Golduck 4","Heracross 4","Gardevoir 4","Tauros 4","Electabuzz 4","Breloom 4","Fearow 4","Raichu 4","Tentacruel 4","Miltank 4","Salamence 4","Slaking 4","Lanturn 4","Jynx 4","Xatu 4","Magmar 4","Mr. Mime 4","Nidoking 4","Kingdra 4","Gyarados 4","Milotic 4","Medicham 4","Shiftry 4","Glalie 4","Altaria 4","Venusaur 4","Meganium 4","Blaziken 4","Blastoise 4","Feraligatr 4","Marowak 4","Nidoqueen 4","Claydol 4","Skarmory 4","Dewgong 4","Ludicolo 4","Victreebel 4","Metagross 4","Exploud 4","Hypno 4","Scizor 4","Vaporeon 4","Umbreon 4","Clefable 4","Wailord 4","Weezing 4","Porygon2 4","Lapras 4","Flareon 4","Walrein 4","Ampharos 4","Ursaring 4","Machamp 4","Exeggutor 4","Blissey 4","Whiscash 4","Swampert 4","Hariyama 4","Donphan 4","Aggron 4","Golem 4","Armaldo 4","Cradily 4","Vileplume 4","Muk 4","Forretress 4","Rhydon 4","Granbull 4","Slowking 4","Steelix 4","Snorlax 4","Quagsire 4","Dusclops 4","Slowbro 4","Shuckle 4","Articuno 1","Zapdos 1","Moltres 1","Raikou 1","Entei 1","Suicune 1","Regirock 1","Regice 1","Registeel 1","Latias 1","Latios 1","Articuno 2","Zapdos 2","Moltres 2","Raikou 2","Entei 2","Suicune 2","Regirock 2","Regice 2","Registeel 2","Latias 2","Latios 2","Articuno 3","Zapdos 3","Moltres 3","Raikou 3","Entei 3","Suicune 3","Regirock 3","Regice 3","Registeel 3","Latias 3","Latios 3","Articuno 4","Zapdos 4","Moltres 4","Raikou 4","Entei 4","Suicune 4","Regirock 4","Regice 4","Registeel 4","Latias 4","Latios 4","Gengar 5","Gengar 6","Gengar 7","Gengar 8","Ursaring 5","Ursaring 6","Ursaring 7","Ursaring 8","Machamp 5","Machamp 6","Machamp 7","Machamp 8","Gardevoir 5","Gardevoir 6","Gardevoir 7","Gardevoir 8","Starmie 5","Starmie 6","Starmie 7","Starmie 8","Lapras 5","Lapras 6","Lapras 7","Lapras 8","Snorlax 5","Snorlax 6","Snorlax 7","Snorlax 8","Salamence 5","Salamence 6","Salamence 7","Salamence 8","Metagross 5","Metagross 6","Metagross 7","Metagross 8","Regirock 5","Regirock 6","Regice 5","Regice 6","Registeel 5","Registeel 6","Latias 5","Latias 6","Latias 7","Latias 8","Latios 5","Latios 6","Latios 7","Latios 8","Dragonite 1","Dragonite 2","Dragonite 3","Dragonite 4","Dragonite 5","Dragonite 6","Dragonite 7","Dragonite 8","Dragonite 9","Dragonite 10","Tyranitar 1","Tyranitar 2","Tyranitar 3","Tyranitar 4","Tyranitar 5","Tyranitar 6","Tyranitar 7","Tyranitar 8","Tyranitar 9","Tyranitar 10","Articuno 5","Articuno 6","Zapdos 5","Zapdos 6","Moltres 5","Moltres 6","Raikou 5","Raikou 6","Entei 5","Entei 6","Suicune 5","Suicune 6"}
LEGAL_SPECIES = {"Absol","Aerodactyl","Aggron","Aipom","Alakazam","Altaria","Ampharos","Anorith","Arbok","Arcanine","Ariados","Armaldo","Aron","Articuno","Azumarill","Banette","Bayleef","Beautifly","Beedrill","Bellossom","Blastoise","Blaziken","Blissey","Breloom","Butterfree","Cacnea","Cacturne","Camerupt","Castform","Chansey","Charizard","Charmeleon","Chimecho","Chinchou","Clamperl","Claydol","Clefable","Cloyster","Combusken","Corsola","Cradily","Crawdaunt","Crobat","Croconaw","Delcatty","Delibird","Dewgong","Dodrio","Donphan","Dragonair","Dragonite","Drowzee","Dugtrio","Dunsparce","Dusclops","Dustox","Eevee","Electabuzz","Electrode","Elekid","Entei","Espeon","Exeggcute","Exeggutor","Exploud","Farfetch'd","Fearow","Feraligatr","Flaaffy","Flareon","Flygon","Forretress","Furret","Gardevoir","Gengar","Girafarig","Glalie","Gligar","Gloom","Golbat","Golduck","Golem","Gorebyss","Granbull","Graveler","Grimer","Grovyle","Growlithe","Grumpig","Gyarados","Hariyama","Haunter","Heracross","Hitmonchan","Hitmonlee","Hitmontop","Houndoom","Houndour","Huntail","Hypno","Illumise","Ivysaur","Jolteon","Jumpluff","Jynx","Kabuto","Kabutops","Kadabra","Kangaskhan","Kecleon","Kingdra","Kingler","Koffing","Krabby","Lairon","Lanturn","Lapras","Latias","Latios","Ledian","Lickitung","Lileep","Linoone","Lombre","Loudred","Ludicolo","Lunatone","Luvdisc","Machamp","Machoke","Magby","Magcargo","Magmar","Magneton","Manectric","Mantine","Marowak","Marshtomp","Masquerain","Mawile","Medicham","Meganium","Metagross","Metang","Mightyena","Milotic","Miltank","Minun","Misdreavus","Moltres","Mr. Mime","Muk","Murkrow","Nidoking","Nidoqueen","Nidorina","Nidorino","Ninetales","Ninjask","Noctowl","Nosepass","Nuzleaf","Octillery","Omanyte","Omastar","Onix","Parasect","Pelipper","Persian","Phanpy","Pidgeot","Pidgeotto","Piloswine","Pinsir","Plusle","Politoed","Poliwhirl","Poliwrath","Ponyta","Porygon","Porygon2","Primeape","Pupitar","Quagsire","Quilava","Qwilfish","Raichu","Raikou","Rapidash","Raticate","Regice","Regirock","Registeel","Relicanth","Rhydon","Rhyhorn","Roselia","Sableye","Salamence","Sandslash","Sceptile","Scizor","Scyther","Seadra","Seaking","Sealeo","Seviper","Sharpedo","Shelgon","Shiftry","Shuckle","Skarmory","Skiploom","Slaking","Slowbro","Slowking","Sneasel","Snorlax","Solrock","Species","Spinda","Spoink","Stantler","Starmie","Staryu","Steelix","Sudowoodo","Suicune","Sunflora","Swalot","Swampert","Swellow","Tangela","Tauros","Teddiursa","Tentacool","Tentacruel","Togetic","Torkoal","Tropius","Typhlosion","Tyranitar","Umbreon","Ursaring","Vaporeon","Venomoth","Venusaur","Vibrava","Victreebel","Vigoroth","Vileplume","Volbeat","Voltorb","Wailmer","Wailord","Walrein","Wartortle","Weepinbell","Weezing","Whiscash","Wigglytuff","Wobbuffet","Xatu","Yanma","Zangoose","Zapdos"}
LEGAL_MOVES = {'Absorb', 'Acid', 'Acid Armor', 'Aerial Ace', 'Agility', 'Air Cutter', 'Amnesia', 'AncientPower', 'Astonish', 'Attract', 'Aurora Beam', 'Barrier', 'Baton Pass', 'Belly Drum', 'Bind', 'Bite', 'Blizzard', 'Block', 'Body Slam', 'Bounce', 'Brick Break', 'BubbleBeam', 'Bulk Up', 'Calm Mind', 'Charm', 'Comet Punch', 'Confuse Ray', 'Confusion', 'Cosmic Power', 'Cotton Spore', 'Counter', 'Covet', 'Crabhammer', 'Cross Chop', 'Crunch', 'Crush Claw', 'Curse', 'Defense Curl', 'Destiny Bond', 'Detect', 'Dig', 'Disable', 'Dive', 'Dizzy Punch', 'Double Kick', 'Double Team', 'Double-Edge', 'DoubleSlap', 'Dragon Claw', 'Dragon Dance', 'Dragon Rage', 'DragonBreath', 'Dream Eater', 'Drill Peck', 'DynamicPunch', 'Earthquake', 'Ember', 'Encore', 'Endeavor', 'Endure', 'Eruption', 'Explosion', 'Extrasensory', 'ExtremeSpeed', 'Facade', 'Faint Attack', 'Fake Out', 'Fake Tears', 'FeatherDance', 'Fire Blast', 'Fire Punch', 'Fire Spin', 'Fissure', 'Flail', 'Flame Wheel', 'Flamethrower', 'Flash', 'Flatter', 'Fly', 'Focus Energy', 'Focus Punch', 'Follow Me', 'Foresight', 'Frustration', 'Fury Cutter', 'Fury Swipes', 'Future Sight', 'Giga Drain', 'Glare', 'GrassWhistle', 'Growth', 'Grudge', 'Guillotine', 'Hail', 'Harden', 'Haze', 'Headbutt', 'Heal Bell', 'Helping Hand', 'Horn Drill', 'Howl', 'Hydro Pump', 'Hyper Fang', 'Hypnosis', 'Ice Ball', 'Ice Beam', 'Ice Punch', 'Icy Wind', 'Ingrain', 'Iron Defense', 'Iron Tail', 'Karate Chop', 'Knock Off', 'Leaf Blade', 'Leech Seed', 'Leer', 'Lick', 'Light Screen', 'Lovely Kiss', 'Low Kick', 'Mach Punch', 'Magic Coat', 'Magical Leaf', 'Magnitude', 'Mean Look', 'Mega Drain', 'Mega Kick', 'Mega Punch', 'Megahorn', 'Memento', 'Metal Claw', 'Metal Sound', 'Meteor Mash', 'Metronome', 'Minimize', 'Mirror Coat', 'Moonlight', 'Mud Shot', 'Mud-Slap', 'Muddy Water', 'Needle Arm', 'Night Shade', 'Nightmare', 'Octazooka', 'Overheat', 'Perish Song', 'Petal Dance', 'Pin Missile', 'Poison Fang', 'Poison Tail', 'Present', 'Protect', 'Psybeam', 'Psych Up', 'Psychic', 'Psywave', 'Pursuit', 'Quick Attack', 'Rain Dance', 'Rapid Spin', 'Razor Leaf', 'Razor Wind', 'Recover', 'Recycle', 'Reflect', 'Refresh', 'Rest', 'Return', 'Revenge', 'Reversal', 'Roar', 'Rock Blast', 'Rock Slide', 'Rock Throw', 'Rock Tomb', 'Role Play', 'Rolling Kick', 'Rollout', 'Safeguard', 'Sand-Attack', 'Sandstorm', 'Scary Face', 'Screech', 'Secret Power', 'Seismic Toss', 'Selfdestruct', 'Shadow Ball', 'Shadow Punch', 'Sheer Cold', 'Shock Wave', 'Signal Beam', 'Silver Wind', 'Sing', 'Skill Swap', 'Sky Uppercut', 'Slam', 'Slash', 'Sleep Powder', 'Sleep Talk', 'Sludge', 'Sludge Bomb', 'Smog', 'SmokeScreen', 'Snore', 'Softboiled', 'SolarBeam', 'SonicBoom', 'Spark', 'Spider Web', 'Spike Cannon', 'Spikes', 'Spit Up', 'Spite', 'Spore', 'Steel Wing', 'Stockpile', 'Stomp', 'Stun Spore', 'Submission', 'Substitute', 'Sunny Day', 'Super Fang', 'Supersonic', 'Surf', 'Swagger', 'Swallow', 'Sweet Kiss', 'Sweet Scent', 'Swift', 'Swords Dance', 'Synthesis', 'Tail Glow', 'Tail Whip', 'Take Down', 'Taunt', 'Teeter Dance', 'Thunder', 'Thunder Wave', 'ThunderPunch', 'Thunderbolt', 'Tickle', 'Torment', 'Toxic', 'Tri Attack', 'Trick', 'Triple Kick', 'Twineedle', 'Uproar', 'ViceGrip', 'Water Gun', 'Water Pulse', 'Water Spout', 'Whirlpool', 'Whirlwind', 'Will-O-Wisp', 'Wish', 'Wrap', 'Yawn'}
LEGAL_ITEMS = {'Black Belt', 'BlackGlasses', 'BrightPowder', 'Charcoal', 'Cheri Berry', 'Chesto Berry', 'Choice Band', 'DeepSeaScale', 'Focus Band', 'Hard Stone', "King's Rock", 'Lax Incense', 'Leftovers', 'Leppa Berry', 'Liechi Berry', 'Lum Berry', 'Magnet', 'Mental Herb', 'Metal Coat', 'Miracle Seed', 'Mystic Water', 'NeverMeltIce', 'Pecha Berry', 'Persim Berry', 'Petaya Berry', 'Poison Barb', 'Quick Claw', 'Rawst Berry', 'Salac Berry', 'Scope Lens', 'Sharp Beak', 'Shell Bell', 'Silk Scarf', 'SilverPowder', 'Sitrus Berry', 'Soft Sand', 'Stick', 'Thick Club', 'TwistedSpoon', 'White Herb'}
LEGAL_TYPES = {"n", "Grass", "Fire", "Water", "Poison", "Normal", "Flying", "Bug", "Rock", "Ghost",
               "Ground", "Steel", "Electric", "Psychic", "Ice", "Dark", "Fighting", "Dragon"}
LEGAL_BATTLE_STYLES = {0, 1, 2, 3, 4, 5, 6, 7, 8}

SPEED_PLUS_NATURES = {"Timid", "Hasty", "Jolly", "Naive"}
SPEED_MINUS_NATURES = {"Brave", "Relaxed", "Quiet", "Sassy"}

MOVES_1 = {"AcidArmor","Agility","Amnesia","Barrier","Belly Drum","Bulk Up","Calm Mind","Charge","Conversion",
"Conversion2","Cosmic Power","Defense Curl","Double Team","Dragon Dance","Focus Energy","Growth",
"Harden","Howl","Iron Defense","Meditate","Minimize","Psych Up","Sharpen","Snatch","Swords Dance",
"Tail Glow","Withdraw"}

MOVES_2 = {"Attract","Block","Confuse Ray","Disable","Encore","Flatter","Glare","GrassWhistle","Hypnosis","Imprison",
"Leech Seed","Lovely Kiss","Mean Look","Poison Gas","PoisonPowder","Sing","Sleep Powder","Snatch",
"Spider Web","Spikes","Spore","Stun Spore","Supersonic","Swagger","Sweet Kiss","Taunt","Teeter Dance",
"Thunder Wave","Torment","Toxic","Yawn","Will-O-Wisp"}

MOVES_3 = {"Aromatherapy","Baton Pass","Detect","Endure","Haze","Heal Bell","Ingrain","Light Screen","Magic Coat",
"Milk Drink","Mist","Moonlight","Morning Sun","Mud Sport","Protect","Recover","Reflect","Rest","Safeguard",
"Slack Off","Softboiled","Swallow","Synthesis","Recycle","Refresh","Water Sport","Wish"}

MOVES_4 = {"Bide","Blast Burn","Counter","Destiny Bond","Double-Edge","Explosion","Facade","Fissure","Flail",
"Focus Punch","Frenzy Plant","Grudge","Guillotine","Horn Drill","Hydro Cannon","Hyper Beam","Memento",
"Mirror Coat","Overheat","Pain Split","Perish Song","Psycho Boost","Reversal","Selfdestruct","Sky Attack",
"Volt Tackle"}

MOVES_5 = {"Charm","Cotton Spore","Fake Tears","FeatherDance","Flash","Growl","Kinesis","Knock Off","Leer",
"Metal Sound","Sand-Attack","Scary Face","Screech","SmokeScreen","Spite","String Shot","Sweet Scent",
"Tail Whip","Tickle"}

MOVES_6 = {"Assist","Camouflage","Curse","Follow Me","Metronome","Mimic","Mirror Move","Present","Role Play",
"Sketch","Skill Swap","Substitute","Transform","Trick"}

MOVES_7 = {"Hail","Rain Dance","Sandstorm","Sunny Day","Weather Ball"}

ALL_SPECIAL_MOVES = set.union(MOVES_1, MOVES_2, MOVES_3, MOVES_4, MOVES_5, MOVES_6, MOVES_7)

#==================================#
#=== DON'T TOUCH ANYTHING BELOW ===#
#==================================#

class PokemonTriplet:
    def __init__(self, Pokemon1, Pokemon2, Pokemon3):
        self.Pokemon1 = Pokemon1
        self.Pokemon2 = Pokemon2
        self.Pokemon3 = Pokemon3

class PokemonSet:
    def __init__(self, Identifier):
        self.Identifier = Identifier
        self.Round = int(MovesetData.loc[MovesetData["Identifier"] == Identifier,"Round"].values[0])
        self.Species = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Species"].values[0]
        self.Instance = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Set#"].values[0]
        self.Item = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Item"].values[0]
        self.Nature = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Nature"].values[0]
        self.Species = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Species"].values[0]
        self.Move1 = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Move 1"].values[0]
        self.Move2 = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Move 2"].values[0]
        self.Move3 = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Move 3"].values[0]
        self.Move4 = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Move 4"].values[0]
        self.NatureMod = 1.1 if self.Nature in SPEED_PLUS_NATURES else (0.9 if self.Nature in SPEED_MINUS_NATURES else 1)
        self.SpeedEV = int(MovesetData.loc[MovesetData["Identifier"] == Identifier,"EV Spread"].values[0].split("/")[-1].strip())
        self.BaseSpeed = int(MovesetData.loc[MovesetData["Identifier"] == Identifier,"Base Speed"].values[0])
        self.MoveList = [self.Move1, self.Move2, self.Move3, self.Move4]
        self.LocalOdds = 1
        self.GlobalOdds = 1

        typ = MovesetData.loc[MovesetData["Identifier"] == Identifier,"Type"].values[0]
        if (" " in typ): # Dual type
            types = typ.split()
            self.Type1 = types[0]
            self.Type2 = types[1]
        else:
            self.Type1 = typ
            self.Type2 = None

    # For use in a set
    def __eq__(self, other):
        return self.Identifier == other.Identifier

    def __hash__(self):
        return hash(self.Identifier)
    
    def draft_set_printer(self, with_int=0):
        if with_int:
            print(f'{with_int:>3}'f'{self.Identifier:>13}',f'{self.Item:>13}',f'{self.Nature:>8}',f'{self.Move1:>13}',f'{self.Move2:>13}',f'{self.Move3:>13}',f'{self.Move4:>13}')
        else:
            print(f'{self.Identifier:>13}',f'{self.Item:>13}',f'{self.Nature:>8}',f'{self.Move1:>13}',f'{self.Move2:>13}',f'{self.Move3:>13}',f'{self.Move4:>13}')

    def Setprinter(self, ivs, Level_50):
        if np.round(self.LocalOdds*100,1) > 0:
            level_mod = 0.5 if Level_50 else 1
            speed = int((int((2 * self.BaseSpeed + ivs + int(self.SpeedEV / 4)) * level_mod) + 5) * self.NatureMod)
            print(f'{self.Identifier:>13}',f'{np.round(self.LocalOdds*100,1):>9}',f'{self.Item:>13}',f'{self.Nature:>8}',f'{self.Move1:>13}',f'{self.Move2:>13}',f'{self.Move3:>13}',f'{self.Move4:>13}',f'{speed:>6}')

def get_opponent_species():
    while(True):
        temp = input()
        if temp not in LEGAL_SPECIES:
            print("\nMust select a species name that is legal.")
        else:
            return temp
    
# For Noland battles to make sure we have no false negatives, remove any ambiguous pokemon sets
def remove_duplicate_species(pokemon_list):
    return_set = set()
    species_count = Counter()
    for poke in pokemon_list:
        species_count[poke.split()[0]] += 1
    for poke in pokemon_list:
        if species_count[poke.split()[0]] < 2:
            return_set.add(poke)
    return return_set

def is_legal_noland_team(pokemon1: PokemonSet, pokemon_set: set, draft_ids: set):
    legal_teams = []
    local_counts = Counter()
    # Generate pairs efficiently
    for pokemon2, pokemon3 in combinations(pokemon_set, 2):
        if (pokemon2.Identifier not in draft_ids and
            pokemon3.Identifier not in draft_ids and
            pokemon1.Species != pokemon2.Species and
            pokemon1.Species != pokemon3.Species and
            pokemon2.Species != pokemon3.Species and
            pokemon1.Item != pokemon2.Item and
            pokemon1.Item != pokemon3.Item and
            pokemon2.Item != pokemon3.Item):
                legal_teams.append({pokemon1, pokemon2, pokemon3})
                local_counts[pokemon1.Identifier] += 1
                local_counts[pokemon2.Identifier] += 1
                local_counts[pokemon3.Identifier] += 1
    return legal_teams, local_counts

def is_legal_team(pokemon1: PokemonSet, pokemon_set: set, draft_set: set, preferred_type: str, battle_style: str):
    legal_teams = []
    local_counts = Counter()
    # Generate pairs efficiently
    for pokemon2, pokemon3 in combinations(pokemon_set, 2):
        if (pokemon2.Species not in draft_set and
            pokemon3.Species not in draft_set and
            pokemon1.Species != pokemon2.Species and
            pokemon1.Species != pokemon3.Species and
            pokemon2.Species != pokemon3.Species and
            pokemon1.Item != pokemon2.Item and
            pokemon1.Item != pokemon3.Item and
            pokemon2.Item != pokemon3.Item and
            types_legal(pokemon1, pokemon2, pokemon3, preferred_type) and
            moves_legal(pokemon1, pokemon2, pokemon3, battle_style)):
                legal_teams.append({pokemon1, pokemon2, pokemon3})
                local_counts[pokemon1.Identifier] += 1
                local_counts[pokemon2.Identifier] += 1
                local_counts[pokemon3.Identifier] += 1
    return legal_teams, local_counts

# Returns True if the types of the Pokemon in the set is compatible with the type preferences of the trainer.
def types_legal(p1: PokemonSet, p2: PokemonSet, p3: PokemonSet, preferred_type: str):
    # If the opponent has a preferred type check how many. No point in checking otherwise.
    if preferred_type != "n":
        total_of_preferred_type = 0
        for p in [p1, p2, p3]:
            if p.Type1 == preferred_type or p.Type2 == preferred_type:
                total_of_preferred_type += 1
        if total_of_preferred_type < 2: # If the team doesn't have at least 2 of the preferred type, return false
            return False
    
    # If the opponent has at least 2 of the preferred type, make sure the other types don't conflict
    total_of_non_preferred_type = 0
    nonpreferred_types = []
    # If the opponent has at least 2 of the preferred type, make sure the other types don't conflict
    if p1.Type1 != preferred_type:
        if p1.Type1 == p2.Type1:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type1)
        if p1.Type1 == p3.Type1:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type1)
        if p1.Type1 == p2.Type2:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type1)
        if p1.Type1 == p3.Type2:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type1)
    if p1.Type2 is not None and p1.Type2 != preferred_type:
        if  p1.Type2 == p2.Type1:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type2)
        if p1.Type2 == p3.Type1:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type2)
        if p1.Type2 == p2.Type2:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type2)
        if p1.Type2 == p3.Type2:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p1.Type2)
    if p2.Type1 != preferred_type:
        if p2.Type1 == p3.Type1:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p2.Type1)
        if p2.Type1 == p3.Type2:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p2.Type1)
    if p2.Type2 is not None and p2.Type2 != preferred_type:
        if p2.Type2 == p3.Type1:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p2.Type2)
        if p2.Type2 == p3.Type2:
            total_of_non_preferred_type += 1
            nonpreferred_types.append(p2.Type2)

    # If there is no preferred type
    if (preferred_type == "n"): 
        if total_of_non_preferred_type == 1: # If two pokes have the same type then it is illegal
            return False
        if total_of_non_preferred_type == 3: # If all 3 pokes share one type with each other then it is illegal
            return False
        if total_of_non_preferred_type == 4: # If all 3 pokes share one type with each other and 2 share the second type then it is illegal
            return False
         # If total_of_non_preferred_type == 0, 2, or 6 then it is legal
        return True
    # If the opponent has a preferred typed
    else:
        # If there are 3 pokes with the same non-preferred type or there is a 3 way tie in types it is illegal
        if total_of_non_preferred_type in {2, 3}:
            return False
        # If 2 pokes have the same non-perferred type AND only 1 or 2 pokes have the preferred type it is illegal
        if total_of_non_preferred_type == 1 and total_of_preferred_type <= 2:
            return False
        return True

# Returns True if the moves of the set is compatible with the battle style of the trainer.
def moves_legal(p1, p2, p3, battle_style):
    # Create a dict to store the number of occurences of each move type
    move_dict = {1:0,2:0,3:0,4:0,5:0,6:0,6:0,7:0}
    for p in [p1, p2, p3]:
        for move in p.MoveList:
            if move in ALL_SPECIAL_MOVES:
                if move in MOVES_1:
                    move_dict[1] = 1 + move_dict.get(1)
                elif move in MOVES_2:
                    move_dict[2] = 1 + move_dict.get(2)
                elif move in MOVES_3:
                    move_dict[3] = 1 + move_dict.get(3)
                elif move in MOVES_4:
                    move_dict[4] = 1 + move_dict.get(4)
                elif move in MOVES_5:
                    move_dict[5] = 1 + move_dict.get(5)
                elif move in MOVES_6:
                    move_dict[6] = 1 + move_dict.get(6)
                elif move in MOVES_7:
                    move_dict[7] = 1 + move_dict.get(7)
    match battle_style:
        # For "free-spirited and unrestrained", return false if it exceeds the limit on any move type.
        case 0:
            first_set = {1,2,3}
            second_set = {4,5,6,7}
            for move, count in move_dict.items():
                if count >= 3 and move in first_set:
                    return False
                elif count >= 2 and move in second_set:
                    return False
            return True
        # For trainers with a preferred move, short circuit if the player doesn't have required num for move type.
        # Needs to have only 1 or 2 preferred moves and the preferred move type has to be the highest.
        case 1:
            if move_dict.get(1) < 3:
                return False
            for move, count in move_dict.items():
                if (count >= 3 and move in {2,3}) or (count >= 2 and move in {4,5,6,7}):
                    return False
            return True
        case 2:
            if move_dict.get(2) < 3:
                return False
            for move, count in move_dict.items():
                if (count >= 3 and move == 3) or (count >= 2 and move in {4,5,6,7}):
                    return False
            return True
        case 3:
            if move_dict.get(3) < 3:
                return False
            moves_greater_than_threshold = 0
            for move, count in move_dict.items():
                if count >= 3 and move in {1,2,3}:
                    moves_greater_than_threshold += 1
                elif count >= 2 and move in {4,5,6,7}:
                    return False
                if moves_greater_than_threshold > 2:
                    return False
            return True
        case 4:
            if move_dict.get(4) < 2:
                return False
            moves_greater_than_threshold = 0
            for move, count in move_dict.items():
                if (count >= 3 and move in {1,2,3}) or (count >= 2 and move == 4):
                    moves_greater_than_threshold += 1
                elif count >= 2 and move in {5,6,7}:
                    return False
                if moves_greater_than_threshold > 2:
                    return False
            return True
        case 5:
            if move_dict.get(5) < 2:
                return False
            moves_greater_than_threshold = 0
            for move, count in move_dict.items():
                if (count >= 3 and move in {1,2,3}) or (count >= 2 and move in {4,5}):
                    moves_greater_than_threshold += 1
                elif count >= 2 and move in {6,7}:
                    return False
                if moves_greater_than_threshold > 2:
                    return False
            return True
        case 6:
            if move_dict.get(6) < 2:
                return False
            moves_greater_than_threshold = 0
            for move, count in move_dict.items():
                if (count >= 3 and move in {1,2,3}) or (count >= 2 and move in {4,5,6}):
                    moves_greater_than_threshold += 1
                elif count >= 2 and move == 7:
                    return False
                if moves_greater_than_threshold > 2:
                    return False
            return True
        case 7:
            if move_dict.get(7) < 2:
                return False
            moves_greater_than_threshold = 0
            for move, count in move_dict.items():
                if (count >= 3 and move in {1,2,3}) or (count >= 2 and move in {4,5,6,7}):
                    moves_greater_than_threshold += 1
                if moves_greater_than_threshold > 2:
                    return False
            return True
        case 8:
            moves_greater_than_threshold = 0
            for move, count in move_dict.items():
                if count >= 3 and move in {1,2,3}:
                    moves_greater_than_threshold += 1
                elif count >= 2 and move in {4,5,6,7}:
                    moves_greater_than_threshold += 1
            if moves_greater_than_threshold < 3:
                return False
            return True
        
def get_possible_pokemon(PokemonList, opponent_species, Level_50, r, ivs):
    print("Possible sets:")
    odds = 0
    possible_pokemon = []
    possible_opponent_set_numbers = []
    for pokemon in PokemonList:
        if pokemon.Species == opponent_species:
            odds += pokemon.LocalOdds
            possible_pokemon.append(pokemon)
            possible_opponent_set_numbers.append(int(pokemon.Identifier.split()[1]))
    for pokemon in possible_pokemon:
        pokemon.LocalOdds /= odds
        pokemon.Setprinter(ivs, Level_50)
    printline()
    
    if (Level_50 and r > 7) or ((not Level_50) and r > 4):
        print("Which set might it be? Do one of the following:\n1. If you know the set, enter the set number and press [Enter].\n2. Type in a move/item to eliminate sets without that move/item.\n3. Enter multiple set numbers separated by spaces to eliminate them from the list.\n4. Type 'n' and [Enter] if you cannot narrow it down further to move to the opponent's next Pokemon.")
        correct = False
        while not correct:
            # Case: There is only a single possible set
            if len(possible_opponent_set_numbers) == 1:
                print(possible_opponent_set_numbers)
                correct = True
            else: 
                temp = input()
                # Case: Single set number
                if len(temp) == 1 and temp.isdigit() and f"{opponent_species} {temp}" in LEGAL_SETS:
                    possible_opponent_set_numbers = [int(temp)]
                    possible_pokemon = [p for p in possible_pokemon if p.Identifier == f"{opponent_species} {temp}"]
                    correct = True
                # Case: Move seen
                elif temp in LEGAL_MOVES and temp in {move for p in possible_pokemon for move in p.MoveList}:
                    possible_pokemon = [p for p in possible_pokemon if temp in p.MoveList]
                    possible_opponent_set_numbers = [int(p.Identifier.split()[1]) for p in possible_pokemon]
                    odds = 0
                    printline()
                    print("Possible sets:")
                    for pokemon in possible_pokemon:
                        odds += pokemon.LocalOdds
                    for pokemon in possible_pokemon:
                        pokemon.LocalOdds /= odds
                        pokemon.Setprinter(ivs, Level_50)
                    printline()
                # Case: Item seen
                elif temp in LEGAL_ITEMS and temp in {p.Item for p in possible_pokemon}:
                    possible_pokemon = [p for p in possible_pokemon if temp == p.Item]
                    possible_opponent_set_numbers = [int(p.Identifier.split()[1]) for p in possible_pokemon]
                    odds = 0
                    printline()
                    print("Possible sets:")
                    for pokemon in possible_pokemon:
                        odds += pokemon.LocalOdds
                    for pokemon in possible_pokemon:
                        pokemon.LocalOdds /= odds
                        pokemon.Setprinter(ivs, Level_50)
                    printline()
                # Case: Multiple set numbers
                elif all(digit.isdigit() for digit in temp.split()) and all(f"{opponent_species} {digit}" in LEGAL_SETS for digit in temp.split()):
                    set_numbers_to_remove = [int(digit) for digit in temp.split()]
                    possible_opponent_set_numbers = [set_num for set_num in possible_opponent_set_numbers if set_num not in set_numbers_to_remove]
                    possible_pokemon = [p for p in possible_pokemon if int(p.Identifier.split()[1]) in possible_opponent_set_numbers]
                    odds = 0
                    printline()
                    print("Possible sets:")
                    for pokemon in possible_pokemon:
                        odds += pokemon.LocalOdds
                    for pokemon in possible_pokemon:
                        pokemon.LocalOdds /= odds
                        pokemon.Setprinter(ivs, Level_50)
                    printline()
                # Case: No more possible filtering
                elif temp == 'n':
                    correct = True
                else:
                    print("Please select one of the 4 legal options provided.")
        printline()
    return possible_pokemon, possible_opponent_set_numbers
    
def species_listing(pokemon_list, is_noland, draft_ids, draft_species):
    species_counter = Counter()
    for species in draft_species:
        species_counter[species] = 0.0
    for pokemon in pokemon_list:
        if is_noland:
            if pokemon.Identifier not in draft_ids:
                species_counter[pokemon.Species] += pokemon.LocalOdds
        else:
            if pokemon.Species not in draft_species:
                species_counter[pokemon.Species] += pokemon.LocalOdds

    species_list = [(k, v) for k, v in species_counter.items()]
    species_list = sorted(list(species_list), key=lambda pair: pair[1], reverse=False)

    for species_pair in species_list:
        print(f'{species_pair[0]:>15}',f'{float(np.round(species_pair[1]*100,1)):>11}')

def get_this_round_pokemon_df(r, Level_50):
    if Level_50:
        # Set 1 and 3 have overlap, these Pokes are marked as '1.5'
        if r == 1:
            sets = [1, 1.5]
        elif r == 2:
            sets = [1.5, 2]
        elif r <= 7:
            sets = [r]
        else:
            sets = [4, 5, 6, 7, 8]
    else:
        if r <= 4:
            sets = [r + 3]
        else:
            sets = [4, 5, 6, 7, 8, 9]
    return MovesetData.loc[MovesetData['Round'].isin(sets)]

def printline():
    print("---------------------------------------------------------------------------------------------------------")

def main():
    print("---- Welcome to the Battle Factory Assistant! ----")
    print("\nAre you playing Level 50 or Open Level? Enter 1 for Level 50 and 2 for Open Level.")
    Level_50 = True
    correct = False
    while(not correct):
        temp = input()
        if (not temp.isdigit()) or (temp != '1' and temp != '2'):
            print("Make sure you enter '1' or '2'.")
        else:
            if (temp == '2'):
                Level_50 = False
            correct = True
    printline()

    print("What round are you currently in?")
    correct = False
    while(not correct):
        temp = input()
        if not temp.isdigit() or int(temp) <= 0:
            print("Make sure the round number is a positive integer.")
        else:
            r = int(temp)
            correct = True
    printline()

    print("How many swaps do you have at the beginning of the Round?")
    correct = False
    while(not correct):
        temp = input()
        if (not temp.isdigit()) or int(temp) < 0:
            print("Make sure the number of swaps is a positive integer.")
        elif int(temp) > ((r - 1) * 7):
            print("The number of swaps is too high, you can only swap 7 times per round.")
        else:
            swaps = int(temp)
            correct = True
    printline()

    def get_number_of_elevations(var):
        if var < 15:
            return 0
        elif var < 22:
            return 1
        elif var < 29:
            return 2
        elif var < 36:
            return 3
        elif var < 43:
            return 4
        else:
            return 5

    elevations = get_number_of_elevations(swaps)

    def get_ivs_for_round(r):
        match r:
            case 1:
                return 3
            case 2:
                return 6
            case 3:
                return 9
            case 4:
                return 12
            case 5:
                return 15
            case 6:
                return 21
            case 9:
                return 'random'
            case _:
                return 31

    current_round_ivs = get_ivs_for_round(r)
    elevated_ivs = get_ivs_for_round(r + 1)

    print(f"Your draft has {elevations} elevations with {elevated_ivs} IVs. The rest of the draft has {current_round_ivs} IVs.")
    printline()
    need_to_set_draft = True
    print("What battle are you currently on? Put '1' if this is the beginning of the round, or a number between '2' and 7' otherwise.")
    correct = False
    while(not correct):
        temp = input()
        if (not temp.isdigit()) or int(temp) not in {1,2,3,4,5,6,7}:
            print("Must select a Battle number between 1 and 7.")
        else:
            correct = True
            opponent = int(temp)
    printline()

    while(opponent < 8):
        ivs = 3 if opponent < 7 else 6
        is_noland = False
        # If it is a potential Noland round
        if (opponent == 7 and r % 3 == 0):
            print(f"It is opponent 7. Is this a Noland battle? Type 'y' or 'n'.")
            correct = False
            while(not correct):
                temp = input()
                if temp not in {'y', 'n'}:
                    print(f"Make sure to type 'y' or 'n'.")
                elif temp == 'y':
                    is_noland = True
                    correct = True
                    if r == 3:
                        ivs = 15
                    else:
                        ivs = 31
                else:
                    correct = True
            
        
        if not is_noland:
            print(f"Is opponent {opponent} skilled in any type? Otherwise, type 'n'.")
            correct = False
            while(not correct):
                temp = input()
                if temp not in LEGAL_TYPES:
                    print(f"Make sure the type is in this set: {LEGAL_TYPES}.")
                else:
                    preferred_type = temp
                    correct = True
            printline()

            battle_style_dict = {
                0 : "Free-spirited and unrestrained",
                1 : "One based on total preparation",
                2 : "Slow and steady",
                3 : "One of endurance",
                4 : "High risk, high return",
                5 : "Weakening the foe to start",
                6 : "Impossible to predict",
                7 : "Depend on the battle's flow",
                8 : "Flexibly adaptable to the situation"
            }

            print(f"Select the number for the opponent's favorite battle style:\n")
            for k, v in battle_style_dict.items():
                print(f"{k} : {v}\n")
            correct = False
            while(not correct):
                temp = input()
                if (not temp.isdigit()) or int(temp) not in LEGAL_BATTLE_STYLES:
                    print(f"You must choose a valid Battle Style, a value between '0' and '8'.")
                else:
                    battle_style = int(temp)
                    correct = True
            printline()

        if need_to_set_draft:
            # Get the possible Pokemon you and the opponent can have
            this_round_pokemon_df = get_this_round_pokemon_df(r, Level_50)
            elevation_pokemon_df = get_this_round_pokemon_df(r + 1, Level_50)
            
            this_round_species_dict = {}
            elevation_species_dict = {}
            
            def update_species_dict(df, species_dict):
                for _, poke in df.iterrows():
                    identifier = poke["Identifier"]
                    species = poke["Species"]
                    
                    if species not in species_dict:
                        species_dict[species] = {identifier}
                    else:
                        species_dict[species] |= {identifier}

            update_species_dict(this_round_pokemon_df, this_round_species_dict)
            update_species_dict(elevation_pokemon_df, elevation_species_dict)
            
            draft = []
            draft_species = []
            if opponent == 1:
                temp_species_dict = {}
                print("Please input your draft, with the Species name (ex.'Charizard') followed by [Enter].")
                for i in range(6):
                    if i < elevations:
                        temp_species_dict = elevation_species_dict
                    else:
                        temp_species_dict = this_round_species_dict
                    if i > 0:
                        print("Please enter the Species name of your next draft Pokemon followed by [Enter].")
                    correct = False
                    while not correct:
                        temp = input()
                        if temp not in LEGAL_SPECIES or temp not in temp_species_dict:
                            print("ERROR - Please type a legal Species name with the first letter capital and the rest lowercase.")
                        else:
                            if len(temp_species_dict[temp]) == 1: # If there is only one possible set for the Pokemon, add it to the list
                                draft_species.append(temp)
                                draft.extend(list(temp_species_dict[temp]))
                                correct = True
                            else: # If there are multiple sets for the species and it is a potential elevation, list them
                                print("Enter the set number for the Species you entered.")
                                draft_species.append(temp)
                                set_to_id_dict = {}
                                for poke_id in sorted(list(temp_species_dict[temp])):
                                    pokemon = PokemonSet(poke_id)
                                    set_number = poke_id.split()[1]
                                    set_to_id_dict[set_number] = poke_id
                                    pokemon.draft_set_printer()
                                inner_correct = False
                                while not inner_correct:
                                    inner_temp = input()
                                    if inner_temp not in set_to_id_dict.keys():
                                        print("ERROR - Please enter one of the set numbers listed.")
                                    else:
                                        draft.append(set_to_id_dict[inner_temp])
                                        inner_correct = True
                                correct = True
                            printline()

                players_team = []
                opponents_team = []
                need_to_set_draft = False
            else:
                # Combine the current and next rounds if there is an elevation
                if elevations:
                    player_species_dict = {this_round_species_dict[k] | elevation_species_dict[k] for k in this_round_species_dict.keys()}
                else:
                    player_species_dict = this_round_species_dict
                draft = []
                print("Please input the 3 Pokemon Species name in your current team (don't account for a swap), followed by [Enter].")
                players_team = []
                opponents_team = []
                for i in range(3):
                    if i > 0:
                        print("Please enter the Species name of your next Pokemon followed by [Enter].")
                    correct = False
                    while not correct:
                        temp = input()
                        if temp not in LEGAL_SPECIES or temp not in player_species_dict:
                            print("ERROR - Please type a legal Species name with the first letter capital and the rest lowercase.")
                        else:
                            if len(player_species_dict[temp]) == 1: # If there is only one possible set for the Pokemon, add it to the list
                                players_team.append(player_species_dict[temp].pop())
                                draft_species.append(temp)
                                draft.extend(list(player_species_dict[temp]))
                                correct = True
                            else: # If there are multiple sets for the species and it is a potential elevation, list them
                                print("Enter the set number for the Species you entered.")
                                draft_species.append(temp)
                                set_to_id_dict = {}
                                for poke_id in sorted(list(player_species_dict[temp])):
                                    pokemon = PokemonSet(poke_id)
                                    set_number = poke_id.split()[1]
                                    set_to_id_dict[set_number] = poke_id
                                    pokemon.draft_set_printer()
                                inner_correct = False
                                while not inner_correct:
                                    inner_temp = input()
                                    if inner_temp not in set_to_id_dict.keys():
                                        print("ERROR - Please enter one of the set numbers listed.")
                                    else:
                                        players_team.append(set_to_id_dict[inner_temp])
                                        draft.append(set_to_id_dict[inner_temp])
                                        inner_correct = True
                                correct = True
                            printline()
                print("Now please input the 3 Pokemon Species names in your opponent's last team. For each Pokemon you don't know, type 'n' followed by [Enter].")
                for _ in range(3):
                    if i > 0:
                        print("Please enter the Species name of your opponent's next Pokemon, or 'n' if you don't know.")
                    correct = False
                    while not correct:
                        temp = input()
                        if (temp == 'n'):
                            correct = True
                        elif temp not in LEGAL_SPECIES or temp not in this_round_species_dict:
                            print("ERROR - Please type a legal Species name with the first letter capital and the rest lowercase.")
                        else:
                            if len(this_round_species_dict[temp]) == 1: # If there is only one possible set for the Pokemon, add it to the list
                                opponents_team.append(this_round_species_dict[temp].pop())
                                draft_species.append(temp)
                                draft.extend(list(this_round_species_dict[temp]))
                                correct = True
                            else: # If there are multiple sets for the species and it is a potential elevation, list them
                                print("Enter the set number for the Species you entered. If you don't know, type 'n'.")
                                draft_species.append(temp)
                                set_to_id_dict = {}
                                for poke_id in sorted(list(this_round_species_dict[temp])):
                                    pokemon = PokemonSet(poke_id)
                                    set_number = poke_id.split()[1]
                                    set_to_id_dict[set_number] = poke_id
                                    pokemon.draft_set_printer()
                                inner_correct = False
                                while not inner_correct:
                                    inner_temp = input()
                                    if (inner_temp == 'n'):
                                        inner_correct = True
                                    elif inner_temp not in set_to_id_dict.keys():
                                        print("ERROR - Please enter one of the set numbers listed.")
                                    else:
                                        opponents_team.append(set_to_id_dict[inner_temp])
                                        draft.append(set_to_id_dict[inner_temp])
                                        inner_correct = True
                                correct = True
                            printline()
                need_to_set_draft = False

        # CONVERT ALL SET IDENTIFIERS TO POKEMON WITH INFORMATION FROM TABLE
        pokemon_list = []
        species_set = set()
        for _, poke in this_round_pokemon_df.iterrows():
            identifier = poke["Identifier"]
            species_set.add(poke['Species'])
            if (opponent == 1 and identifier.split()[0] not in draft_species) or (opponent > 1 and identifier not in players_team and identifier not in opponents_team):
                pokemon = PokemonSet(identifier)
                pokemon_list.append(pokemon)
        print("Calculating opposing team probabilites... (This can take a while in No Man's Land and Noland Battles)")
        printline()
        TeamCombinations = []
        count_dict = Counter()
        pokemon_set = set(pokemon_list)
        with ProcessPoolExecutor() as executor:
            if is_noland:
                draft_set = remove_duplicate_species(draft)
                futures = {executor.submit(is_legal_noland_team, pokemon, pokemon_set, draft_set): pokemon for pokemon in pokemon_set if pokemon.Identifier not in draft_set}
            else:
                draft_species_set = set(draft_species)
                futures = {executor.submit(is_legal_team, pokemon, pokemon_set, draft_species_set, preferred_type, battle_style): pokemon for pokemon in pokemon_set if pokemon.Species not in draft_species_set}
            def future_generator():
                for future in as_completed(futures):
                    teams, local_counts = future.result()
                    if teams:
                        TeamCombinations.extend(teams)
                        count_dict.update(local_counts)
                    yield future

            for _ in tqdm(future_generator(), total=len(futures)):
                pass

        for pokemon in pokemon_list:
            local_odds = count_dict[pokemon.Identifier] / len(TeamCombinations)
            pokemon.LocalOdds = local_odds
        
        PokemonList = sorted(pokemon_list, key=lambda pokemon: pokemon.LocalOdds, reverse=False)

        for pokemon in PokemonList:
            pokemon.Setprinter(ivs, Level_50)
        printline()

        # Print species odds if it is in No Man's Land
        if (Level_50 and r > 7) or ((not Level_50) and r > 4):
            if is_noland:
                draft_set = remove_duplicate_species(draft)
                draft_species_set = {p.split()[0] for p in draft}
                species_listing(PokemonList, is_noland, draft_set, draft_species_set)
            else:
                draft_species_set = set(draft_species)
                species_listing(PokemonList, is_noland, draft, draft_species_set)
            printline()

        print("There are", len(pokemon_list), "different sets, which constitute", int(len(TeamCombinations)/6), "different teams.")
        printline()

        if opponent == 1:
            draft_dict = {}
            counter = 1
            for p in draft:
                draft_dict[counter] = p
                counter += 1
            print(f"Here are the Pokemon from the draft:\n")
            for k, v in draft_dict.items():
                PokemonSet(v).draft_set_printer(k)
            print("Please input your team selection from the draft. Type the number for the Pokemon you want to use followed by [Enter]")
            for i in range(3):
                correct = False
                while(not correct):
                    temp = input()
                    if (not temp.isdigit()) or int(temp) not in [1,2,3,4,5,6]:
                        print("Must select a Poke using values between 1 and 6.")
                    else:
                        players_team.append(draft_dict[int(temp)])
                        correct = True
            printline()
        else:
            # Set draft to  player plus opponents team for opposing team calcs
            draft = players_team + opponents_team
            draft_species = [p.split()[0] for p in draft]
            print(f"Your current team is:")
            counter = 1
            for id in players_team:
                PokemonSet(id).draft_set_printer(counter)
                counter += 1
            printline()
            print(f"The previous opponent's possible Pokemon are:")
            counter = 1
            for id in opponents_team:
                PokemonSet(id).draft_set_printer(counter)
                counter += 1
            printline()
            print(f"Did you swap? Then type your Pokemon's number [Enter]. Otherwise type 'n'.")
            
            correct = False
            swap = True
            while((not correct) and swap):
                temp = input()
                if temp == "n":
                    swap = False
                elif (not temp.isdigit()) or int(temp) not in {1, 2, 3}:
                    print("Must select a Poke on the player's current team by choosing '1', '2', or '3'.")
                else:
                    player_replace = players_team[int(temp) - 1]
                    correct = True
            correct = False
            printline()
            print(f"Put the number for the opponent's Pokemon you swapped with and press [Enter]. Or cancel the swap by typing 'Cancel'.")
            while((not correct) and swap):
                temp = input()
                if temp == "Cancel":
                    swap = False
                elif (not temp.isdigit()) or int(temp) not in {i for i in range(1, len(opponents_team) + 1)}:
                    print("ERROR - Must select a number listed above.")
                else:
                    opponent_replace = opponents_team[int(temp) - 1]
                    correct = True

            if swap:
                players_team = [opponent_replace if poke == player_replace else poke for poke in players_team]

            printline()

            if swap:
                print(f"Your new team is:")
                counter = 1
                for id in players_team:
                    PokemonSet(id).draft_set_printer(counter)
                    counter += 1
                printline()	

        # Reset opponents team before starting the battles
        opponents_team = []

        print("What Pokemon Species does the opponent lead with? (Only capitalize the first letter and don't include set numbers).")
        opponent_species = get_opponent_species()
        printline()
        possible_pokemon_1, possible_opponent_set_numbers = get_possible_pokemon(PokemonList, opponent_species, Level_50, r, ivs)
        opponents_team.extend([opponent_species + ' ' + str(opponent_set) for opponent_set in possible_opponent_set_numbers])
        draft.extend([p.Identifier for p in possible_pokemon_1])
        draft_species.extend([p.Species for p in possible_pokemon_1])

        TeamCombinationsAfterFirstPoke = []
        count_dict = Counter()

        with ProcessPoolExecutor() as executor:
            if is_noland:
                draft_set = remove_duplicate_species(draft)
                futures = {executor.submit(is_legal_noland_team, pokemon1, set(pokemon_list), draft_set): pokemon1 for pokemon1 in pokemon_list if pokemon1 in possible_pokemon_1}
            else:
                draft_species_set = set(draft_species)
                futures = {executor.submit(is_legal_team, pokemon1, set(pokemon_list), draft_species_set, preferred_type, battle_style): pokemon1 for pokemon1 in pokemon_list if pokemon1 in possible_pokemon_1}
            
            def future_generator():
                for future in as_completed(futures):
                    teams, local_counts = future.result()
                    if teams:
                        TeamCombinationsAfterFirstPoke.extend(teams)
                        count_dict.update(local_counts)
                    yield future

            for _ in tqdm(future_generator(), total=len(futures)):
                pass

        for pokemon in pokemon_list:
            local_odds = count_dict[pokemon.Identifier] / len(TeamCombinationsAfterFirstPoke)
            pokemon.LocalOdds = local_odds
        
        PokemonList = sorted(pokemon_list, key=lambda pokemon: pokemon.LocalOdds, reverse=False)

        for pokemon in PokemonList:
            if pokemon not in possible_pokemon_1:
                pokemon.Setprinter(ivs, Level_50)
        printline()

        print("There are", len(pokemon_list), "different sets, which constitute", int(len(TeamCombinationsAfterFirstPoke)/6), "different teams.")
        printline()

        # Print species odds if it is in No Man's Land
        if (Level_50 and r > 7) or ((not Level_50) and r > 4):
            if is_noland:
                draft_set = remove_duplicate_species(draft)
                draft_species_set = {p.split()[0] for p in draft}
                species_listing(PokemonList, is_noland, draft_set, draft_species_set)
            else:
                draft_species_set = set(draft_species)
                species_listing(PokemonList, is_noland, draft, draft_species_set)
            printline()

        print("Please enter another Pokemon Species you have seen. (Only capitalize the first letter).")
        printline()
        opponent_species = get_opponent_species()
        printline()
        possible_pokemon_2, possible_opponent_set_numbers = get_possible_pokemon(PokemonList, opponent_species, Level_50, r, ivs)
        opponents_team.extend([opponent_species + ' ' + str(opponent_set) for opponent_set in possible_opponent_set_numbers])
        draft.extend([p.Identifier for p in possible_pokemon_2])
        draft_species.extend([p.Species for p in possible_pokemon_2])

        TeamCombinationsAfterSecondPoke = []

        draft_set = remove_duplicate_species(draft)
        draft_species_set = set(draft_species)
        for pokemon1 in possible_pokemon_1:
            for pokemon2 in possible_pokemon_2:
                if is_noland:
                    for pokemon3 in PokemonList:
                        if (    pokemon3.Identifier not in draft_set  
                            and pokemon1.Species != pokemon2.Species
                            and pokemon2.Species != pokemon3.Species
                            and pokemon1.Species != pokemon3.Species
                            and pokemon1.Item    != pokemon2.Item
                            and pokemon2.Item    != pokemon3.Item
                            and pokemon1.Item    != pokemon3.Item
                            ):
                            TeamCombinationsAfterSecondPoke.append((pokemon1,pokemon2,pokemon3))
                else:
                    for pokemon3 in PokemonList:
                        if (    pokemon3.Species not in draft_species_set
                            and pokemon1.Species != pokemon2.Species 
                            and pokemon2.Species != pokemon3.Species
                            and pokemon1.Species != pokemon3.Species
                            and pokemon1.Item    != pokemon2.Item
                            and pokemon2.Item    != pokemon3.Item
                            and pokemon1.Item    != pokemon3.Item
                            and types_legal(pokemon1, pokemon2, pokemon3, preferred_type)
                            and moves_legal(pokemon1, pokemon2, pokemon3, battle_style)
                            ):
                            TeamCombinationsAfterSecondPoke.append((pokemon1,pokemon2,pokemon3))

        for pokemon in PokemonList:
            counter = 0
            for team in TeamCombinationsAfterSecondPoke:
                if team[2] == pokemon:
                    counter += 1
            pokemon.LocalOdds = counter/len(TeamCombinationsAfterSecondPoke)

        PokemonList = sorted(PokemonList, key=lambda pokemon: pokemon.LocalOdds, reverse=False)

        for pokemon in PokemonList:
            if pokemon not in possible_pokemon_1 and pokemon not in possible_pokemon_2:
                pokemon.Setprinter(ivs, Level_50)
        printline()

         # Print species odds if it is in No Man's Land
        if (Level_50 and r > 7) or ((not Level_50) and r > 4):
            if is_noland:
                draft_set = remove_duplicate_species(draft)
                draft_species_set = {p.split()[0] for p in draft}
                species_listing(PokemonList, is_noland, draft_set, draft_species_set)
            else:
                draft_species_set = set(draft_species)
                species_listing(PokemonList, is_noland, draft, draft_species_set)
            printline()
        
        print("Please enter the last Pokemon's Species name. (Only capitalize the first letter).")
        printline()
        opponent_species = get_opponent_species()
        printline()
        _, possible_opponent_set_numbers = get_possible_pokemon(PokemonList, opponent_species, Level_50, r, ivs)
        opponents_team.extend([opponent_species + ' ' + str(opponent_set) for opponent_set in possible_opponent_set_numbers])
        draft = players_team + opponents_team
        draft_species = [p.split()[0] for p in draft]

        print("You won the battle!\n")
        printline()
        opponent += 1
    print(f"Congrats! You won Round {r} of the Battle Factory!")
    exit()

if __name__ == "__main__":
    main()