4
\$\begingroup\$

This is a random word generator I wrote in Python 3 today, I don't know how many Python implemetations of this exist, but this is my first try and it is completely working.

It returns a random word like string that is pronounceable (most likely pronounceable), I used some English letter frequency statistics data to ensure the letter frequencies in the result would follow English letter distribution patterns, and shuffled that data to make the result truly random;

And then I used some English letter following rules (what letters can follow a letter) to make the result pronounceable.

Beware this script is feature incomplete, new features will be added tomorrow, but it is now completely stable and achieved like 70% of the intended ends, anyway here is the code:

import random
from collections import Counter

SAMPLE = Counter({
    'e': 1202,
    't': 910,
    'a': 812,
    'o': 768,
    'i': 731,
    'n': 695,
    's': 628,
    'r': 602,
    'h': 592,
    'd': 432,
    'l': 398,
    'u': 288,
    'c': 271,
    'm': 261,
    'f': 230,
    'y': 211,
    'w': 209,
    'g': 203,
    'p': 182,
    'b': 149,
    'v': 111,
    'k': 69,
    'x': 17,
    'q': 11,
    'j': 10,
    'z': 8
})

pool = list(SAMPLE.elements())
randpool = []

while len(pool) > 0:
    elem = random.choice(pool)
    randpool.append(elem)
    pool.remove(elem)

LETTERS = 'abcdefghijklmnopqrstuvwxyz'

tails = {
    'a': LETTERS,
    'b': 'aeioubjlry',
    'c': 'aeiouchjklry',
    'd': 'aeioudgjwy',
    'e': LETTERS,
    'f': 'aeioufjlry',
    'g': 'aeioughjlrwy',
    'h': 'aeiouy',
    'i': LETTERS,
    'j': 'aeiouy',
    'k': 'aeiouhklrvwy',
    'l': 'aeioulvwy',
    'm': 'aeioucmy',
    'n': 'aeiougny',
    'o': LETTERS,
    'p': 'aeioufhlprsty',
    'q': 'aeiou',
    'r': 'aeiouhrwy',
    's': 'aeiouchjklmnpqrstvwy',
    't': 'aeiouhjrstwy',
    'u': LETTERS,
    'v': 'aeioulvy',
    'w': 'aeiouhry',
    'x': 'aeiouhy',
    'y': 'aeiousvwy',
    'z': 'aeiouhlmvwy'
}

def randomword():
    count = random.randint(1, 6)
    heads = [random.choice(randpool) for i in range(count)]
    i = 0
    segments = []
    while count > 0:
        h = heads[i]
        w = h
        while True:
            r = random.choice(randpool)
            if r in tails[h]:
                if i == 0 and r == h: continue
                else: break
        w += r
        while True:
            f = r
            r = random.choice(randpool)
            if r in tails[f]:
                w += r
                if random.randint(0, 9999) % 2 == 0:
                    segments.append(w)
                    count -= 1
                    break
        i += 1
    return ''.join(segments)

if __name__ == '__main__':
    print(randomword())

Please share your thoughts on my script, about how it can be improved, about what new rules I need to add to make the result resemble real words more.

I currently am thinking about making consecutive vowels less than three and no more than three consecutive consonants without a vowel, I am able to implement these, but they are all I can think of now.

Please share your thoughts on possible improvements, any help is welcome!

\$\endgroup\$
1
  • \$\begingroup\$ Hi Xeнεi Ξэnвϵς. Please do not update the code in your question to incorporate feedback from answers, doing so goes against the question & answer style of Code Review. As such I have rolled back your latest edit. Please see what you may and may not do after receiving answers. \$\endgroup\$
    – Peilonrayz
    May 31, 2021 at 8:57

3 Answers 3

6
\$\begingroup\$

This isn't Python specific, but in tails, every letter will map to at least all the vowels. Since every entry contains the vowels aeiou, I'd probably make that implicit for the sake of brevity, maintainability, and space. You could automatically append the voewels to the result of the lookup, then only store in the dictionary the unique letters ("bjlry" for the case of "b" for example). You'd have to figure out how to handle letters like "a" that map to everything though, but that might be made easier with my next suggestions.


LETTERS is unnecessary. Python already has this in the string module:

from string import ascii_lowercase

You could use that with sets to generate consonants:

LETTERS = set(ascii_lowercase)
VOWELS = set("aeiou")
CONSONANTS = LETTERS - VOWELS

Then you could change your tails to something like:

