In [40]:
import re
try:
    sum = __builtins__.sum
except AttributeError:
    sum = __builtins__["sum"]

RINGS = [ring.lower() for ring in 'BDMJPRSTLN', 'AEIOUYRTLH', 'ACDEORSTLN', 'DHKYRSTLNE']
rx_string = "^" + "".join( ("[" + ring + "]" for ring in RINGS)) + "$"
print rx_string
rx = re.compile(rx_string)
for s in "BAAD", "BAAQ", "ABAAD", "baad", "baada", "abaad":
    if rx.match(s):
        print s, "YES"
    else:
        print s, "NO"
print
^[bdmjprstln][aeiouyrtlh][acdeorstln][dhkyrstlne]$
BAAD NO
BAAQ NO
ABAAD NO
baad YES
baada NO
abaad NO

In [42]:
dictionary = (line.rstrip('\n') for line in file("/usr/share/dict/words", "r"))
matchwords = [word for word in dictionary if rx.match(word)]

print len(matchwords), "matching words"
    
1048 matching words
In [54]:
NUMDICTS = [ {letter: position for position, letter in enumerate(ring)}
             for ring in RINGS ]

def unique_key(s):
    nums = [numdict[letter] for numdict, letter in zip(NUMDICTS, s)]
    return tuple( (num - nums[0]) % len(RINGS[0]) for num in nums[1:] )
    
for word in "baad", "bean", "salt", "noon":
    print word, unique_key(word)
baad (0, 0, 0)
bean (1, 0, 8)
salt (4, 2, 0)
noon (4, 5, 9)
In [46]:
from collections import defaultdict

GROUPS = defaultdict(list)

for word in matchwords:
    GROUPS[unique_key(word)].append(word)
    
GROUPS_OF_3 = [words for key, words in GROUPS.iteritems() if len(words) == 3]

print len(GROUPS), "distinct groups of words"
print len(GROUPS_OF_3), "groups of three words"
699 distinct groups of words
59 groups of three words
In [47]:
for words in GROUPS_OF_3:
    print unique_key(words[0]), "  ", " ".join(words)
(2, 8, 2)    bilk neth slon
(6, 6, 3)    paal side toed
(4, 3, 0)    node sant teal
(2, 4, 0)    biod musk slat
(6, 0, 9)    brae lull siss
(6, 7, 5)    pace redd took
(7, 2, 4)    jarl rite sold
(2, 5, 8)    birn dose laet
(3, 8, 7)    boll less tarr
(1, 3, 6)    beet diol morn
(6, 8, 2)    reel sion tore
(1, 0, 0)    bead dich nane
(2, 3, 7)    doon mure thar
(8, 6, 4)    malt pian sudd
(8, 3, 2)    jess roll sunn
(9, 6, 5)    jinn mell trek
(6, 3, 9)    bree paty toat
(3, 1, 2)    bock lend tale
(4, 9, 6)    bunt pled sark
(6, 1, 0)    lunn parr toll
(5, 7, 1)    byth lore seel
(4, 5, 6)    burt lier sack
(5, 4, 5)    rand seah tick
(1, 7, 7)    mone nast stey
(6, 5, 2)    pant real tode
(7, 9, 0)    jady peer sort
(9, 1, 8)    dade meed pork
(9, 7, 1)    dalk meny tron
(2, 3, 1)    dook lace than
(2, 4, 5)    bios lady neer
(6, 8, 1)    reet siol torn
(1, 4, 8)    dire nael star
(2, 7, 4)    lark munt sled
(0, 7, 8)    dele mind puck
(3, 8, 2)    bolk duny tare
(6, 3, 3)    brey sine toad
(1, 0, 7)    beal mode nant
(9, 7, 8)    dale mend pock
(0, 5, 2)    bark punt ryal
(3, 2, 3)    body duer nick
(2, 6, 2)    doty prat thee
(2, 5, 4)    birr doss mutt
(8, 2, 4)    jerl rote suld
(1, 6, 3)    junt nark pyal
(4, 3, 3)    lich sane tead
(8, 5, 5)    pine road such
(2, 7, 6)    bitt doll ness
(6, 3, 5)    lucy pate sinh
(8, 5, 9)    math piny roar
(5, 4, 1)    lode rant seal
(8, 7, 8)    mand pick suer
(8, 7, 5)    jean pice rodd
(0, 5, 4)    dess joll mitt
(6, 5, 6)    pand sick tody
(3, 4, 7)    bool durn shay
(8, 9, 7)    dhan mace rook
(6, 4, 5)    pale rend tock
(7, 8, 1)    lyse sool turn
(2, 9, 9)    bine much prey