├── card.db ├── amonkhet_boosterbundledeckbuilder.txt ├── deck.db ├── README.md └── mtgdeckhunter.py /card.db: -------------------------------------------------------------------------------- 1 | {"Open into Wonder": 1, "Limits of Solidarity": 2, "Reduce // Rubble": 2, "Pathmaker Initiate": 5, "Welding Sparks": 1, "Token: Anointer Priest": 5, "Naga Oracle": 4, "Air Elemental": 1, "Sixth Sense": 1, "Compelling Argument": 6, "Desert Cerodon": 3, "Filigree Familiar": 1, "Sacred Excavation": 2, "Spidery Grasp": 7, "Lord of the Accursed": 3, "Perilous Predicament": 1, "Token: Unwavering Initiate": 3, "Irrigated Farmland": 1, "Evolving Wilds": 10, "Aerial Responder": 1, "Wild Wanderer": 3, "Baleful Ammit": 1, "Binding Mummy": 4, "Honored Crop-Captain": 4, "Scattered Groves": 1, "Fling": 7, "Sparring Mummy": 4, "Cut // Ribbons": 1, "Painted Bluffs": 5, "Battlefield Scavenger": 1, "Trial of Ambition": 1, "Oketra's Monument": 2, "Pouncing Cheetah": 3, "Ahn-Crop Crasher": 5, "Neheb, the Worthy": 1, "Start // Finish": 3, "Horror of the Broken Lands": 6, "Shed Weakness": 5, "Tricks of the Trade": 1, "Raise Dead": 1, "Hazoret's Monument": 2, "Miasmic Mummy": 5, "Token: Tah-Crop Skirmisher": 3, "Outland Boar": 1, "Vizier of Deferment": 4, "Island": 44, "Stealer of Secrets": 1, "Crocodile of the Crossing": 4, "Failure // Comply": 1, "Sengir Vampire": 1, "Shefet Monitor": 4, "Enigma Drake": 1, "Devoted Crop-Mate": 2, "Submerged Boneyard": 2, "Token: Temmet, Vizier of Naktamun": 2, "Cryptic Serpent": 2, "Divine Verdict": 1, "Stir the Sands": 1, "Destined // Lead": 2, "Shipwreck Moray": 1, "Watchful Naga": 1, "Wing Snare": 1, "Trespasser's Curse": 5, "Spring // Mind": 4, "Unwavering Initiate": 6, "Sheltered Thicket": 1, "Pyramid of the Pantheon": 1, "Flameblade Adept": 2, "Tranquil Expanse": 2, "Harvest Season": 2, "Naga Vitalist": 5, "Oketra's Attendant": 2, "Cruel Reality": 2, "Decimator Beetle": 1, "Hungry Flames": 1, "Mighty Leap": 5, "Impeccable Timing": 5, "Standing Troops": 1, "Seraph of the Suns": 1, "Dusk // Dawn": 1, "Djeru's Resolve": 4, "Minotaur Sureshot": 4, "Blighted Bat": 7, "Nimble-Blade Khenra": 2, "Magma Spray": 5, "Shadow of the Grave": 1, "Those Who Serve": 5, "Snare Thopter": 1, "Final Reward": 5, "Cursed Minotaur": 3, "Splendid Agony": 7, "Highland Lake": 2, "Emberhorn Minotaur": 4, "Bloodhunter Bat": 1, "Plains": 48, "Deem Worthy": 2, "Bounty of the Luxa": 2, "Heart-Piercer Manticore": 1, "Forsaken Sanctuary": 2, "Defiant Greatmaw": 1, "Tattered Mummy": 2, "Aven Mindcensor": 1, "Canyon Slough": 1, "Approach of the Second Sun": 2, "Bastion Mastodon": 1, "Renegade Map": 3, "Watchers of the Dead": 1, "Harnessed Lightning": 1, "Trial of Knowledge": 2, "In Oketra's Name": 4, "Revolutionary Rebuff": 1, "Floodwaters": 3, "Aven Initiate": 5, "Rhet-Crop Spearmaster": 5, "Token: Labyrinth Guardian": 3, "Cartouche of Ambition": 5, "Stalking Tiger": 1, "Oketra the True": 1, "Labyrinth Guardian": 3, "Galestrike": 1, "Oakenform": 1, "Nissa, Steward of Elements": 1, "Benefaction of Rhonas": 6, "Onward // Victory": 1, "Festering Mummy": 5, "Larger Than Life": 2, "Trial of Solidarity": 5, "Stormfront Pegasus": 1, "Soulstinger": 4, "Violent Impact": 6, "Weldfast Monitor": 1, "Shivan Dragon": 1, "Negate": 1, "Ancient Crab": 5, "Channeler Initiate": 2, "Stone Quarry": 2, "Trueheart Duelist": 3, "Time to Reflect": 2, "Hijack": 1, "Mind Rot": 1, "Gifted Aetherborn": 1, "Anointer Priest": 8, "Giant Spider": 4, "Bone Picker": 1, "Rootwalla": 1, "Victory's Herald": 1, "Skyswirl Harrier": 1, "Commit // Memory": 1, "Token: Aven Initiate": 5, "Colossapede": 4, "Censor": 1, "Implement of Improvement": 1, "Universal Solvent": 1, "Unburden": 4, "Cascading Cataracts": 1, "Winds of Rebuke": 4, "Vizier of Tumbling Sands": 2, "Cartouche of Knowledge": 8, "Liliana's Mastery": 1, "Resourceful Return": 1, "Quarry Hauler": 4, "Thundering Giant": 1, "Cinder Barrens": 2, "Mountain": 46, "As Foretold": 1, "Bloodlust Inciter": 4, "Blazing Volley": 5, "Token: Heart-Piercer Manticore": 1, "Electrify": 6, "Aethergeode Miner": 1, "Daring Demolition": 1, "Token: Oketra's Attendant": 2, "Token: Trueheart Duelist": 2, "Token: Sacred Cat": 3, "Soul-Scar Mage": 1, "Bastion Inventor": 1, "Compulsory Rest": 4, "Forest": 48, "Sacred Cat": 7, "Token: Aven Wind Guide": 3, "Serra Angel": 1, "Natural Obsolescence": 1, "Vizier of Remedies": 3, "Wayward Servant": 3, "Hazoret's Favor": 1, "Oashra Cultivator": 6, "Temmet, Vizier of Naktamun": 1, "Gate to the Afterlife": 1, "Garruk's Horde": 1, "Blossoming Defense": 1, "Consuming Fervor": 2, "Aetherborn Marauder": 1, "Stinging Shot": 3, "Greater Sandwurm": 6, "Drake Haven": 1, "Faith of the Devoted": 2, "Hyena Pack": 3, "Synchronized Strike": 3, "Cast Out": 1, "Brazen Scourge": 1, "Sunscorched Desert": 6, "Woodland Stream": 2, "Nightmare": 1, "Ornery Kudu": 4, "Slither Blade": 7, "Manticore of the Gauntlet": 4, "Doomed Dissenter": 5, "Honored Hydra": 1, "Rhonas's Monument": 1, "Supply Caravan": 4, "Hapatra's Mark": 2, "Aether Poisoner": 1, "Fragmentize": 1, "Nimble Innovator": 1, "Token: Vizier of Many Faces": 1, "Tasseled Dromedary": 1, "Tah-Crop Elite": 6, "Nest of Scarabs": 1, "Foul Orchard": 2, "Scaled Behemoth": 1, "Meandering River": 2, "Rags // Riches": 1, "Arborback Stomper": 1, "Malfunction": 1, "Swamp": 46, "Renegade Freighter": 1, "Hieroglyphic Illumination": 7, "Irontread Crusher": 1, "Edifice of Authority": 2, "Timber Gorge": 2, "Cartouche of Solidarity": 8, "Bloodrage Brawler": 2, "Wasteland Scorpion": 3, "Ruthless Sniper": 1, "Lay Claim": 4, "Cancel": 5, "Shimmerscale Drake": 4, "Thresher Lizard": 4, "Druid of the Cowl": 2, "Bitterblade Warrior": 3, "Champion of Rhonas": 2, "Bastion Enforcer": 1, "Glory-Bound Initiate": 1, "Embalmer's Tools": 2, "Pitiless Vizier": 8, "Gift of Paradise": 4, "Heaven // Earth": 1, "Initiate's Companion": 4, "Winged Shepherd": 4, "Shock": 3, "Supernatural Stamina": 5, "Leave in the Dust": 1, "Brute Strength": 8, "Shadowstorm Vizier": 2, "Hekma Sentinels": 6, "Cradle of the Accursed": 4, "Khenra Charioteer": 1, "River Serpent": 7, "Ahn-Crop Champion": 1, "Zenith Seeker": 1, "Aviary Mechanic": 1, "Vedalken Blademaster": 1, "Rush of Vitality": 1, "By Force": 2, "Dune Beetle": 3, "Painful Lesson": 7, "Exemplar of Strength": 2, "Token: Honored Hydra": 1, "Merciless Javelineer": 1, "Token: Glyph Keeper": 2, "Fan Bearer": 4, "Token: Angel of Sanctions": 2, "Angler Drake": 6, "Protection of the Hekma": 2, "Curator of Mysteries": 1, "Kefnet's Monument": 1, "Glyph Keeper": 1, "Cathartic Reunion": 1, "Scarab Feast": 4, "Graceful Cat": 1, "Ridgescale Tusker": 1, "Wander in Death": 5, "Weaver of Currents": 4, "Essence Scatter": 5, "Dissenter's Deliverance": 3, "Harsh Mentor": 1, "Pursue Glory": 5, "Forsake the Worldly": 3, "Manglehorn": 3, "Scribe of the Mindful": 3, "Renewed Faith": 1, "Haze of Pollen": 7, "Bontu's Monument": 1, "Trueheart Twins": 2, "Trial of Zeal": 3, "Untamed Hunger": 1, "Honed Khopesh": 6, "Cartouche of Zeal": 9, "Lay Bare the Heart": 2, "Decision Paralysis": 6, "Sphinx of Magosi": 1, "Anointed Procession": 1, "Dispossess": 1, "Hooded Brawler": 5, "Nef-Crop Entangler": 7, "Tah-Crop Skirmisher": 5, "Spireside Infiltrator": 2, "Seeker of Insight": 3, "Warfire Javelineer": 2, "Aven Wind Guide": 3, "Grim Strider": 1, "Vizier of Many Faces": 2, "Revoke Privileges": 1, "Cartouche of Strength": 4, "Luxa River Shrine": 4, "Trial of Strength": 5, "Dread Wanderer": 1, "Cultivator of Blades": 1, "Tormenting Voice": 4, "Illusory Wrappings": 3, "Gust Walker": 7, "Plague Belcher": 2, "Insult // Injury": 1, "Glorious End": 1, "Hinterland Drake": 1, "Night Market Guard": 1} -------------------------------------------------------------------------------- /amonkhet_boosterbundledeckbuilder.txt: -------------------------------------------------------------------------------- 1 | 2 Edifice of Authority 2 | 2 Embalmer's Tools 3 | 1 Gate to the Afterlife 4 | 1 Implement of Improvement 5 | 4 Luxa River Shrine 6 | 1 Pyramid of the Pantheon 7 | 3 Renegade Map 8 | 1 Universal Solvent 9 | 6 Honed Khopesh 10 | 1 Irontread Crusher 11 | 1 Renegade Freighter 12 | 1 Watchers of the Dead 13 | 1 Night Market Guard 14 | 1 Bastion Mastodon 15 | 1 Filigree Familiar 16 | 1 Weldfast Monitor 17 | 1 Snare Thopter 18 | 48 Forest 19 | 44 Island 20 | 46 Mountain 21 | 48 Plains 22 | 46 Swamp 23 | 1 Aetherborn Marauder 24 | 1 Gifted Aetherborn 25 | 1 Seraph of the Suns 26 | 1 Serra Angel 27 | 4 Winged Shepherd 28 | 4 Ornery Kudu 29 | 1 Bloodhunter Bat 30 | 1 Arborback Stomper 31 | 3 Desert Cerodon 32 | 1 Garruk's Horde 33 | 3 Manglehorn 34 | 1 Ridgescale Tusker 35 | 1 Bone Picker 36 | 1 Skyswirl Harrier 37 | 2 Oketra's Attendant 38 | 5 Aven Initiate 39 | 3 Aven Wind Guide 40 | 6 Tah-Crop Elite 41 | 1 Aven Mindcensor 42 | 1 Zenith Seeker 43 | 1 Outland Boar 44 | 4 Quarry Hauler 45 | 4 Supply Caravan 46 | 1 Tasseled Dromedary 47 | 1 Graceful Cat 48 | 4 Initiate's Companion 49 | 3 Pouncing Cheetah 50 | 7 Sacred Cat 51 | 1 Stalking Tiger 52 | 5 Ancient Crab 53 | 4 Crocodile of the Crossing 54 | 1 Scaled Behemoth 55 | 1 Baleful Ammit 56 | 1 Shivan Dragon 57 | 6 Angler Drake 58 | 1 Enigma Drake 59 | 1 Hinterland Drake 60 | 4 Shimmerscale Drake 61 | 1 Aviary Mechanic 62 | 1 Aethergeode Miner 63 | 1 Aerial Responder 64 | 1 Bastion Enforcer 65 | 1 Air Elemental 66 | 1 Cultivator of Blades 67 | 2 Druid of the Cowl 68 | 3 Wild Wanderer 69 | 1 Shipwreck Moray 70 | 1 Thundering Giant 71 | 1 Brazen Scourge 72 | 1 Defiant Greatmaw 73 | 1 Grim Strider 74 | 6 Horror of the Broken Lands 75 | 5 Doomed Dissenter 76 | 1 Ruthless Sniper 77 | 1 Aether Poisoner 78 | 8 Anointer Priest 79 | 1 Harsh Mentor 80 | 6 Hekma Sentinels 81 | 3 Scribe of the Mindful 82 | 2 Shadowstorm Vizier 83 | 4 Vizier of Deferment 84 | 3 Vizier of Remedies 85 | 2 Vizier of Tumbling Sands 86 | 2 Channeler Initiate 87 | 6 Oashra Cultivator 88 | 2 Spireside Infiltrator 89 | 1 Stealer of Secrets 90 | 1 Standing Troops 91 | 1 Ahn-Crop Champion 92 | 4 Bloodlust Inciter 93 | 2 Devoted Crop-Mate 94 | 2 Exemplar of Strength 95 | 1 Glory-Bound Initiate 96 | 4 Honored Crop-Captain 97 | 7 Nef-Crop Entangler 98 | 5 Rhet-Crop Spearmaster 99 | 3 Trueheart Duelist 100 | 6 Unwavering Initiate 101 | 7 Gust Walker 102 | 5 Pathmaker Initiate 103 | 3 Seeker of Insight 104 | 1 Soul-Scar Mage 105 | 3 Hyena Pack 106 | 3 Labyrinth Guardian 107 | 4 Colossapede 108 | 1 Decimator Beetle 109 | 3 Dune Beetle 110 | 1 Battlefield Scavenger 111 | 3 Bitterblade Warrior 112 | 2 Champion of Rhonas 113 | 2 Flameblade Adept 114 | 1 Khenra Charioteer 115 | 2 Nimble-Blade Khenra 116 | 2 Trueheart Twins 117 | 1 Rootwalla 118 | 4 Shefet Monitor 119 | 4 Thresher Lizard 120 | 1 Heart-Piercer Manticore 121 | 4 Manticore of the Gauntlet 122 | 4 Minotaur Sureshot 123 | 8 Pitiless Vizier 124 | 5 Ahn-Crop Crasher 125 | 2 Bloodrage Brawler 126 | 4 Emberhorn Minotaur 127 | 1 Merciless Javelineer 128 | 2 Warfire Javelineer 129 | 4 Naga Oracle 130 | 5 Naga Vitalist 131 | 4 Weaver of Currents 132 | 7 Slither Blade 133 | 5 Hooded Brawler 134 | 5 Tah-Crop Skirmisher 135 | 1 Watchful Naga 136 | 1 Nightmare 137 | 1 Stormfront Pegasus 138 | 3 Wasteland Scorpion 139 | 4 Soulstinger 140 | 2 Cryptic Serpent 141 | 7 River Serpent 142 | 2 Vizier of Many Faces 143 | 1 Honored Hydra 144 | 1 Curator of Mysteries 145 | 1 Glyph Keeper 146 | 1 Sphinx of Magosi 147 | 4 Giant Spider 148 | 1 Sengir Vampire 149 | 1 Bastion Inventor 150 | 1 Nimble Innovator 151 | 1 Vedalken Blademaster 152 | 6 Greater Sandwurm 153 | 4 Binding Mummy 154 | 4 Fan Bearer 155 | 5 Festering Mummy 156 | 3 Lord of the Accursed 157 | 4 Sparring Mummy 158 | 5 Those Who Serve 159 | 3 Wayward Servant 160 | 7 Blighted Bat 161 | 2 Plague Belcher 162 | 1 Dread Wanderer 163 | 5 Miasmic Mummy 164 | 2 Tattered Mummy 165 | 3 Cursed Minotaur 166 | 1 Victory's Herald 167 | 1 Anointed Procession 168 | 1 As Foretold 169 | 2 Bounty of the Luxa 170 | 1 Cast Out 171 | 1 Drake Haven 172 | 2 Faith of the Devoted 173 | 1 Hazoret's Favor 174 | 1 Liliana's Mastery 175 | 1 Nest of Scarabs 176 | 2 Protection of the Hekma 177 | 1 Trial of Ambition 178 | 2 Trial of Knowledge 179 | 5 Trial of Solidarity 180 | 5 Trial of Strength 181 | 3 Trial of Zeal 182 | 4 Compulsory Rest 183 | 2 Consuming Fervor 184 | 4 Gift of Paradise 185 | 3 Illusory Wrappings 186 | 4 Lay Claim 187 | 1 Malfunction 188 | 1 Oakenform 189 | 1 Revoke Privileges 190 | 1 Sixth Sense 191 | 1 Tricks of the Trade 192 | 1 Untamed Hunger 193 | 5 Cartouche of Ambition 194 | 8 Cartouche of Knowledge 195 | 8 Cartouche of Solidarity 196 | 4 Cartouche of Strength 197 | 9 Cartouche of Zeal 198 | 2 Cruel Reality 199 | 5 Trespasser's Curse 200 | 1 Blossoming Defense 201 | 8 Brute Strength 202 | 5 Cancel 203 | 1 Censor 204 | 1 Commit // Memory 205 | 6 Decision Paralysis 206 | 2 Deem Worthy 207 | 2 Destined // Lead 208 | 3 Dissenter's Deliverance 209 | 1 Divine Verdict 210 | 4 Djeru's Resolve 211 | 6 Electrify 212 | 5 Essence Scatter 213 | 1 Failure // Comply 214 | 5 Final Reward 215 | 7 Fling 216 | 3 Forsake the Worldly 217 | 1 Galestrike 218 | 1 Glorious End 219 | 2 Hapatra's Mark 220 | 1 Harnessed Lightning 221 | 7 Haze of Pollen 222 | 1 Heaven // Earth 223 | 7 Hieroglyphic Illumination 224 | 1 Hungry Flames 225 | 5 Impeccable Timing 226 | 4 In Oketra's Name 227 | 1 Leave in the Dust 228 | 5 Magma Spray 229 | 5 Mighty Leap 230 | 1 Natural Obsolescence 231 | 1 Negate 232 | 1 Onward // Victory 233 | 1 Perilous Predicament 234 | 5 Pursue Glory 235 | 2 Reduce // Rubble 236 | 1 Renewed Faith 237 | 1 Revolutionary Rebuff 238 | 1 Rush of Vitality 239 | 4 Scarab Feast 240 | 1 Shadow of the Grave 241 | 5 Shed Weakness 242 | 3 Shock 243 | 7 Spidery Grasp 244 | 7 Splendid Agony 245 | 3 Start // Finish 246 | 3 Stinging Shot 247 | 5 Supernatural Stamina 248 | 3 Synchronized Strike 249 | 2 Time to Reflect 250 | 1 Welding Sparks 251 | 4 Winds of Rebuke 252 | 1 Cascading Cataracts 253 | 2 Cinder Barrens 254 | 10 Evolving Wilds 255 | 2 Forsaken Sanctuary 256 | 2 Foul Orchard 257 | 2 Highland Lake 258 | 2 Meandering River 259 | 2 Stone Quarry 260 | 2 Submerged Boneyard 261 | 2 Timber Gorge 262 | 2 Tranquil Expanse 263 | 2 Woodland Stream 264 | 4 Cradle of the Accursed 265 | 5 Painted Bluffs 266 | 6 Sunscorched Desert 267 | 1 Scattered Groves 268 | 1 Sheltered Thicket 269 | 1 Irrigated Farmland 270 | 1 Canyon Slough 271 | 1 Bontu's Monument 272 | 2 Hazoret's Monument 273 | 1 Kefnet's Monument 274 | 2 Oketra's Monument 275 | 1 Rhonas's Monument 276 | 1 Oketra the True 277 | 1 Temmet, Vizier of Naktamun 278 | 1 Neheb, the Worthy 279 | 1 Nissa, Steward of Elements 280 | 2 Approach of the Second Sun 281 | 6 Benefaction of Rhonas 282 | 5 Blazing Volley 283 | 2 By Force 284 | 1 Cathartic Reunion 285 | 6 Compelling Argument 286 | 1 Cut // Ribbons 287 | 1 Daring Demolition 288 | 1 Dispossess 289 | 1 Dusk // Dawn 290 | 3 Floodwaters 291 | 1 Fragmentize 292 | 2 Harvest Season 293 | 1 Hijack 294 | 1 Insult // Injury 295 | 2 Larger Than Life 296 | 2 Lay Bare the Heart 297 | 2 Limits of Solidarity 298 | 1 Mind Rot 299 | 1 Open into Wonder 300 | 7 Painful Lesson 301 | 1 Rags // Riches 302 | 1 Raise Dead 303 | 1 Resourceful Return 304 | 2 Sacred Excavation 305 | 4 Spring // Mind 306 | 1 Stir the Sands 307 | 4 Tormenting Voice 308 | 4 Unburden 309 | 6 Violent Impact 310 | 5 Wander in Death 311 | 1 Wing Snare 312 | 2 Token: Angel of Sanctions 313 | 2 Token: Oketra's Attendant 314 | 5 Token: Aven Initiate 315 | 3 Token: Aven Wind Guide 316 | 3 Token: Sacred Cat 317 | 5 Token: Anointer Priest 318 | 2 Token: Temmet, Vizier of Naktamun 319 | 2 Token: Trueheart Duelist 320 | 3 Token: Unwavering Initiate 321 | 3 Token: Labyrinth Guardian 322 | 1 Token: Heart-Piercer Manticore 323 | 3 Token: Tah-Crop Skirmisher 324 | 1 Token: Vizier of Many Faces 325 | 1 Token: Honored Hydra 326 | 2 Token: Glyph Keeper 327 | -------------------------------------------------------------------------------- /deck.db: -------------------------------------------------------------------------------- 1 | {"goldfish": {"deckURL": []}, "mtgtop8": {"deckURL": []}, "precon": {"Eldritch Moon - Unlikely Alliances": {"deckCount": 60, "deckCards": {"Blessed Alliance": 2, "Gavony Unhallowed": 1, "Spectral Reserves": 2, "Plains": 13, "Vessel of Ephemera": 2, "Lunarch Mantle": 2, "Morkrut Necropod": 1, "Unruly Mob": 2, "Ruthless Disposal": 2, "Sanitarium Skeleton": 1, "Borrowed Grace": 1, "Bound by Moonsilver": 1, "Campaign of Vengeance": 2, "Skirsdag Supplicant": 2, "Angelic Purge": 2, "Desperate Sentry": 3, "Swamp": 12, "Haunted Dead": 2, "Borrowed Malevolence": 1, "Providence": 1, "Sanctifier of Souls": 1, "Nearheath Chaplain": 1, "Repel the Abominable": 1, "Emissary of the Sleepless": 1, "Vampire Noble": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/eldritch-moon-intro-pack-decklists-2016-07-13#unlikely_alliances"}, "Shadows Over Innistrad - Horrific Visions": {"deckCount": 60, "deckCards": {"Merciless Resolve": 1, "Might Beyond Reason": 1, "Vessel of Nascency": 2, "Wicker Witch": 2, "Groundskeeper": 2, "Crow of Dark Tidings": 1, "Rabid Bite": 1, "Forest": 12, "Liliana's Indignation": 1, "Tooth Collector": 2, "Fork in the Road": 1, "Morkrut Necropod": 1, "Loam Dryad": 1, "Hound of the Farbogs": 1, "Warped Landscape": 1, "Obsessive Skinner": 2, "Wild-Field Scarecrow": 1, "Moldgraf Scavenger": 3, "Throttle": 1, "Stallion of Ashmouth": 2, "Explosive Apparatus": 2, "Kessig Dire Swine": 1, "Foul Orchard": 1, "Soul Swallower": 1, "Swamp": 11, "Crawling Sensation": 1, "Inexorable Blob": 1, "Ghoulsteed": 1, "Dead Weight": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/shadows-over-innistrad-intro-pack-decklists-2016-03-30#horrific_visions"}, "Kaladesh - Nissa, Nature's Artisan": {"deckCount": 60, "deckCards": {"Thriving Turtle": 2, "Aethersquall Ancient": 1, "Janjeet Sentry": 2, "Guardian of the Great Conduit": 3, "Longtusk Cub": 2, "Attune with Aether": 3, "Riparian Tiger": 1, "Servant of the Conduit": 2, "Terrain Elemental": 4, "Island": 8, "Long-Finned Skywhale": 2, "Empyreal Voyager": 2, "Nissa, Nature's Artisan": 1, "Sage of Shaila's Claim": 1, "Arborback Stomper": 2, "Malfunction": 2, "Woodland Stream": 4, "Bristling Hydra": 1, "Thriving Rhino": 2, "Verdant Crescendo": 2, "Appetite for the Unnatural": 2, "Forest": 11}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/planeswalker-decklists-2016-09-21#nissa_natures_artisan"}, "Eldritch Moon - Shallow Graves": {"deckCount": 60, "deckCards": {"Reckless Scholar": 1, "Compelling Deterrence": 2, "Cryptbreaker": 1, "Crow of Dark Tidings": 1, "Liliana's Indignation": 1, "Seagraf Skaab": 2, "Murder": 1, "Liliana's Elite": 2, "Cemetery Recruitment": 3, "Graf Harvest": 2, "Wailing Ghoul": 1, "Spontaneous Mutation": 2, "Throttle": 1, "Island": 12, "Succumb to Temptation": 1, "Stitchwing Skaab": 1, "Laboratory Brute": 1, "Swamp": 13, "Noosegraf Mob": 1, "Tattered Haunter": 2, "Lamplighter of Selhoff": 1, "Ghostly Wings": 1, "Advanced Stitchwing": 2, "Stitched Mangler": 2, "Boon of Emrakul": 1, "Exultant Cultist": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/eldritch-moon-intro-pack-decklists-2016-07-13#Shallow_graves"}, "Aether REvolt - Tezzeret, Master of Metal": {"deckCount": 60, "deckCards": {"Treasure Keeper": 1, "Barricade Breaker": 1, "Reverse Engineer": 1, "Foundry Assembler": 1, "Tezzeret's Betrayal": 2, "Bastion Inventor": 2, "Quicksmith Spy": 1, "Essence Extraction": 1, "Augmenting Automaton": 2, "Pendulum of Patterns": 4, "Tezzeret's Simulacrum": 3, "Implement of Examination": 2, "Ornithopter": 1, "Dhund Operative": 2, "Tezzeret's Touch": 2, "Ironclad Revolutionary": 1, "Island": 11, "Submerged Boneyard": 4, "Fen Hauler": 1, "Swamp": 10, "Dukhara Peafowl": 1, "Tezzeret, Master of Metal": 1, "Wind-Kin Raiders": 2, "Merchant's Dockhand": 1, "Universal Solvent": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/aether-revolt-planeswalker-deck-lists-2017-01-11#tezzeret_master_of_metal"}, "Eldritch Moon - Untamed Wild": {"deckCount": 60, "deckCards": {"Mountain": 13, "Prey Upon": 2, "Primal Druid": 2, "Magmatic Chasm": 1, "Silverfur Partisan": 1, "Forest": 12, "Clear Shot": 1, "Bold Impaler": 2, "Quilled Wolf": 2, "Brazen Wolves": 3, "Rush of Adrenaline": 1, "Somberwald Stag": 2, "Uncaged Fury": 1, "Noose Constrictor": 1, "Furyblade Vampire": 2, "Howlpack Wolf": 2, "Lightning Axe": 1, "Clip Wings": 1, "Blood Mist": 2, "Mad Prophet": 1, "Assembled Alphas": 1, "Wolfkin Bond": 3, "Spreading Flames": 2, "Voldaren Duelist": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/eldritch-moon-intro-pack-decklists-2016-07-13#untamed_wild"}, "Eldritch Moon - Dangerous Knowledge": {"deckCount": 60, "deckCards": {"Mountain": 12, "Shreds of Sanity": 2, "Bedlam Reveler": 1, "Make Mischief": 2, "Take Inventory": 3, "Pore Over the Pages": 1, "Weaver of Lightning": 2, "Sanguinary Mage": 1, "Drag Under": 1, "Silburlind Snapper": 1, "Dance with Devils": 1, "Niblis of Dusk": 1, "Pyre Hound": 1, "Island": 13, "Pieces of the Puzzle": 1, "Galvanic Bombardment": 3, "Incendiary Flow": 1, "Convolute": 1, "Geistblast": 2, "Niblis of Frost": 1, "Thermo-Alchemist": 2, "Mercurial Geists": 2, "Ingenious Skaab": 2, "Reduce to Ashes": 1, "Rise from the Tides": 1, "Turn Aside": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/eldritch-moon-intro-pack-decklists-2016-07-13#dangerous_knowledge"}, "Shadows Over Innistrad - Ghostly Tide": {"deckCount": 60, "deckCards": {"Reckless Scholar": 1, "Apothecary Geist": 2, "Silverstrike": 1, "Sleep Paralysis": 1, "Plains": 13, "Vessel of Ephemera": 2, "Dauntless Cathar": 2, "Rattlechains": 1, "Seagraf Skaab": 2, "Nearheath Chaplain": 2, "Pore Over the Pages": 1, "Catalog": 1, "Stormrider Spirit": 2, "Spectral Shepherd": 2, "Bound by Moonsilver": 2, "Not Forgotten": 2, "Niblis of Dusk": 2, "Puncturing Light": 2, "Island": 12, "Deny Existence": 1, "Drogskol Cavalry": 1, "Essence Flux": 1, "Chaplain's Blessing": 1, "Silent Observer": 1, "Emissary of the Sleepless": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/shadows-over-innistrad-intro-pack-decklists-2016-03-30#ghostly_tide"}, "Shadows Over Innistrad - Unearthed Secrets": {"deckCount": 60, "deckCards": {"Jace's Scrutiny": 1, "Drownyard Explorers": 2, "Forest": 12, "Watcher in the Web": 1, "Thornhide Wolves": 2, "Briarbridge Patrol": 2, "Quilled Wolf": 2, "Gone Missing": 2, "Ongoing Investigation": 2, "Tireless Tracker": 1, "Confront the Unknown": 3, "Island": 13, "Root Out": 1, "Byway Courier": 2, "Nephalia Moondrakes": 1, "Ghostly Wings": 1, "Graf Mole": 2, "Stitched Mangler": 2, "Erdwal Illuminator": 2, "Magnifying Glass": 1, "Aim High": 1, "Ulvenwald Mysteries": 1, "Press for Answers": 1, "Gloomwidow": 1, "Pack Guardian": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/shadows-over-innistrad-intro-pack-decklists-2016-03-30#unearthed_secrets"}, "Shadows Over Innistrad - Angelic Fury": {"deckCount": 60, "deckCards": {"Rush of Adrenaline": 2, "Mountain": 13, "Inner Struggle": 2, "Flameblade Angel": 1, "Magmatic Chasm": 1, "Inspiring Captain": 2, "Ember-Eye Wolf": 2, "Devils' Playground": 1, "Unruly Mob": 1, "Murderer's Axe": 1, "Devilthorn Fox": 3, "Cathar's Companion": 3, "Stern Constable": 2, "Runaway Carriage": 1, "Gryff's Boon": 1, "Nahiri's Machinations": 2, "Angelic Purge": 1, "Dance with Devils": 2, "Howlpack Wolf": 2, "Lightning Axe": 2, "Plains": 12, "Dissension in the Ranks": 1, "Pyre Hound": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/shadows-over-innistrad-intro-pack-decklists-2016-03-30#angelic_fury"}, "Eldritch Moon - Weapons and Wards": {"deckCount": 60, "deckCards": {"Equestrian Skill": 1, "Courageous Outrider": 1, "Militant Inquisitor": 1, "Inspiring Captain": 1, "Plains": 13, "Steadfast Cathar": 1, "Forest": 12, "Hope Against Hope": 1, "Fiend Binder": 2, "Ironclad Slayer": 2, "Open the Armory": 1, "Intrepid Provisioner": 2, "Sigardian Priest": 1, "Hamlet Captain": 2, "Crossroads Consecrator": 2, "Gryff's Boon": 1, "Strength of Arms": 2, "Ulvenwald Observer": 1, "Cultist's Staff": 2, "Choking Restraints": 1, "Subjugator Angel": 2, "Slayer's Cleaver": 2, "Thraben Standard Bearer": 2, "Faith Unbroken": 2, "Emrakul's Evangel": 1, "Ironwright's Cleansing": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/eldritch-moon-intro-pack-decklists-2016-07-13#weapons_and_wards"}, "Aether Revolt - Ajani, Valiant Protector": {"deckCount": 60, "deckCards": {"Airdrop Aeronauts": 1, "Plains": 9, "Unbridled Growth": 2, "Forest": 9, "Natural Obsolescence": 1, "Verdant Automaton": 2, "Audacious Infiltrator": 3, "Prey Upon": 1, "Lifecraft Cavalry": 2, "Aid from the Cowl": 1, "Renegade Map": 3, "Ghirapur Guide": 1, "Silkweaver Elite": 2, "Ridgescale Tusker": 1, "Narnam Renegade": 2, "Daredevil Dragster": 1, "Solemn Recruit": 1, "Armorcraft Judge": 2, "Inspiring Roar": 4, "Deadeye Harpooner": 1, "Tranquil Expanse": 4, "Ajani's Comrade": 3, "Engineered Might": 1, "Ajani's Aid": 2, "Ajani, Valiant Protector": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/aether-revolt-planeswalker-deck-lists-2017-01-11#ajani_valiant_protector"}, "Amonkhet - Gideon, Martial Paragon": {"deckCount": 60, "deckCards": {"Gideon's Resolve": 2, "Glory-Bound Initiate": 1, "Gideon, Martial Paragon": 1, "Mountain": 10, "Electrify": 2, "Devoted Crop-Mate": 1, "Plains": 10, "Stone Quarry": 4, "Hazoret's Favor": 1, "Sparring Mummy": 2, "Hyena Pack": 2, "Graceful Cat": 4, "Ahn-Crop Crasher": 3, "Companion of the Trials": 3, "Trial of Zeal": 3, "Cartouche of Zeal": 2, "Tah-Crop Elite": 1, "Nef-Crop Entangler": 1, "Honored Crop-Captain": 2, "Pathmaker Initiate": 1, "Gust Walker": 3, "Impeccable Timing": 1}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/amonkhet-planeswalker-deck-lists-2017-04-19#gideon_martial_paragon"}, "Amonkhet - Liliana, Death Wielder": {"deckCount": 60, "deckCards": {"Gift of Paradise": 1, "Splendid Agony": 2, "Gravedigger": 2, "Desiccated Naga": 3, "Luxa River Shrine": 1, "Decimator Beetle": 2, "Forest": 9, "Tattered Mummy": 4, "Dune Beetle": 2, "Baleful Ammit": 2, "Trial of Ambition": 2, "Cartouche of Ambition": 2, "Grasping Dunes": 1, "Festering Mummy": 1, "Crocodile of the Crossing": 1, "Oracle's Vault": 1, "Swamp": 11, "Channeler Initiate": 1, "Foul Orchard": 4, "Giant Spider": 2, "Liliana, Death Wielder": 1, "Cartouche of Strength": 2, "Edifice of Authority": 1, "Liliana's Influence": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/news/amonkhet-planeswalker-deck-lists-2017-04-19#liliana_death_wielder"}, "Shadows Over Innistrad - Vampiric Thirst": {"deckCount": 60, "deckCards": {"Bloodmad Vampire": 2, "Mountain": 12, "Stromkirk Mentor": 2, "Ravenous Bloodseeker": 2, "Markov Dreadknight": 1, "Sanguinary Mage": 2, "Sinister Concoction": 1, "Alms of the Vein": 2, "Fiery Temper": 2, "Burn from Within": 1, "Malevolent Whispers": 2, "Creeping Dread": 1, "Macabre Waltz": 1, "Murderous Compulsion": 1, "Senseless Rage": 1, "Twins of Maurer Estate": 2, "Indulgent Aristocrat": 2, "Swamp": 13, "Incorrigible Youths": 2, "Mad Prophet": 1, "Tormenting Voice": 2, "Olivia's Bloodsworn": 1, "Voldaren Duelist": 2, "Vampire Noble": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/shadows-over-innistrad-intro-pack-decklists-2016-03-30#vampiric_thirst"}, "Kaladesh - Chandra, Pyrogenius": {"deckCount": 60, "deckCards": {"Mountain": 11, "Liberating Combustion": 2, "Snare Thopter": 1, "Fleetwheel Cruiser": 1, "Plains": 10, "Fateful Showdown": 1, "Weldfast Monitor": 1, "Stone Quarry": 4, "Aerial Responder": 2, "Sky Skiff": 2, "Cathartic Reunion": 1, "Built to Last": 2, "Brazen Scourge": 1, "Renegade Freighter": 2, "Chandra, Pyrogenius": 1, "Renegade Firebrand": 3, "Ovalchase Dragster": 1, "Speedway Fanatic": 1, "Flame Lash": 4, "Spireside Infiltrator": 2, "Skyswirl Harrier": 1, "Veteran Motorist": 2, "Bomat Bazaar Barge": 1, "Gearshift Ace": 1, "Trusty Companion": 2}, "deckURL": "http://magic.wizards.com/en/articles/archive/feature/planeswalker-decklists-2016-09-21#chandra_pyrogenius"}}, "deckbox": {}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mtgdeckhunter 2 | Uses your Magic the Gathering (MtG) library of cards to search within decks downloaded from [Deckbox](https://deckbox.org/), [MtG Goldfish](https://www.mtggoldfish.com/), and [MtG Top 8](http://mtgtop8.com/index) for ones you can build. 3 | 4 | Create your library of cards on the Deckbox inventory page and enter your ID when you run the script for the first time. My collection is listed [here](https://deckbox.org/sets/1721702) and in file "card.db" that I've included for testing purposes. Additionally, you can download a "deck.db" file I've uploaded with contains 16 official pre-constructed decks. You're on your own for actually downloading the decks from the sites as it takes long time and fairly temporal in context (current format/sets). 5 | 6 | Blog post - [02JUL2017 - MtG Deck Hunting](http://ropgadget.com/posts/mtg_deck_hunting.html) 7 | 8 | I've included a number of features to make searching the downloaded decks slightly easier and I'll cover these flags towards the end. 9 | 10 | Each deck will have a corresponding URL that links back to the source of the deck where you can get additional information on the respective site. This is purely meant as a quick way to hone in on potential decks you can build with your current library and is not meant to suggest any particular deck or style. 11 | 12 | *NOTE: I will not be actively maintaining this script and just created it to suit a specific need of mine, which turned out to be rather fruitless, so you're on your own for fixes and updates. 13 | 14 | Before getting into it, below is the usage for the script. 15 | 16 | ``` 17 | usage: mtgdeckhunter.py [-h] [-u] [-c ] [-n] [-v] [-f FILTER] 18 | [-s ,] 19 | 20 | Find playable decks from DeckBox, MTGTOP8, MTGGoldFish, and Official PreCons 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -u, --update Perform full deck update. 25 | -c , --commonality 26 | Match above specified percentage, default is 100%. 27 | -n, --noupdate Perform just deck matches on existing data. 28 | -v, --verbose Print additional meta data. Can take a LONG time to 29 | process. 30 | -f FILTER, --filter FILTER 31 | Comma separated list of words to filter from matched 32 | decks. 33 | -s ,, --size , 34 | Limit returned decks to a certain deck size, default 35 | is 40-80. 36 | ``` 37 | 38 | Running the script for the first time will look similar to the below output (truncated for space). 39 | 40 | ``` 41 | $ python mtgdeckhunter.py 42 | 43 | __ __, _ __,__, __, _ 44 | / \_( \ /_\ //~_) |//~' _ (_) _ 45 | / % : '<_v_\\\~/|_(\\__, _ (_) (_) 46 | / /\ C\ \ ~ ~~ ~ ~ ~~~~~ _ _ 47 | / / \ \ \ \ The Gathering (_) (_) 48 | ~~~ ~~` ~~` 49 | 50 | [!] Failed to load/find existing 'card.db'. Creating a new one. 51 | 52 | [!] Provide DeckBox Inventory number: 1721702 53 | 54 | [+] Loaded 16 existing deck data from 'deck.db' 55 | 56 | goldfish - 0 Decks 57 | mtgtop8 - 0 Decks 58 | precon - 16 Decks 59 | deckbox - 0 Decks 60 | 61 | [+] Checking sites for new decks... 62 | 63 | ############### 64 | # MtGGoldfish # 65 | ############### 66 | 67 | [!] Press CTRL+C to stop updating a section at any time 68 | 69 | [#] Downloading standard format new decks... 70 | [-] Adding Deck 688877 - Temur Energy Deck 71 | [-] Adding Deck 688879 - Mardu Vehicles Deck 72 | [-] Adding Deck 679204 - GB Energy Deck 73 | 74 | [-] Adding Deck 487606 - GU Emerge — Kaladesh Update Deck 75 | [-] Adding Deck 487411 - GW Bloodbriar — Kaladesh Update Deck 76 | [-] Adding Deck 487602 - Evolutionary Dredge — Kaladesh Update Deck 77 | [#] Downloading modern format new decks... 78 | [-] Adding Deck 686965 - Grixis Death's Shadow Deck 79 | [-] Adding Deck 680198 - Eldrazi Tron Deck 80 | 81 | [-] Adding Deck 677706 - Mardu Burn Deck 82 | [-] Adding Deck 689488 - UR Deck 83 | [-] Adding Deck 675310 - BR Deck 84 | 85 | [-] Adding Deck 673466 - WR Deck 86 | [-] Adding Deck 686989 - Jund Ramp Deck 87 | [-] Adding Deck 686967 - Mono-Black Devotion Deck 88 | [*] Saving data... 89 | [*] Saving backup... 90 | [*] Saves complete. Resuming... 91 | [-] Adding Deck 686973 - R/G Through the Breach Deck 92 | [-] Adding Deck 686974 - Four-Color Saheeli Deck 93 | 94 | [#] Downloading pauper format new decks... 95 | 96 | [#] Downloading legacy format new decks... 97 | 98 | [#] Downloading vintage format new decks... 99 | 100 | [#] Downloading frontier format new decks... 101 | 102 | [#] Downloading commander_1v1 format new decks... 103 | 104 | [#] Downloading commander format new decks... 105 | 106 | [#] Downloading tiny_leaders format new decks... 107 | [-] Adding Deck 660642 - Shu Yun, the Silent Tempest Deck 108 | [-] Adding Deck 686100 - Alesha, Who Smiles at Death Deck 109 | 110 | ########### 111 | # Deckbox # 112 | ########### 113 | 114 | [!] Press CTRL+C to stop updating at any time 115 | 116 | [+] Performing incremental deck update 117 | 118 | [-] Adding Deck 1742911 - Copy of HOU Mono U Fray 119 | [-] Adding Deck 1742910 - insect tribal v2 devour 120 | [-] Adding Deck 1742909 - Death and Taxes Brian Coval 121 | 122 | [-] Adding Deck 1742864 - nekuar 123 | [-] Adding Deck 1742863 - M - Grixis Death\u0026#39;s Shadow 1 124 | [-] Adding Deck 1742862 - BoslanKek 125 | 126 | ########### 127 | # MtGTop8 # 128 | ########### 129 | 130 | [!] Press CTRL+C to stop updating a section at any time 131 | 132 | [+] Performing incremental deck update 133 | 134 | [#] Downloading decks from event 16039 - Environmental Master - Amonkhet 135 | [-] Adding deck 298748 - Emerge 136 | [-] Adding deck 298749 - Golgari Constrictor 137 | [-] Adding deck 298751 - Mardu Vehicle 138 | [-] Adding deck 298750 - Temur Energy 139 | [-] Adding deck 298754 - Emerge 140 | [-] Adding deck 298747 - Golgari Constrictor 141 | [-] Adding deck 298752 - Golgari Constrictor 142 | [-] Adding deck 298753 - Jund 143 | [#] Downloading decks from event 16038 - MTGO Pauper League 144 | [-] Adding deck 298743 - Deep Hours Aggro 145 | 146 | [#] Downloading decks from event 15778 - MTGO Competitive Standard League 147 | [-] Adding deck 296787 - Mono Black Zombies 148 | [-] Adding deck 296788 - Jund 149 | [-] Adding deck 296789 - Red Deck Wins 150 | [-] Adding deck 296786 - Golgari Constrictor 151 | [-] Adding deck 296785 - Golgari Constrictor 152 | [-] Adding deck 296784 - Jund 153 | [-] Adding deck 296783 - Marvelous Energy 154 | 155 | [+] Pruned 24 decks due to size restrictions. 156 | [*] Saving data... 157 | [*] Saving backup... 158 | [*] Saves complete. Resuming downloads... 159 | 160 | [+] Searching for playable decks with 100% match 161 | 162 | [-] Deck 298009 - NoName Sealed (40 cards - 100%) 163 | http://mtgtop8.com/event?d=298009 164 | 165 | [-] Deck 298008 - NoName Sealed (40 cards - 100%) 166 | http://mtgtop8.com/event?d=298008 167 | 168 | [-] Deck 298579 - NoName Sealed (61 cards - 100%) 169 | http://mtgtop8.com/event?d=298579 170 | 171 | [-] Deck 296929 - NoName Sealed (61 cards - 100%) 172 | http://mtgtop8.com/event?d=296929 173 | ``` 174 | 175 | Basically it will iterate over each site and try to download the latest decks from when you last ran the script. For MtG Top 8 and Deckbox you can use the "-u" flag for full update which will cause it to just keep downloading decks from those sites. For MtG Top 8 it's feasible to download all of the decks in a night but for Deckbox, which is primarily creative deck ideas versus actual decks used like on Top 8, you'll be downloading until the end of enternity and most will be garbage. Alternatively, you can use the "-n" flag to not do any updates and just focus on the deck hunting. 176 | 177 | Alright, so let's say I don't care about sealed draft decks, test decks, or X decks because they are most likely not theorycrafted enough and people were just dealt what they were dealt. You can use the "-f" flag to filter out certain decks. 178 | 179 | ``` 180 | $ python mtgdeckhunter.py -n -f sealed,draft,test 181 | 182 | __ __, _ __,__, __, _ 183 | / \_( \ /_\ //~_) |//~' _ (_) _ 184 | / % : '<_v_\\\~/|_(\\__, _ (_) (_) 185 | / /\ C\ \ ~ ~~ ~ ~ ~~~~~ _ _ 186 | / / \ \ \ \ The Gathering (_) (_) 187 | ~~~ ~~` ~~` 188 | 189 | [+] Loaded existing card data from 'card.db' 190 | [+] Loaded 94593 existing deck data from 'deck.db' 191 | 192 | goldfish - 1294 Decks 193 | mtgtop8 - 78463 Decks 194 | precon - 16 Decks 195 | deckbox - 14820 Decks 196 | 197 | [+] Searching for playable decks with 100% match 198 | 199 | [-] Deck 1703378 - Amonkhet League (42 cards - 100%) 200 | https://deckbox.org/sets/1703378 201 | 202 | [-] Deck 1740542 - Bant Control AKHx6 (40 cards - 100%) 203 | https://deckbox.org/sets/1740542 204 | 205 | [-] Deck 1742059 - Missing AKH C (62 cards - 100%) 206 | https://deckbox.org/sets/1742059 207 | 208 | [-] Deck 1741945 - 2017/06/30 (40 cards - 100%) 209 | https://deckbox.org/sets/1741945 210 | 211 | [-] Deck 1697612 - Day 2 Chaff (52 cards - 100%) 212 | https://deckbox.org/sets/1697612 213 | ``` 214 | 215 | Cool, but what if I want to search for decks where I have most of the cards and only decks between 60-70 cards? You can use the "-c" commonality flag to specify what percentage the deck needs to match and the "-s" flag to specify the low/high numbers for the deck size. 216 | 217 | ``` 218 | $ python mtgdeckhunter.py -n -f sealed,draft,test -c 80 -s 60-70 219 | 220 | __ __, _ __,__, __, _ 221 | / \_( \ /_\ //~_) |//~' _ (_) _ 222 | / % : '<_v_\\\~/|_(\\__, _ (_) (_) 223 | / /\ C\ \ ~ ~~ ~ ~ ~~~~~ _ _ 224 | / / \ \ \ \ The Gathering (_) (_) 225 | ~~~ ~~` ~~` 226 | 227 | [+] Loaded existing card data from 'card.db' 228 | [+] Loaded 94593 existing deck data from 'deck.db' 229 | 230 | goldfish - 1294 Decks 231 | mtgtop8 - 78463 Decks 232 | precon - 16 Decks 233 | deckbox - 14820 Decks 234 | 235 | [+] Searching for playable decks with 80% match 236 | 237 | [-] Deck 223629 - Unknown Scapeshift (60 cards - 80%) 238 | http://mtgtop8.com/event?d=223629 239 | 240 | [-] Official - Amonkhet - Gideon, Martial Paragon (60 cards - 81%) 241 | http://magic.wizards.com/en/articles/archive/news/amonkhet-planeswalker-deck-lists-2017-04-19#gideon_martial_paragon 242 | 243 | [-] Deck 1741853 - GR (60 cards - 91%) 244 | https://deckbox.org/sets/1741853 245 | 246 | [-] Deck 1742059 - Missing AKH C (62 cards - 100%) 247 | https://deckbox.org/sets/1742059 248 | 249 | [-] Deck 1740177 - Zombies (67 cards - 86%) 250 | https://deckbox.org/sets/1740177 251 | 252 | [-] Deck 1740194 - Embalm (Standard) (60 cards - 95%) 253 | https://deckbox.org/sets/1740194 254 | 255 | [-] Deck 1692181 - Amonkhet League SPEC (63 cards - 93%) 256 | https://deckbox.org/sets/1692181 257 | 258 | [-] Deck 1739950 - 20170621 - BG - -1/-1 - AKH (60 cards - 90%) 259 | https://deckbox.org/sets/1739950 260 | 261 | [-] Deck 1707170 - Deck Deck1 (61 cards - 96%) 262 | https://deckbox.org/sets/1707170 263 | 264 | [-] Deck 1740700 - Gideon (60 cards - 90%) 265 | https://deckbox.org/sets/1740700 266 | 267 | [-] Deck 1740712 - Copy of Zombie (Tournament 1) (60 cards - 88%) 268 | https://deckbox.org/sets/1740712 269 | 270 | [-] Deck 1740325 - Decimator Beetle \u0026amp; Hepatra (60 cards - 90%) 271 | https://deckbox.org/sets/1740325 272 | 273 | [-] Deck 1729593 - Mazo Eche (70 cards - 87%) 274 | https://deckbox.org/sets/1729593 275 | 276 | [-] Deck 1710712 - Deck 1 (60 cards - 88%) 277 | https://deckbox.org/sets/1710712 278 | 279 | [-] Deck 1742528 - Blue \u0026amp; White Tokens (63 cards - 92%) 280 | https://deckbox.org/sets/1742528 281 | 282 | [-] Deck 1742026 - Emily Amonkhet 1 red (60 cards - 85%) 283 | https://deckbox.org/sets/1742026 284 | 285 | [-] Deck 1740111 - w - trial of solidarity (60 cards - 81%) 286 | https://deckbox.org/sets/1740111 287 | 288 | [-] Deck 1742735 - White Zombie/Token (60 cards - 85%) 289 | https://deckbox.org/sets/1742735 290 | 291 | [-] Deck 1741227 - Attempt at Control (60 cards - 93%) 292 | https://deckbox.org/sets/1741227 293 | 294 | [-] Deck 1740340 - Crocodiles (63 cards - 87%) 295 | https://deckbox.org/sets/1740340 296 | 297 | [-] Deck 1742757 - Gideon Preconstructed Amonkhet (60 cards - 81%) 298 | https://deckbox.org/sets/1742757 299 | ``` 300 | 301 | Now I can check out the titles, see if anything stands out and hit the respective URL's to read more about the deck. 302 | 303 | The "-v" verbose flag will give you some additional information about what you're missing, if that's interesting for knowing whether you might have a quick substitute. 304 | 305 | ``` 306 | $ python mtgdeckhunter.py -n -f sealed,draft,test -c 95 -v 307 | 308 | __ __, _ __,__, __, _ 309 | / \_( \ /_\ //~_) |//~' _ (_) _ 310 | / % : '<_v_\\\~/|_(\\__, _ (_) (_) 311 | / /\ C\ \ ~ ~~ ~ ~ ~~~~~ _ _ 312 | / / \ \ \ \ The Gathering (_) (_) 313 | ~~~ ~~` ~~` 314 | 315 | [+] Loaded existing card data from 'card.db' 316 | 317 | [-] Total cards in your collection - 1094 318 | [-] Unique cards in your collection - 321 319 | [-] Most common non-basic land card - Evolving Wilds 320 | [-] Least common non-basic land card - Open into Wonder 321 | 322 | [+] Loaded 94593 existing deck data from 'deck.db' 323 | 324 | goldfish - 1294 Decks 325 | mtgtop8 - 78463 Decks 326 | precon - 16 Decks 327 | deckbox - 14820 Decks 328 | 329 | [+] Searching for playable decks with 95% match 330 | 331 | [-] Deck 298214 - TBD (45 cards - 97%) 332 | http://mtgtop8.com/event?d=298214 333 | 334 | [!] 97% (44 /45 ) - Missing Cards: 1 - Bontu's Monument 335 | 336 | [-] Deck 298211 - TBD (45 cards - 95%) 337 | http://mtgtop8.com/event?d=298211 338 | 339 | [!] 95% (43 /45 ) - Missing Cards: 1 - Ruthless Sniper, 1 - Mouth // Feed 340 | 341 | [-] Deck 1733716 - TSMTG Summer 2017 (45 cards - 95%) 342 | https://deckbox.org/sets/1733716 343 | 344 | [!] 95% (43 /45 ) - Missing Cards: 1 - Gravedigger, 1 - Regal Caracal 345 | 346 | [-] Deck 1740194 - Embalm (Standard) (60 cards - 95%) 347 | https://deckbox.org/sets/1740194 348 | 349 | [!] 95% (57 /60 ) - Missing Cards: 2 - Open into Wonder, 1 - Temmet, Vizier of Naktamun 350 | 351 | [-] Deck 1729729 - Amonkhet Pool (55 cards - 96%) 352 | https://deckbox.org/sets/1729729 353 | 354 | [!] 96% (53 /55 ) - Missing Cards: 1 - Sweltering Suns, 1 - Grasping Dunes 355 | 356 | [-] Deck 1739960 - W/R (50 cards - 96%) 357 | https://deckbox.org/sets/1739960 358 | 359 | [!] 96% (48 /50 ) - Missing Cards: 1 - Standing Troops, 1 - Wrangle 360 | 361 | [-] Deck 1707170 - Deck Deck1 (61 cards - 96%) 362 | https://deckbox.org/sets/1707170 363 | 364 | [!] 96% (59 /61 ) - Missing Cards: 1 - Throne of the God-Pharaoh, 1 - Regal Caracal 365 | 366 | [-] Deck 1697612 - Day 2 Chaff (52 cards - 100%) 367 | https://deckbox.org/sets/1697612 368 | 369 | [!] 100% (52 /52 ) - Missing Cards: 370 | 371 | [-] Deck 1739498 - Amonkhet- Gideon \u0026amp; Friends (40 cards - 97%) 372 | https://deckbox.org/sets/1739498 373 | 374 | [!] 97% (39 /40 ) - Missing Cards: 1 - Gideon of the Trials 375 | 376 | [-] Deck 1703378 - Amonkhet League (42 cards - 100%) 377 | https://deckbox.org/sets/1703378 378 | 379 | [!] 100% (42 /42 ) - Missing Cards: 380 | 381 | [-] Deck 1701266 - Work League (59 cards - 98%) 382 | https://deckbox.org/sets/1701266 383 | 384 | [!] 98% (58 /59 ) - Missing Cards: 1 - Sweltering Suns 385 | 386 | [-] Deck 1742059 - Missing AKH C (62 cards - 100%) 387 | https://deckbox.org/sets/1742059 388 | 389 | [!] 100% (62 /62 ) - Missing Cards: 390 | 391 | [-] Deck 1741945 - 2017/06/30 (40 cards - 100%) 392 | https://deckbox.org/sets/1741945 393 | 394 | [!] 100% (40 /40 ) - Missing Cards: 395 | 396 | [-] Deck 1741948 - 3 X AKH 6/30/17 (40 cards - 95%) 397 | https://deckbox.org/sets/1741948 398 | 399 | [!] 95% (38 /40 ) - Missing Cards: 1 - Sweltering Suns, 1 - Regal Caracal 400 | 401 | [-] Deck 1740542 - Bant Control AKHx6 (40 cards - 100%) 402 | https://deckbox.org/sets/1740542 403 | 404 | [!] 100% (40 /40 ) - Missing Cards: 405 | 406 | [-] Deck 1690805 - Magic League 1 all cards (46 cards - 97%) 407 | https://deckbox.org/sets/1690805 408 | 409 | [!] 97% (45 /46 ) - Missing Cards: 1 - Hippo 410 | 411 | [-] Deck 1742238 - White-Blue 7-1-17 (40 cards - 97%) 412 | https://deckbox.org/sets/1742238 413 | 414 | [!] 97% (39 /40 ) - Missing Cards: 1 - Galestrike 415 | ``` 416 | 417 | Enjoy. 418 | -------------------------------------------------------------------------------- /mtgdeckhunter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import urllib2, re, argparse, json, operator, random, io, sys 3 | 4 | __author__ = "Jeff White [karttoon] @noottrak" 5 | __email__ = "karttoon@gmail.com" 6 | _version_ = "1.0.0" 7 | __date__ = "02JUL017" 8 | 9 | # 10 | # JSON struct 11 | # 12 | # deckDB { 13 | # 14 | # "deckbox": { 15 | # deckID: { 16 | # "deckCount": 1, 17 | # "deckTitle": deckName, 18 | # "deckCards": { 19 | # "cardName": 1, 20 | # 21 | # "goldfish": { 22 | # deckID: { 23 | # "deckCount": 1, 24 | # "deckTitle": deckName, 25 | # "deckCards": { 26 | # "cardName": 1, 27 | # 28 | # "mtgtop8": { 29 | # "deckURL": webURL, 30 | # deckID: { 31 | # "deckCount": 1, 32 | # "deckTitle": deckName, 33 | # "deckCards": { 34 | # "cardName": 1, 35 | # "precon": { 36 | # deckID: { 37 | # "deckURL": webURL, 38 | # "deckCount": 1, 39 | # "deckTitle": deckName, 40 | # "deckCards": { 41 | # "cardName": 1, 42 | # 43 | # 44 | 45 | def loadCard(args): 46 | 47 | try: 48 | cardDB = json.load(open("card.db")) 49 | print "[+] Loaded existing card data from 'card.db'" 50 | 51 | except: 52 | print "[!] Failed to load/find existing 'card.db'. Creating a new one." 53 | cardDB = createCard(cardDB={}) 54 | 55 | if args.verbose: 56 | cardStats(cardDB) 57 | 58 | return cardDB 59 | 60 | def createCard(cardDB): 61 | 62 | playerNum = raw_input("\n [!] Provide DeckBox Inventory number: ") 63 | 64 | print 65 | 66 | # Parse card collection 67 | response = urllib2.urlopen("https://deckbox.org/sets/%s/export?s=&f=&o=" % playerNum) 68 | html = response.read() 69 | 70 | html = html.replace("\n", "") 71 | html = re.search("\.+\<\/body\>", html).group() 72 | html = html.replace("", "").replace("", "") 73 | html = html.replace("
", "\n") 74 | html = html.strip().split("\n") 75 | 76 | for line in html: 77 | 78 | cardCount = line.split(" ")[0] 79 | cardName = " ".join(line.split(" ")[1:]) 80 | 81 | if cardName not in cardDB.values(): 82 | cardDB[cardName] = int(cardCount) 83 | else: 84 | cardDB[cardName] += int(cardCount) 85 | 86 | saveCard(cardDB) 87 | 88 | return cardDB 89 | 90 | def loadDeck(args): 91 | 92 | try: 93 | deckDB = json.load(open("deck.db")) 94 | deckCount = 0 95 | 96 | for site in deckDB: 97 | deckCount += len(deckDB[site]) 98 | 99 | print "[+] Loaded %s existing deck data from 'deck.db'" % (deckCount - 2) 100 | 101 | except: 102 | deckDB = {"deckbox" :{}, 103 | "goldfish" :{"deckURL":[]}, 104 | "mtgtop8" :{"deckURL":[]}, 105 | "precon" :{} 106 | } 107 | 108 | print "[!] Failed to load/find existing 'deck.db'. Creating a new one." 109 | 110 | # Takes VERY long as deckDB grows - leave commented unless you really care about it 111 | #if args.verbose: 112 | # deckStats(deckDB) 113 | 114 | # Print deck count per site 115 | print 116 | 117 | for site in deckDB: 118 | 119 | if site == "goldfish" or site == "mtgtop8": 120 | 121 | deckLen = len(deckDB[site]) - 1 122 | 123 | else: 124 | 125 | deckLen = len(deckDB[site]) 126 | 127 | print " %-10s - %s Decks" % (site, deckLen) 128 | 129 | return deckDB 130 | 131 | def saveCard(cardDB): 132 | 133 | json.dump(cardDB, open("card.db", "w")) 134 | 135 | return 136 | 137 | def saveDeck(deckDB): 138 | 139 | # Save the deck and keep a successful copy as backup 140 | # If you cancel / script crashes sometimes JSON will get corrupt so best to keep a backup 141 | try: 142 | print " [*] Saving data..." 143 | 144 | json.dump(deckDB, open("deck.db", "w")) 145 | 146 | print " [*] Saving backup..." 147 | 148 | json.dump(deckDB, open("backup_deck.db", "w")) 149 | 150 | print " [*] Saves complete. Resuming..." 151 | 152 | except: 153 | print "\n[!] Issue with the save! Backup deck can be found at backup_deck.db!" 154 | 155 | sys.exit(1) 156 | 157 | return deckDB 158 | 159 | def deckStats(deckDB): 160 | 161 | highCard, lowCard, uniqueCards, totalCards = deckCount(deckDB) 162 | 163 | print "\n [-] Total cards in deck data - %s" % totalCards 164 | print " [-] Unique cards in deck data - %s" % uniqueCards 165 | print " [-] Most common non-basic land card - %s" % highCard 166 | print " [-] Least common non-basic land card - %s" % lowCard 167 | 168 | return 169 | 170 | def deckCount(deckDB): 171 | 172 | totalCards = 0 173 | 174 | cardList = {} 175 | 176 | for site in deckDB: 177 | 178 | for deck in deckDB[site]: 179 | 180 | ignoreDecks = ["deckURL"] 181 | 182 | if deck not in ignoreDecks: 183 | 184 | for card in deckDB[site][deck]["deckCards"]: 185 | 186 | totalCards += deckDB[site][deck]["deckCards"][card] 187 | 188 | if card in cardList: 189 | 190 | cardList[card] += deckDB[site][deck]["deckCards"][card] 191 | 192 | else: 193 | 194 | cardList[card] = deckDB[site][deck]["deckCards"][card] 195 | 196 | # Remove lands from list since these will always surpass regular cards 197 | cardList.pop("Forest", None) 198 | cardList.pop("Plains", None) 199 | cardList.pop("Swamp", None) 200 | cardList.pop("Mountain", None) 201 | cardList.pop("Island", None) 202 | 203 | highCard = max(cardList.iteritems(), key=operator.itemgetter(1))[0] 204 | lowCard = min(cardList.iteritems(), key=operator.itemgetter(1))[0] 205 | uniqueCards = len(cardList) 206 | 207 | return highCard, lowCard, uniqueCards, totalCards 208 | 209 | def deckClean(deckDB): 210 | 211 | deleteDecks = {} 212 | ignoreList = ["deckURL"] 213 | 214 | for site in deckDB: 215 | 216 | deleteDecks[site] = [] 217 | 218 | for deckID in deckDB[site]: 219 | 220 | if deckID not in ignoreList: 221 | 222 | # Delete decks under 40 cards 223 | if deckDB[site][deckID]["deckCount"] < 40: 224 | 225 | deleteDecks[site].append(deckID) 226 | 227 | # Delete decks over 135 cards 228 | if deckDB[site][deckID]["deckCount"] > 135: 229 | 230 | deleteDecks[site].append(deckID) 231 | 232 | deletedCount = 0 233 | 234 | for site in deleteDecks: 235 | 236 | for deckID in deleteDecks[site]: 237 | 238 | deckDB[site].pop(deckID) 239 | deletedCount += 1 240 | 241 | print "\n[+] Pruned %s decks due to size restrictions." % deletedCount 242 | 243 | deckDB = saveDeck(deckDB) 244 | 245 | return deckDB 246 | 247 | def cardStats(cardDB): 248 | 249 | highCard, lowCard, uniqueCards, totalCards = cardCount(cardDB) 250 | 251 | print "\n [-] Total cards in your collection - %s" % totalCards 252 | print " [-] Unique cards in your collection - %s" % uniqueCards 253 | print " [-] Most common non-basic land card - %s" % highCard 254 | print " [-] Least common non-basic land card - %s\n" % lowCard 255 | 256 | return 257 | 258 | def cardCount(cardDB): 259 | 260 | cardList = {} 261 | totalCards = 0 262 | 263 | for card in cardDB: 264 | 265 | totalCards += cardDB[card] 266 | 267 | for card in cardDB: 268 | 269 | if card in cardList: 270 | 271 | cardList[card] += cardDB[card] 272 | 273 | else: 274 | cardList[card] = cardDB[card] 275 | 276 | cardList.pop("Forest", None) 277 | cardList.pop("Plains", None) 278 | cardList.pop("Swamp", None) 279 | cardList.pop("Mountain", None) 280 | cardList.pop("Island", None) 281 | 282 | highCard = max(cardList.iteritems(), key=operator.itemgetter(1))[0] 283 | lowCard = min(cardList.iteritems(), key=operator.itemgetter(1))[0] 284 | uniqueCards = len(cardList) 285 | 286 | return highCard, lowCard, uniqueCards, totalCards 287 | 288 | def checkDecks(deckDB, args): 289 | 290 | # Randomly select order of sites from which to parse for updates 291 | print "[+] Checking sites for new decks..." 292 | 293 | siteList = ["deckbox", "goldfish", "mtgtop8"] 294 | 295 | random.shuffle(siteList) 296 | 297 | for site in siteList: 298 | 299 | if site == "deckbox": 300 | 301 | deckDB = dbcheckSite(deckDB, args) 302 | 303 | if site == "goldfish": 304 | 305 | deckDB = gfcheckSite(deckDB, args) 306 | 307 | if site == "mtgtop8": 308 | 309 | deckDB = t8checkSite(deckDB, args) 310 | 311 | return deckDB 312 | 313 | def t8checkSite(deckDB, args): 314 | 315 | print """\n########### 316 | # MtGTop8 # 317 | ########### 318 | """ 319 | 320 | response = urllib2.urlopen("http://mtgtop8.com/index") 321 | html = response.read() 322 | 323 | # Parse eventID's out 324 | urlList = [x.replace("event?e=","").replace("&","") for x in re.findall("event\?e=[0-9]+&", html)] 325 | 326 | if urlList[-1] not in deckDB["mtgtop8"]["deckURL"] or args.update != False: 327 | 328 | print "[!] Press CTRL+C to stop updating a section at any time\n" 329 | 330 | try: 331 | deckDB = t8updateDecks(deckDB, urlList, args) 332 | 333 | except KeyboardInterrupt: 334 | deckDB = saveDeck(deckDB) 335 | return deckDB 336 | 337 | else: 338 | 339 | print " [!] MtGTop8 up-to-date" 340 | 341 | return deckDB 342 | 343 | def t8updateDecks(deckDB, urlList, args): 344 | 345 | deckCount = 0 346 | noName = ["W.png", 347 | "B.png", 348 | "R.png", 349 | "G.png", 350 | "U.png", 351 | "C.png"] 352 | 353 | if urlList[0] not in deckDB["mtgtop8"]["deckURL"]: 354 | 355 | print "[+] Performing incremental deck update\n" 356 | 357 | try: 358 | 359 | # Check if this is the first run and adjust accordingly to correctly download some 360 | if deckDB["mtgtop8"]["deckURL"] == []: 361 | 362 | deckDB["mtgtop8"]["deckURL"] = urlList 363 | 364 | highID = int(urlList[0]) 365 | 366 | for event in range(highID, int(urlList[-1]), -1): 367 | 368 | response = urllib2.urlopen("http://mtgtop8.com/event?e=%s" % event) 369 | 370 | # Site uses Windows-1252 so we'll convert as necessary 371 | try: 372 | html = response.read().decode("Windows-1252").encode("utf-8") 373 | except: 374 | html = response.read() 375 | 376 | # Remove newline as their HTML sometimes splits titles 377 | html = html.replace("\n", "") 378 | 379 | if "data[Deck][cards]" not in html: 380 | 381 | pass 382 | 383 | else: 384 | 385 | # Parse deckID's from each event 386 | deckList = re.findall("\?e=[0-9]+\&d=[0-9]+\&f=[A-Z]{2,3}", html) 387 | eventTitle = str(re.search("class=S18 align=center\>.+?\", html).group().replace("class=S18 align=center>","").replace("", "")) 388 | 389 | if event not in deckDB["mtgtop8"]["deckURL"]: 390 | 391 | # Need to keep events separate from deck data 392 | deckDB["mtgtop8"]["deckURL"].append(event) 393 | 394 | print " [#] Downloading decks from event %s - %s" % (event, eventTitle) 395 | 396 | for deckID in deckList: 397 | 398 | deckID = deckID.split("=")[2].replace("&f", "") 399 | titleSearch = r"d=" + deckID + "&f=[A-Z].+?\>.+?\<\/a\>" 400 | deckTitle = str(re.search(titleSearch, html).group().split(">")[1].replace(".+?\", html).group().replace("class=S18 align=center>", "").replace("", "")) 466 | 467 | if event not in deckDB["mtgtop8"]["deckURL"]: 468 | 469 | # Need to keep events separate from deck data 470 | deckDB["mtgtop8"]["deckURL"].append(event) 471 | 472 | print " [#] Downloading decks from event %s - %s" % (event, eventTitle) 473 | 474 | for deckID in deckList: 475 | 476 | deckID = deckID.split("=")[2].replace("&f", "") 477 | titleSearch = r"d=" + deckID + "&f=[A-Z].+?\>.+?\<\/a\>" 478 | deckTitle = str(re.search(titleSearch, html).group().split(">")[1].replace("\<", html)]: 578 | 579 | urlList.append(urlEntry) 580 | 581 | # Parse "budget" ($) deckURL's 582 | response = urllib2.urlopen("https://www.mtggoldfish.com/decks/budget/%s#paper" % format) 583 | html = response.read() 584 | 585 | for urlEntry in [x for x in re.findall("\/deck\/[0-9]{1,7}'\>\<", html)]: 586 | 587 | urlList.append(urlEntry) 588 | 589 | if all(x in deckDB["goldfish"]["deckURL"] for x in urlList): 590 | 591 | print " [!] %s format decks up-to-date" % format 592 | 593 | else: 594 | 595 | print " [#] Downloading %s format new decks..." % format 596 | 597 | deckDB = gfupdateDecks(deckDB, urlList, args) 598 | 599 | except KeyboardInterrupt: 600 | deckDB = saveDeck(deckDB) 601 | return deckDB 602 | 603 | return deckDB 604 | 605 | def gfupdateDecks(deckDB, urlList, args): 606 | 607 | deckCount = 0 608 | deckList = {} 609 | 610 | for urlEntry in urlList: 611 | 612 | if urlEntry not in deckDB["goldfish"]["deckURL"]: 613 | 614 | # Parse deck for card 615 | response = urllib2.urlopen("https://www.mtggoldfish.com/%s#paper" % urlEntry.replace("'><", "")) 616 | html = response.read() 617 | 618 | deckDB["goldfish"]["deckURL"].append(urlEntry) 619 | 620 | try: 621 | deckTitle = str(re.search("\.+?\<\/title\>", html).group().replace("", "").replace("","")) 622 | 623 | if "/deck/download/" not in html: 624 | #print " [!] No download link for deck - %s" % deckTitle 625 | break 626 | 627 | deckID = str(re.search("\/deck\/download\/[0-9]{1,7}\"\>", html).group().replace("/deck/download/","").replace("\">", "")) 628 | deckList[deckID] = deckTitle 629 | 630 | try: 631 | gfscrapeDeck(deckDB, deckID, deckList) 632 | 633 | except: 634 | pass 635 | 636 | deckCount += 1 637 | 638 | # Save after every 100 decks 639 | if deckCount == 100: 640 | 641 | deckDB = saveDeck(deckDB) 642 | 643 | deckCount = 0 644 | 645 | except: 646 | pass 647 | 648 | return deckDB 649 | 650 | def gfscrapeDeck(deckDB, deckID, deckList): 651 | 652 | deckTitle = deckList[deckID].replace("for Magic: the Gathering", "") 653 | deckDB["goldfish"][deckID] = {"deckCards": {}, "deckCount": 0} 654 | deckDB["goldfish"][deckID]["deckTitle"] = deckTitle 655 | 656 | print " [-] Adding Deck %s - %s" % (deckID, deckTitle) 657 | 658 | # Parse cards from deck 659 | response = urllib2.urlopen("https://www.mtggoldfish.com/deck/download/%s" % deckID) 660 | html = response.read() 661 | 662 | cardList = re.findall("[0-9]{1,2} .+?\r\n", html) 663 | 664 | for card in cardList: 665 | 666 | cardCount = int(card.split(" ")[0]) 667 | cardName = str(" ".join(card.replace("\r\n", "").split(" ")[1:])) 668 | 669 | if cardName in deckDB["goldfish"][deckID]["deckCards"]: 670 | 671 | deckDB["goldfish"][deckID]["deckCards"][cardName] += cardCount 672 | 673 | else: 674 | 675 | deckDB["goldfish"][deckID]["deckCards"][cardName] = cardCount 676 | 677 | deckDB["goldfish"][deckID]["deckCount"] += cardCount 678 | 679 | return deckDB 680 | 681 | def dbcheckSite(deckDB, args): 682 | 683 | print """\n########### 684 | # Deckbox # 685 | ########### 686 | """ 687 | 688 | # Parse deckID's 689 | response = urllib2.urlopen("https://deckbox.org/decks/mtg") 690 | html = response.read() 691 | 692 | latestID = re.search("\/sets\/[0-9]+\"\>", html).group().replace("/sets/", "").replace("\">", "") 693 | 694 | if latestID not in deckDB["deckbox"] or args.update == True: 695 | 696 | try: 697 | deckDB = dbupdateDecks(deckDB, html, args) 698 | 699 | except KeyboardInterrupt: 700 | deckDB = saveDeck(deckDB) 701 | return deckDB 702 | 703 | else: 704 | print " [!] Deckbox up-to-date" 705 | 706 | return deckDB 707 | 708 | def dbupdateDecks(deckDB, html, args): 709 | 710 | deckList = [] 711 | ignoreList = ["deckURL"] 712 | 713 | for deckID in deckDB["deckbox"]: 714 | 715 | if deckID not in ignoreList: 716 | 717 | deckList.append(deckID) 718 | 719 | deckList.sort() 720 | 721 | latestID = int(re.search("\/sets\/[0-9]+\"\>", html).group().replace("/sets/", "").replace("\">", "")) 722 | 723 | # First run 724 | if deckList == []: 725 | 726 | lowID = latestID - 50 727 | highID = latestID - 50 728 | 729 | else: 730 | 731 | lowID = int(deckList[0]) 732 | highID = int(deckList[-1]) 733 | 734 | deckCount = 0 735 | 736 | if args.verbose: 737 | print "\n [-] Latest ID on Deckbox - %s" % latestID 738 | print " [-] Most recent saved ID - %s" % highID 739 | print " [-] Oldest saved ID - %s" % lowID 740 | 741 | print "[!] Press CTRL+C to stop updating at any time\n" 742 | 743 | if latestID > highID: 744 | 745 | print "[+] Performing incremental deck update\n" 746 | 747 | for deckID in range(latestID, highID, -1): 748 | 749 | try: 750 | deckDB = dbscrapeDeck(deckDB, deckID) 751 | 752 | except KeyboardInterrupt: 753 | deckDB = saveDeck(deckDB) 754 | return deckDB 755 | 756 | except: 757 | pass 758 | 759 | deckCount += 1 760 | 761 | # Save after every 100 decks 762 | if deckCount == 100: 763 | 764 | deckDB = saveDeck(deckDB) 765 | 766 | deckCount = 0 767 | 768 | # Full update will likely be canceled 769 | if args.update != False and lowID > 0: 770 | 771 | print "[+] Performing full deck update\n" 772 | 773 | for deckID in range(lowID, 0, -1): 774 | 775 | try: 776 | deckDB = dbscrapeDeck(deckDB, deckID) 777 | 778 | except KeyboardInterrupt: 779 | deckDB = saveDeck(deckDB) 780 | return deckDB 781 | 782 | except: 783 | pass 784 | 785 | deckCount += 1 786 | 787 | # Save after every 100 decks 788 | if deckCount == 100: 789 | 790 | deckDB = saveDeck(deckDB) 791 | 792 | deckCount = 0 793 | 794 | return deckDB 795 | 796 | def dbscrapeDeck(deckDB, deckID): 797 | 798 | deckDB["deckbox"][deckID] = {"deckCards": {}, "deckCount": 0} 799 | 800 | # Parse cards from deck 801 | response = urllib2.urlopen("https://deckbox.org/sets/%s" % deckID) 802 | html = response.read() 803 | 804 | html = html.replace("\n", "") 805 | 806 | cardEntries = re.findall("\.+?\<\/tr\>", html) 807 | 808 | deckTitle = re.search("\"name\":\".+?\",\"", html).group() 809 | deckTitle = str(deckTitle.replace("\"name\":\"", "").replace("\",\"", "")) 810 | 811 | if deckTitle == "Wishlist" or deckTitle == "Tradelist" or deckTitle == "Inventory": 812 | 813 | return deckDB 814 | 815 | else: 816 | 817 | print " [-] Adding Deck %s - %s" % (deckID, deckTitle) 818 | 819 | deckDB["deckbox"][deckID]["deckTitle"] = deckTitle 820 | 821 | for entry in cardEntries: 822 | 823 | entry = entry.replace("<", "\n") 824 | entry = entry.strip().split("\n") 825 | 826 | cardName = str(entry[4].split(">")[1]) 827 | cardCount = int(entry[1].replace("td class='card_count'>", "").replace("= lowSize and deckDB[site][deckID]["deckCount"] <= highSize: 858 | 859 | matchState, matchDelta, matchCount = matchDecks(cardDB, deckDB, deckID, site, args) 860 | 861 | if matchState == True: 862 | 863 | deckList[site][deckID] = {"matchDelta": matchDelta, "matchCount": matchCount} 864 | 865 | return deckList 866 | 867 | def matchDecks(cardDB, deckDB, deckID, site, args): 868 | 869 | commonality = float(args.commonality) / float(100) 870 | matchCount = 0 871 | deckCount = 0 872 | deckDelta = [] 873 | 874 | for card in deckDB[site][deckID]["deckCards"]: 875 | 876 | deckCount += deckDB[site][deckID]["deckCards"][card] 877 | 878 | for card in deckDB[site][deckID]["deckCards"]: 879 | 880 | if card not in cardDB and commonality == 1.0: 881 | 882 | return False, None, None 883 | 884 | if card in cardDB: 885 | 886 | if deckDB[site][deckID]["deckCards"][card] > cardDB[card] and commonality == 1.0: 887 | 888 | return False, None, None 889 | 890 | if deckDB[site][deckID]["deckCards"][card] <= cardDB[card]: 891 | 892 | matchCount += deckDB[site][deckID]["deckCards"][card] 893 | 894 | else: 895 | 896 | # Capture cards in collection but missing count 897 | matchCount += cardDB[card] 898 | deckDelta.append("%s - %s" % (deckDB[site][deckID]["deckCards"][card] - cardDB[card],card)) 899 | 900 | # Capture cards not in collection 901 | if card not in cardDB: 902 | 903 | deckDelta.append("%s - %s" % (deckDB[site][deckID]["deckCards"][card], card)) 904 | 905 | # Check for 100% (default) or specified match % 906 | try: 907 | if float(matchCount)/float(deckCount) >= commonality: 908 | 909 | return True, deckDelta, str("%s%% (%-3s/%-3s)" % (int(float(matchCount)/float(deckCount) * 100.0), matchCount, deckCount)) 910 | 911 | else: 912 | 913 | return False, None, None 914 | 915 | except: 916 | return False, None, None 917 | 918 | def printDecks(deckDB, deckID, site, args, filterWords, matchCount, matchDelta): 919 | 920 | # Deck structure for pre-cons slightly different since no ID 921 | if site == "precon": 922 | 923 | deckTitle = deckID 924 | 925 | else: 926 | 927 | deckTitle = deckDB[site][deckID]["deckTitle"] 928 | 929 | if any(x.lower() in deckTitle.lower() for x in filterWords): 930 | 931 | return 932 | 933 | if site == "deckbox": 934 | 935 | print " [-] Deck %s - %s (%s cards - %s)\n https://deckbox.org/sets/%s\n" % (deckID, 936 | deckTitle, 937 | deckDB[site][deckID]["deckCount"], 938 | matchCount.split(" ")[0], 939 | deckID) 940 | if args.verbose: 941 | 942 | print " [!] %-5s - Missing Cards: %s\n" % (matchCount, ", ".join(matchDelta)) 943 | 944 | if site == "goldfish": 945 | 946 | print " [-] Deck %s - %s (%s cards - %s)\n https://www.mtggoldfish.com/deck/%s\n" % (deckID, 947 | deckTitle, 948 | deckDB[site][deckID]["deckCount"], 949 | matchCount.split(" ")[0], 950 | deckID) 951 | if args.verbose: 952 | 953 | print " [!] %-5s - Missing Cards: %s\n" % (matchCount, ", ".join(matchDelta)) 954 | 955 | if site == "mtgtop8": 956 | 957 | print " [-] Deck %s - %s (%s cards - %s)\n http://mtgtop8.com/event?d=%s\n" % (deckID, 958 | deckTitle, 959 | deckDB[site][deckID]["deckCount"], 960 | matchCount.split(" ")[0], 961 | deckID) 962 | if args.verbose: 963 | 964 | print " [!] %-5s - Missing Cards: %s\n" % (matchCount, ", ".join(matchDelta)) 965 | 966 | if site == "precon": 967 | 968 | print " [-] Official - %s (%s cards - %s)\n %s\n" % (deckTitle, 969 | deckDB[site][deckID]["deckCount"], 970 | matchCount.split(" ")[0], 971 | deckDB["precon"][deckID]["deckURL"]) 972 | 973 | def main(): 974 | 975 | parser = argparse.ArgumentParser(description="Find playable decks from DeckBox, MTGTOP8, MTGGoldFish, and Official PreCons") 976 | parser.add_argument("-u", "--update", help="Perform full deck update.", action="store_true") 977 | parser.add_argument("-c", "--commonality", help="Match above specified percentage, default is 100%%.", metavar="", type=int, default=100) 978 | parser.add_argument("-n", "--noupdate", help="Perform just deck matches on existing data.", action="store_true") 979 | parser.add_argument("-v", "--verbose", help="Print additional meta data. Can take a LONG time to process.", action="store_true") 980 | parser.add_argument("-f", "--filter", help="Comma separated list of words to filter from matched decks.") 981 | parser.add_argument("-s", "--size", help="Limit returned decks to a certain deck size, default is 40-80.", metavar=",", default="40-80") 982 | args = parser.parse_args() 983 | 984 | print """ 985 | __ __, _ __,__, __, _ 986 | / \_( \ /_\ //~_) |//~' _ (_) _ 987 | / % : '<_v_\\\\\\~/|_(\\\\__, _ (_) (_) 988 | / /\ C\ \ ~ ~~ ~ ~ ~~~~~ _ _ 989 | / / \ \ \ \ The Gathering (_) (_) 990 | ~~~ ~~` ~~` 991 | """ 992 | 993 | cardDB = loadCard(args) 994 | deckDB = loadDeck(args) 995 | 996 | filterWords = [] 997 | 998 | if args.filter: 999 | 1000 | filterWords = args.filter.split(",") 1001 | 1002 | if not args.noupdate: 1003 | 1004 | checkDecks(deckDB, args) 1005 | 1006 | deckDB = deckClean(deckDB) 1007 | 1008 | deckList = searchDecks(cardDB, deckDB, args) 1009 | 1010 | if len(deckList) > 0: 1011 | 1012 | for site in deckList: 1013 | 1014 | for deckID in deckList[site]: 1015 | 1016 | printDecks(deckDB, 1017 | deckID, 1018 | site, 1019 | args, 1020 | filterWords, 1021 | deckList[site][deckID]["matchCount"], 1022 | deckList[site][deckID]["matchDelta"]) 1023 | 1024 | else: 1025 | 1026 | print "[!] No playable decks with current cards\n" 1027 | 1028 | if __name__ == "__main__": 1029 | 1030 | main() 1031 | --------------------------------------------------------------------------------