unique_tails = {
    'a': CONSONANTS,
    'b': 'bjlry',
    'c': 'chjklry',
    'd': 'dgjwy',
    'e': CONSONANTS,
    . . .

And then change your loops to something like:

if r in tails[h] or r in VOWELS:

You may find this ends up nicer. I like to avoid duplication, so I'd go in this direction, but you may find it complicates matters.


You use a lot of single-letter variable names, and it's hurting readability. i for an index is common and fine, but h, w, and r should really be spelled out so their purpose is clearer.


random.randint(0, 9999) % 2 == 0 seems convoluted. As far as I can tell, that's just basically a "coin-flip", but it's basing it on a random number from 0-9999 for some reason. I think the 9999 is a red-herring that makes the code harder to understand,and it would be clearer to just generate a single random bit:

if random.getrandbits(1) == 0:  # Or maybe just if random.getrandbits(1):
\$\endgroup\$
3
\$\begingroup\$

Since you've requested input on how to improve the algorithm in addition to directly improving code (and I know very little about Python anyways), I'll share my idea on it.

Use Markov chains

It's pretty close to what you're already doing, but determines the following letter based on many letters before it, not just one. Obviously, it will have vastly bigger lookup table, so you'll also want to write a (probably separate) program that parses dictionary and stores that data for word generation. It's up to you how to store result, one of viable options is to use sqlite due to relative ease of use, great speed and ability to operate in-memory. My testing reveals that optimal amount of letters to account for is 3: less gives incoherent results and more leads to words being too close to existing ones.

Also, Markov chains aren't limited to word generation. With some tweaking it should be possible to produce (rather odd) texts.

\$\endgroup\$
0
\$\begingroup\$

I had made a few improvements to the code, its outputs are now much better, though still it isn't not quite what I had in mind when I wrote the code, but it is much closer than before, though it still isn't ideal.

It now imports ascii_lowercase from String module, has a constant that stores vowels, and gets consonants by subtracting vowels from letters.

I capitalized the name of tails because it acts as a constant during code execution, and only store unique entries in it.

I made a few corrections to the letter following rules, and extended the definition of vowels to include the letter "y", because y can follow virtually any letter.

I lowered the maximum limit of heads a word can have to four, and set the limit to the maximum number of tails a head can have to a random integer in the range between two and five, just in the improbable case the coin-flip fails to break the loops.

I have also made the script discard the random letter if there are already two consecutive vowels or consonants or same letters.

So this is the updated code:

import random
from collections import Counter
from string import ascii_lowercase

SAMPLE = Counter({
    'e': 1202,
    't': 910,
    'a': 812,
    'o': 768,
    'i': 731,
    'n': 695,
    's': 628,
    'r': 602,
    'h': 592,
    'd': 432,
    'l': 398,
    'u': 288,
    'c': 271,
    'm': 261,
    'f': 230,
    'y': 211,
    'w': 209,
    'g': 203,
    'p': 182,
    'b': 149,
    'v': 111,
    'k': 69,
    'x': 17,
    'q': 11,
    'j': 10,
    'z': 8
})

pool = list(SAMPLE.elements())
randpool = []

while len(pool) > 0:
    elem = random.choice(pool)
    randpool.append(elem)
    pool.remove(elem)

LETTERS = set(ascii_lowercase)
VOWELS = set('aeiouy')
CONSONANTS = LETTERS - VOWELS

TAILS = {
    'a': CONSONANTS,
    'b': 'bjlr',
    'c': 'chjklr',
    'd': 'dgjw',
    'e': CONSONANTS,
    'f': 'fjlr',
    'g': 'ghjlrw',
    'h': '',
    'i': CONSONANTS,
    'j': '',
    'k': 'hklrvw',
    'l': 'l',
    'm': 'cm',
    'n': 'gn',
    'o': CONSONANTS,
    'p': 'fhlprst',
    'q': '',
    'r': 'hrw',
    's': 'chjklmnpqstw',
    't': 'hjrstw',
    'u': CONSONANTS,
    'v': 'lv',
    'w': 'hr',
    'x': 'h',
    'y': 'sv',
    'z': 'hlvw'
}

# variables expanded:
# w: Word, r: Random Letter, sc: Serial Consonants count, sv: Serial Vowels Count, ss: Serial Same-letter count, lm: Max Length of tails, l: Length of tails

def randomword():
    count = random.randint(1, 4)
    heads = [random.choice(randpool) for i in range(count)]
    i = 0
    segments = []
    while count > 0:
        sc, ss, sv = 0, 0, 0 
        w = heads[i]
        if w in CONSONANTS: sc += 1
        else: sv += 1
        while True:
            r = random.choice(randpool)
            if r in TAILS[w] or r in VOWELS:
                if i == 0 and r == w: continue
                else:
                    if r in VOWELS:
                        sc = 0
                        sv += 1
                        break
                    else:
                        sv = 0
                        sc += 1
                        break
        w += r
        l = 1
        lm = random.randint(2, 5)
        while True:
            if l == lm:
                segments.append(w)
                count -= 1
                break
            f = r
            r = random.choice(randpool)
            if r in TAILS[f] or r in VOWELS:
                if r in VOWELS:
                    sc = 0
                    sv += 1
                elif r in CONSONANTS:
                    sv = 0
                    sc += 1
                if sv == 3 or sc == 3: continue
                if r != f: ss = 0
                if r == f and ss == 1: continue
                if r == f: ss += 1
                w += r
                l += 1
                if random.getrandbits(1):
                    segments.append(w)
                    count -= 1
                    break
        i += 1
    return ''.join(segments)

if __name__ == '__main__':
    print(randomword())
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.