PennyDreadfulMTG / Penny-Dreadful-Tools

A suite of tools for the Penny Dreadful MTGO community
https://pennydreadfulmagic.com
MIT License
41 stars 28 forks source link

Fuzzy matching #21

Closed bakert closed 8 years ago

bakert commented 8 years ago

[Ad Nauseum] should load Ad Nauseam. [all sun's dawn] and [all suns dawn] should load All Suns' Dawn.

bakert commented 8 years ago

Some examples of spelling mistakes folks have made that it would have been nice to match anyway:

[Define Bloodlord] => Defiant Bloodlord [Ashenmoor Gourger] => Ashenmoor Gouger [Ashenmmor] => Ashenmoor [Frantic Scavenging] => Frantic Salvage [Frantic Salvaging] => Frantic Salvage [narcomeba] => Narcomoeba [Winds of Wratg] => Winds of Rath [Winds of Wrath] => Winds of Rath [Thespian Stage] => Thespian's Stage [Glistner elf] => Glistener Elf [mercurial giests] => Mercurial Geists [Dread Statuary,] => Dread Statuary [HEROES PODIUM] => Heroes' Podium [thermoalc] => Thermo-Alchemist [Chain Lighting] => Chain Lightning [Grand Colliseum] => Grand Coliseum [krosna tusker] => Krosan Tusker [Day of Judgement] => Day of Judgment [Hundred handed] => Hundred-Handed One

bakert commented 8 years ago

[Mind Swipe] => Mindswipe [corpse connisseur] => Corpse Connoisseur [Tarmogofy] => Tarmogoyf [crircular logic] => Circular Logic [Decree of Anih] => Decree of Annihilation [Hurricane?] => Hurricane [Vulshock Refugee] => Vulshok Refugee [grim laavamancer] => Grim Lavamancer

bakert commented 8 years ago

SOUNDEX() matching hits all of these except crircular logic and define bloodlord. I'm gonna implement it that way.

bakert commented 8 years ago

Hm. SOUNDEX matching finds 13 cards for 'Decree of Annihilation'. I'm less excited about that now :)

Including Dukhara Peafowl.

bakert commented 8 years ago

http://www.sqlite.org/spellfix1.html seems to be what we want. It is not part of any standard sqlite3 distribution and probably has to be built locally. But it is documented as a supported module off the root of sqlite.org so I think it's ok to adopt it as a dependency? I'm going to mess around with it and see what's what.

bakert commented 8 years ago

spellfix1 is glorious.

sqlite> SELECT distance, word FROM demo WHERE word MATCH 'Define Bloodlord'; 115|Defiant Bloodlord 475|Divine Verdict 490|Defiant Vanguard 520|Divine Favor 563|Divine Deflection 566|Devils' Playground 585|Defense Grid 600|Divine Offering 605|Divine Presence 605|Defiant Falcon 620|Defiant Ogre 622|Defend the Hearth 630|Divine Light 633|Divine Congregation 635|Defiant Strike 636|Thawing Glaciers 645|Divine Sacrament 650|Divine Reckoning 655|Topan Freeblade 655|Defiant Stand sqlite> SELECT distance, word FROM demo WHERE word MATCH 'Ad Nauseum'; 30|Ad Nauseam 430|Outmaneuver 505|Outnumber 590|Admonition Angel sqlite> SELECT distance, word FROM demo WHERE word MATCH 'mercurial giests'; 65|Mercurial Geists 255|Mercurial Kite 316|Mercurial Chemister 440|Mercurial Pretender 455|Mercadian Lift 545|Mercadian Atlas 545|Mercadian Bazaar 557|Morkrut Banshee 561|Morsel Theft 575|Merciless Resolve 580|Marker Beetles 581|Mirri's Guile 591|Mercenary Knight 595|Mirror Golem 595|Mirror Gallery 598|Mercadia's Downfall 605|Mercenaries 606|Marsh Gas 607|Marchesa's Emissary 610|Mercy Killing

We're going to have to work out the cross-platform-ness.

bakert commented 8 years ago

Default python sqlite3 lib does not allow loading of extensions so we'd probably have to compile that too. Frustrating, this would be a really sweet improvement.

bakert commented 8 years ago

Finding the right threshold here is tricky. If we want 'Frantic Scavenging' to match 'Frantic Salvaging' we need distance threshold above 500. But that lets 'Winds of Wrath' match 'Wands of the Elements'.

Probably what we want is to use the best result when nothing has a good distance but restrict to a good distance in the first instance. So 'ashennmoor' only finds the 'Ashenmoor X' cards but 'Winds of Wratg' finds 'Winds of Rath' because it's the best match.

For now I'm going to hardcode something like 150 or 200 and see how it goes.

Some example distances:

define bloodlord [{'distance': 165, 'word': 'Defiant Bloodlord'}, {'distance': 520, 'word': 'Divine Verdict'}, {'distance': 540, 'word': 'Defiant Vanguard'}, {'distance': 560, 'word': 'Divine Deflection'}, {'distance': 570, 'word': 'Divine Favor'}, {'distance': 580, 'word': 'Divine Congregation'}, {'distance': 610, 'word': 'Divine Retribution'}, {'distance': 615, 'word': 'Divine Presence'}, {'distance': 616, 'word': "Devils' Playground"}, {'distance': 620, 'word': 'Divine Sacrament'}, {'distance': 625, 'word': 'Divine Offering'}, {'distance': 635, 'word': 'Defense Grid'}, {'distance': 640, 'word': 'Divine Transformation'}, {'distance': 645, 'word': 'Divine Intervention'}, {'distance': 655, 'word': 'Defiant Falcon'}, {'distance': 660, 'word': 'Defiant Strike'}, {'distance': 670, 'word': 'Defiant Ogre'}, {'distance': 672, 'word': 'Defend the Hearth'}, {'distance': 675, 'word': 'Divine Reckoning'}, {'distance': 680, 'word': 'Topan Freeblade'}] ashenmoor gourger [{'distance': 135, 'word': 'Ashenmoor Gouger'}, {'distance': 325, 'word': 'Ashenmoor Cohort'}, {'distance': 380, 'word': 'Ashenmoor Liege'}, {'distance': 545, 'word': 'Ashen Rider'}, {'distance': 550, 'word': 'Ashen Powder'}, {'distance': 557, 'word': "Ajani's Presence"}, {'distance': 571, 'word': 'Aysen Bureaucrats'}, {'distance': 576, 'word': 'Essence Vortex'}, {'distance': 576, 'word': 'Ajani, Caller of the Pride'}, {'distance': 578, 'word': "Ajani's Chosen"}, {'distance': 580, 'word': 'Echoing Courage'}, {'distance': 581, 'word': 'Aysen Crusader'}, {'distance': 592, 'word': "Ajani's Pridemate"}, {'distance': 596, 'word': 'Azimaet Drake'}, {'distance': 601, 'word': 'Exhumer Thrull'}, {'distance': 607, 'word': "Ajani's Mantra"}, {'distance': 611, 'word': 'Essence Filter'}, {'distance': 611, 'word': 'Acidic Dagger'}, {'distance': 612, 'word': "Ajani's Sunstriker"}, {'distance': 615, 'word': 'Ashen Monstrosity'}] ashenmmor [{'distance': 70, 'word': 'Ashenmoor Liege'}, {'distance': 70, 'word': 'Ashenmoor Gouger'}, {'distance': 70, 'word': 'Ashenmoor Cohort'}, {'distance': 206, 'word': 'Aquamorph Entity'}, {'distance': 210, 'word': 'Ashen Rider'}, {'distance': 225, 'word': 'Ashen Monstrosity'}, {'distance': 231, 'word': 'Isamaru, Hound of Konda'}, {'distance': 261, 'word': 'Aysen Crusader'}, {'distance': 265, 'word': 'Ashen Firebeast'}, {'distance': 266, 'word': 'Oakenform'}, {'distance': 270, 'word': 'Exhumer Thrull'}, {'distance': 281, 'word': 'Aysen Bureaucrats'}, {'distance': 285, 'word': 'Ashen Powder'}, {'distance': 285, 'word': 'Ashen-Skin Zubera'}, {'distance': 285, 'word': 'Ashen Ghoul'}, {'distance': 296, 'word': 'Assemble the Legion'}, {'distance': 296, 'word': 'Axegrinder Giant'}, {'distance': 296, 'word': 'Assembly Hall'}, {'distance': 296, 'word': 'Aquamoeba'}, {'distance': 296, 'word': 'Assemble the Rank and Vile'}] frantic scavenging [{'distance': 565, 'word': 'Frantic Salvage'}, {'distance': 595, 'word': 'Frantic Search'}, {'distance': 675, 'word': 'Frantic Purification'}, {'distance': 690, 'word': 'Burning Vengeance'}, {'distance': 695, 'word': 'Frenetic Sliver'}, {'distance': 710, 'word': 'Frontier Siege'}, {'distance': 745, 'word': 'Burnt Offering'}, {'distance': 755, 'word': 'Bronze Calendar'}, {'distance': 765, 'word': 'Frenzied Tilling'}, {'distance': 770, 'word': 'Paranoid Delusions'}, {'distance': 770, 'word': 'Firemane Avenger'}, {'distance': 775, 'word': 'Formless Nurturing'}, {'distance': 775, 'word': 'Barrenton Cragtreads'}, {'distance': 785, 'word': 'Frankie Peanuts'}, {'distance': 790, 'word': 'Frontier Bivouac'}, {'distance': 795, 'word': 'Frenzied Goblin'}, {'distance': 806, 'word': "Frankenstein's Monster"}, {'distance': 810, 'word': 'Frenzy Sliver'}, {'distance': 815, 'word': 'Furnace Celebration'}, {'distance': 815, 'word': 'Prying Questions'}] frantic salvaging [{'distance': 290, 'word': 'Frantic Salvage'}, {'distance': 525, 'word': 'Frantic Search'}, {'distance': 555, 'word': 'Frantic Purification'}, {'distance': 610, 'word': 'Paranoid Delusions'}, {'distance': 640, 'word': 'Frenzied Tilling'}, {'distance': 660, 'word': 'Frontier Mastodon'}, {'distance': 671, 'word': 'Bruna, the Fading Light'}, {'distance': 675, 'word': 'Formless Nurturing'}, {'distance': 675, 'word': 'Burnt Offering'}, {'distance': 690, 'word': 'Frenetic Sliver'}, {'distance': 700, 'word': 'Frontier Siege'}, {'distance': 700, 'word': 'Frontier Bivouac'}, {'distance': 700, 'word': "Barrin's Unmaking"}, {'distance': 715, 'word': 'Furnace Dragon'}, {'distance': 730, 'word': 'Frenzied Goblin'}, {'distance': 745, 'word': 'Frenetic Raptor'}, {'distance': 750, 'word': 'Frenetic Efreet'}, {'distance': 750, 'word': 'Frenetic Efreet Avatar'}, {'distance': 755, 'word': 'Barrenton Cragtreads'}, {'distance': 760, 'word': 'Brine Elemental'}] ashenmmor [{'distance': 70, 'word': 'Ashenmoor Liege'}, {'distance': 70, 'word': 'Ashenmoor Gouger'}, {'distance': 70, 'word': 'Ashenmoor Cohort'}, {'distance': 206, 'word': 'Aquamorph Entity'}, {'distance': 210, 'word': 'Ashen Rider'}, {'distance': 225, 'word': 'Ashen Monstrosity'}, {'distance': 231, 'word': 'Isamaru, Hound of Konda'}, {'distance': 261, 'word': 'Aysen Crusader'}, {'distance': 265, 'word': 'Ashen Firebeast'}, {'distance': 266, 'word': 'Oakenform'}, {'distance': 270, 'word': 'Exhumer Thrull'}, {'distance': 281, 'word': 'Aysen Bureaucrats'}, {'distance': 285, 'word': 'Ashen Powder'}, {'distance': 285, 'word': 'Ashen-Skin Zubera'}, {'distance': 285, 'word': 'Ashen Ghoul'}, {'distance': 296, 'word': 'Assemble the Legion'}, {'distance': 296, 'word': 'Axegrinder Giant'}, {'distance': 296, 'word': 'Assembly Hall'}, {'distance': 296, 'word': 'Aquamoeba'}, {'distance': 296, 'word': 'Assemble the Rank and Vile'}] narcomeba [{'distance': 105, 'word': 'Narcomoeba'}, {'distance': 265, 'word': 'Narcolepsy'}, {'distance': 345, 'word': 'Markov Warlord'}, {'distance': 345, 'word': 'Markov Patrician'}, {'distance': 350, 'word': 'Nirkana Assassin'}, {'distance': 365, 'word': "Marchesa's Decree"}, {'distance': 365, 'word': "Marchesa's Infiltrator"}, {'distance': 365, 'word': "Marchesa's Emissary"}, {'distance': 365, 'word': 'Mercenaries'}, {'distance': 365, 'word': "Marchesa's Smuggler"}, {'distance': 365, 'word': 'Mercenary Knight'}, {'distance': 365, 'word': 'Mercenary Informer'}, {'distance': 365, 'word': 'Marchesa, the Black Rose'}, {'distance': 370, 'word': 'Nirkana Revenant'}, {'distance': 380, 'word': 'Narcissism'}, {'distance': 385, 'word': 'Mercadian Lift'}, {'distance': 385, 'word': 'Mercadian Bazaar'}, {'distance': 385, 'word': "Mercadia's Downfall"}, {'distance': 385, 'word': 'Mercadian Atlas'}, {'distance': 385, 'word': 'Marked by Honor'}] winds of wratg [{'distance': 235, 'word': 'Winds of Rath'}, {'distance': 300, 'word': 'Winds of Change'}, {'distance': 345, 'word': 'Bonds of Mortality'}, {'distance': 385, 'word': 'Bonds of Faith'}, {'distance': 400, 'word': 'Winds of Qal Sisma'}, {'distance': 416, 'word': 'Wings of the Guard'}, {'distance': 431, 'word': 'Wings of Aesthir'}, {'distance': 450, 'word': 'Bonds of Quicksilver'}, {'distance': 450, 'word': 'Font of Fertility'}, {'distance': 450, 'word': 'Font of Fortunes'}, {'distance': 455, 'word': 'Vines of Vastwood'}, {'distance': 455, 'word': 'Wings of Velis Vel'}, {'distance': 485, 'word': 'Font of Return'}, {'distance': 490, 'word': 'Bane of Progress'}, {'distance': 490, 'word': 'Bend or Break'}, {'distance': 496, 'word': 'Whims of the Fates'}, {'distance': 510, 'word': 'Vines of the Recluse'}, {'distance': 510, 'word': 'Wand of Ith'}, {'distance': 510, 'word': 'Wand of the Elements'}, {'distance': 520, 'word': 'Wings of Hope'}] winds of wrath [{'distance': 135, 'word': 'Winds of Rath'}, {'distance': 246, 'word': 'Bonds of Mortality'}, {'distance': 285, 'word': 'Bonds of Faith'}, {'distance': 301, 'word': 'Winds of Qal Sisma'}, {'distance': 301, 'word': 'Winds of Change'}, {'distance': 335, 'word': 'Wings of Aesthir'}, {'distance': 345, 'word': 'Wings of the Guard'}, {'distance': 351, 'word': 'Font of Fertility'}, {'distance': 351, 'word': 'Font of Fortunes'}, {'distance': 356, 'word': 'Wings of Velis Vel'}, {'distance': 391, 'word': 'Font of Return'}, {'distance': 396, 'word': 'Whims of the Fates'}, {'distance': 406, 'word': 'Vines of Vastwood'}, {'distance': 410, 'word': 'Wand of Ith'}, {'distance': 410, 'word': 'Vines of the Recluse'}, {'distance': 410, 'word': 'Wand of the Elements'}, {'distance': 411, 'word': 'Bonds of Quicksilver'}, {'distance': 421

bakert commented 8 years ago

With a limit of 200 the most unsatisfying behaviors are:

[Frantic Scavenging] (no matches) [Frantic Salvaging] (no matches) [Winds of Wratg] (no matches) [thermoalc] (too many matches) [krosna tukser] (no matches)

All the other examples above find one card exactly and it's the right card.

bakert commented 8 years ago

758033c is a first stab at this.

bakert commented 8 years ago

This is released thanks to the team effort in #146!