I've been playing the game for 50 years and this happened to me recently:
I had the following rack
PBQZJDH
And so I could not play a legal move.
I've been trying to work out the odds of this (no legal word on the first turn) happening.
I've been playing the game for 50 years and this happened to me recently:
I had the following rack
PBQZJDH
And so I could not play a legal move.
I've been trying to work out the odds of this (no legal word on the first turn) happening.
(NOTE : Final revision of my original answer)
The odds of the first tileset not having a single valid word is exactly 91,595,416 / 16,007,560,800 or .5722%, with it occuring once every 174.76378 games. This value is calculated by using the dictionary found in this answer, but can be adapted for any other dictionary.
This was brute-forced via python. Code will be available at the end of the answer.
We start by noticing that any tileset that contains at least one blank tile can play a valid word. The only letter that does not form a two letter word is the letter "V", and there are only 2 "V" tiles in a scrabble game. Therefore, any tileset with one blank can form a valid word, because (?VV....) must contain 4 more letters, one of which will combine with the "?" to form a valid word.
We also discovered, after the fact, that all possible tilesets that contain the letter "O" are also, in fact, valid. The only 6 tile tileset that contains "O" and cannot form a word is "OCCVVQ" and any other letter will form a word. "A" is the only other letter that doesn't form a two letter word with "O" but "AVO" and "OCA" are both valid words, and you can't include more than one O or one A or else you can form "AA" or "OO".
These observations remove 53.32% of all possible tilesets, and allows us to perform the rest of our calculations using a 90-tile scrabble set that has no blanks and no Os in it.
We then remove from the dictionary all words that satisfy one of the following criteria
By applying these three rules, we can reduce the scrabble dictionary down to 149 words out of the original 280k~.
AA AB AD AE AG AH AI AL AM AN AR AS AT AW AX AY BE BI BRR BUB BUD BULK BULL BY CH CIRRI CIVIC CLY CRY CUB CUD CUE CUFF CULL CUZ CWM DE DI DRY DUD EE EF EGG EH EL EM EN ER ES ET EUK EW EX FA FLU FUB FUCK FUD FY GHYLL GI GRR GU GYP HI HM HYP ICE ICY IF IN IS IT IVY IWI JA JEU JIZ JUD JUKU KA KEG KI KUDU KUZU KY LI LUCK LUD LULL LUV LUZ MI MM MU MY NTH NU NY PA PE PFFT PHT PI PLY PRY PWN PYX QI QUA RHY RIZ SH SLY SPY ST SWY THY TRY TWP TYG TYPP TYPY UH ULU UP UR US UT UVA VAC VAV VEG VIE VLY WHY WIZ WRY WUD WULL WUZ XI XU XYLYL YE YIRR YU ZA ZE ZO
At this point, we are going to brute force the number of invalid tilesets to get the numerator. We do this by examining tilesets that are in alphabetical order, but do not care if it's unique. For example, we will examine (A1A2BCDEF) and (A1A3BCDEF) but not (A2A1BCDEF), where A1, A2, and A3 refer to distinct A tiles in the bag.
The first tileset examined is (A1A2A3A4A5A6A7), followed by (A1A2A3A4A5A6A8), then (A1A2A3A4A5A6A9), and then finally (A1A2A3A4A5A6B1). We continue down that path in the way that only a computer can do to determine every single alphabetized combination.
(Note that in the final version of my code, we change the definition of "alphabetical order" in order to get a speed improvement, but the final solution is the same. Also, for speed reasons, we would never actually examine (AA.....) because "AA" is a valid word, so we would skip all tilesets of the form (AA.....))
After running through all the tilesets, we end with a final count of 91,595,416 tilesets. This is our numerator, and the denominator is very easily calculated
100! / ((100-7)! * 7!) = 16,007,560,800
This is the calculation for finding the number of combinations possible, without replacement, and not caring about order (which we don't because otherwise this problem would be an few orders of magnitude harder to brute force.
I'm going to be putting some interesting notes down here as I play around with the program.
Code, if anyone wants to run it for themselves (WARNING: slow, running on a beefy computer it still about a half hour to spit out an answer)
words = []
words_in_dictionary = 0
words_short_enough = 0
def is_all_c(arr1):
for c in arr1:
if c in ['A','E','I','O','U']:
return False
return True
def is_all_v(arr1):
for v in arr1:
if v in ['B','C','D','F','G','H','J','K','L','M','N','P','Q','R','S','T','V','W','X','Y','Z']:
return False
return True
#basic function to check if an array, arr1, is fully within a second array, arr2
def i_in_j(arr1, arr2):
for a in arr1:
if a in arr2:
arr2.remove(a)
else:
return False
return True
#basic function to check to see if word can be made from the tileset passed in
def is_valid_tileset(tileset):
for word in words:
if i_in_j(word["sorted"][:], tileset[:]):
return word["base"]
return None
# we have a big dictionary, 270k-ish words long, we need to load it
print("loading dictionary")
with open("dictionary.txt", "r") as dictfile:
for line in dictfile:
words_in_dictionary = words_in_dictionary + 1
base_word = line.strip()
#we are going to parse out the words longer than 7 letters now because it's quicker
#we are also going to get rid of words with "O" in it
if len(base_word) <= 7:
if not "O" in base_word:
words_short_enough = words_short_enough + 1
word = {"base": base_word, "sorted": sorted(base_word)}
words.append(word)
print("total words in dictionary is " + str(words_in_dictionary))
print("words 7 letters or shorter is " + str(words_short_enough))
# now we need to build our dictionary of unique words
# any word where a subset of the letters in that word can be used to build another word will be discarded
# for example, "AND" will be discarded because we can make "AN" out of that word
i = 0
while i < len(words):
temp_sorted_working_word = words[i]["sorted"]
j = 0
while j < len(words):
if i == j:
j = j + 1
continue
if i_in_j(words[i]["sorted"][:], words[j]["sorted"][:]):
del words[j]
if i > j:
i = i - 1
j = j - 1
elif j > i:
j = j - 1
j = j + 1
i = i + 1
# there are also 8 words from this list that cannot be built without blanks, and we know that any tileset
# with at least one blank is valid
i = 0
while i < len(words):
if words[i]["base"] in ["FUFF", "JUJU", "KECK", "KUKU", "SYZYGY", "ZIZ", "ZUZ", "ZZZ"]:
del words[i]
else:
i = i + 1
print("unique minimilized word combinations is " + str(len(words)))
# this is an array of all tiles in scrabble, minus the blanks (since we don't care about them)
# because we're pruning words from the dictionary, we've rearranged the letters to try and remove as many
# words as possible as quickly as possible, to make lookups run faster, this means that the vowels and
# R,S,T,L,N have been moved up the list, it doesn't really impact anything, as alphabetical order is arbitary
# and nothing from this point forward depends on us having a sorted list
letters = ['A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'Y', 'Y', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'U', 'U', 'U', 'U', 'H', 'H', 'P', 'P', 'M', 'M', 'R', 'R', 'R', 'R', 'R', 'R', 'T', 'T', 'T', 'T', 'T', 'T', 'W', 'W', 'C', 'C', 'N', 'N', 'N', 'N', 'N', 'N', 'S', 'S', 'S', 'S', 'B', 'B', 'F', 'F', 'G', 'G', 'G', 'L', 'L', 'L', 'L', 'D', 'D', 'D', 'D', 'J', 'K', 'Q', 'V', 'V', 'X', 'Z']
invalid_tilesets = 0
for a in range(0, 84):
# if we've finished using a letter in the tileset, we're gonna remove the words in the condensed dictionary
# that utilize those letters, this is more of a speed thing than anything else because those dictionary lookups
# are expensive
if a > 0 and letters[a-1] != letters[a]:
i = 0
while i < len(words):
if i_in_j([letters[a-1]], words[i]["sorted"]):
del words[i]
else:
i = i + 1
print("invalid_tilesets = " + str(invalid_tilesets) + " | new_first_letter = " + letters[a] + " | words in dictionary = " + str(len(words)))
for b in range(a+1, 85):
if not is_valid_tileset([letters[a], letters[b]]):
for c in range(b+1, 86):
if not is_valid_tileset([letters[a], letters[b], letters[c]]):
for d in range(c+1, 87):
if not is_valid_tileset([letters[a], letters[b], letters[c], letters[d]]):
for e in range(d+1, 88):
if not is_valid_tileset([letters[a], letters[b], letters[c], letters[d], letters[e]]):
for f in range(e+1, 89):
if not is_valid_tileset([letters[a], letters[b], letters[c], letters[d], letters[e], letters[f]]):
for g in range(f+1, 90):
if not is_valid_tileset([letters[a], letters[b], letters[c], letters[d], letters[e], letters[f], letters[g]]):
invalid_tilesets += 1
if invalid_tilesets % 10000 == 0:
print("invalid_tilesets = " + str(invalid_tilesets) + " | " + str([letters[a], letters[b], letters[c], letters[d], letters[e], letters[f], letters[g]]) + " | " + str([a,b,c,d,e,f,g]))
print("invalid_tilesets = " + str(invalid_tilesets))
I'm going to make an estimate from the following assumption:
Any hand that contains at least one vowel, y, or a blank allows a valid move. Any hand that contains entirely consonants does not. Obviously there are exceptions, but they should be rare enough to have a negligible effect (and the false positives and false negatives work to cancel each other out).
There are 46 of these tiles and 54 that are not. The chance of consecutively drawing 7 consonants is therefore:
54/100 * 53/99 * 52/98 * 51/97 * 50/96 * 49/95 * 48/94
This works out at 1.11%, or about 1 in 90 games.
I used the beginning of the program in @DenisS's answer to build the Scrabble dictionary, then I used it to write a small monte-carlo program to estimate the probability that no word can be formed with seven random tiles.
The result is a 0.58% probability that no word can be formed.
Output
$ python3 get_proba.py 1000 50
loading dictionary
total words in dictionary is 279497
words 7 letters or shorter is 77459
Running for 50 experiments of 1000 draws...
Ran for 50 experiments of 1000 draws.
Successes: [996, 996, 996, 995, 992, 996, 998, 993, 994, 993, 992, 993, 998, 994, 994, 986, 994, 996, 990, 994, 997, 998, 994, 993, 993, 991, 999, 991, 997, 996, 993, 989, 995, 996, 998, 996, 995, 996, 992, 992, 998, 994, 993, 989, 993, 991, 991, 999, 995, 995]
Proba of failure = 0.00582000000000005
Code
def build_dict():
words = []
words_in_dictionary = 0
words_short_enough = 0
print("loading dictionary")
with open("dictionary.txt", "r") as dictfile:
for line in dictfile:
base_word = line.strip()
if len(base_word) > 0:
words_in_dictionary = words_in_dictionary + 1
if len(base_word) <= 7:
words_short_enough = words_short_enough + 1
word = {"base": base_word, "sorted": sorted(base_word)}
words.append(word)
print("total words in dictionary is " + str(words_in_dictionary))
print("words 7 letters or shorter is " + str(words_short_enough))
ok_combinations = [''.join(word["sorted"]) for word in words]
return(ok_combinations)
def flatten(ll):
return [x for l in ll for x in l]
def build_letter_bag():
return flatten([['A']*9, ['B']*2, ['C']*2, ['D']*4, ['E']*12, ['F']*2, ['G']*3, ['H']*2, ['I']*9, ['J']*1, ['K']*1, ['L']*4, ['M']*2, ['N']*6, ['O']*8, ['P']*2, ['Q']*1, ['R']*6, ['S']*4, ['T']*6, ['U']*4, ['V']*2, ['W']*2, ['X']*1, ['Y']*2, ['Z']*1, ['*']*2])
dico = build_dict()
letter_bag=build_letter_bag()
from itertools import chain, combinations
def powerset(iterable):
"powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
def can_make_word(letters):
if '*' in letters:
return True
return any((''.join(subset) in dico) for subset in powerset(sorted(letters)))
import random
def montecarlo(n):
nb_ok = 0
for i in range(n):
letters = random.sample(letter_bag, 7)
nb_ok += (1 if can_make_word(letters) else 0)
return nb_ok
import statistics
def run_experiments(nb_draws, nb_experiments):
nb_ok_list = [montecarlo(nb_draws) for i in range(nb_experiments)]
average = statistics.fmean(nb_ok_list)
stdev = statistics.pstdev(nb_ok_list, mu=average)
return average, stdev, nb_ok_list
def get_args(argv):
nb_draws, nb_exp = 1000, 1
if len(argv) > 1:
nb_draws = int(argv[1])
if len(argv) > 2:
nb_exp = int(argv[2])
return nb_draws, nb_exp
def main(argv):
random.seed()
nb_draws, nb_experiments = get_args(argv)
print('Running for {} experiments of {} draws...'.format(nb_experiments, nb_draws))
average, stdev, l = run_experiments(nb_draws, nb_experiments)
print('Ran for {} experiments of {} draws.'.format(nb_experiments, nb_draws))
print('Successes:', l)
print('Proba of failure = {} +- {}'.format((nb_draws - average)/nb_draws, stdev/nb_draws))
import sys
if __name__=='__main__':
main(sys.argv)
Rendering unto Caesar:
build_dict()
is from @DenisS's answer;dictionary.txt
is the 2019 Collins Scrabble Words file linked in this answer to a related question;if '*' in letters: return True
in my code);91592097 in 16007560800 which is approximately 0.572% (or 1 in 175).
Some of what follows is already covered in @DenisS's answer and I have used the same dictionary of words (Collins Scrabble Words (2019)) for easy comparison. Note in particular in that answer the argument for discounting blanks when looking for valid combinations without words (i.e. that the only letter not in a 2 letter word is a V and that there are not enough of those to fill up our 7 selected tiles) and the discussions on pruning.
The following approach is rather “quick and dirty” and relies on several tools that are available on multiple platforms.
Firstly, I took the dictionary and alphabetised the letters in each word. (I removed duplicates, caused by words which were anagrams of each other, although this was not necessary. This resulted in a dictionary containing 247491 words.)
The 2 letter words (93 unique alphabetised words) were then removed and the dictionary pruned so that it no longer contained any words which contained all the letters of one of those words. (For example, the word AE
removed words from the list including those where the letters were adjacent such as AESSSY
and where they were not adjacent AABCELN
).
This was done as a simple iteration over the 2 letter words in bash
using grep
with some shell parameter expansions.
for f in $(cat 2LetterWords) ; do grep -v ${f:0:1}".*"${f:1:1} temp > a; rm temp; mv a temp; done
The 3 letter words (61 unique alphabetised words) were then extracted and the new dictionary pruned in a similar manner. 4 letter words (15) and 5 letter words (4) were similarly extracted. At each stage, the handful of dictionary words that could not be formed without using blanks were also removed.
As all other words in the dictionary contain the letters that enable us to make one of these 2-5 letter words, these are the only ones we need to consider. I.e. we only need to find the combinations of 7 tiles where we cannot make any of the following 173 words:
AA AB AD AE AF AG AH AI AJ AK AL AM AN AP AR AS AT AW AX AY AZ BE BI BO BY CH DE DI DO EE EF EH EL EM EN EO EP ER ES ET EW EX EY EZ FI FO FY GI GO GU HI HM HO HS HU IK IL IM IN IO IP IQ IS IT IX JO KO KY LO MM MO MU MY NO NU NY OO OP OR OS OT OU OW OX OY OZ PU RU ST SU TU UX UY ACO ACV AOV AQU AUV AVV BBU BCU BDU BFU BRR CDU CEI CEU CIY CLY CMW CRY CUZ DDU DFU DJU DLU DRY DUW EGG EGK EGV EIV EJU EKU FLU GPY GRR GTY HNT HPT HPY HRY HTY HWY IIW IJZ IRZ IVY IWZ LPY LSY LUU LUV LUZ LVY NPW PRY PSY PTW PXY RTY RWY SWY UWZ BKLU BLLU CFFU CFKU CKLU CLLU DKUU FFPT IRRY JKUU KUUZ LLLU LLUW PPTY PTYY CCIIV CIIRR GHLLY LLXYY
There are 16,007,560,800 (100 C 7) combinations of tiles we can pick, although some of these combinations will be indistinguishable from each other. If we only consider the number of combinations that are distinguishable we are reduced to 3,199,724 which is a far more tractable value and, from any given distinguishable combination we can easily calculate the number of different combinations of tiles which are indistinguishable.
That value can be calculated using some brute-force methods. A bunch of nested loops in C such as
for (A=0;A<=anMax[0];A++)
for (B=0;B<=anMax[1];B++)
for (C=0;C<=anMax[2];C++)
for (D=0;D<=anMax[3];D++)
…
where the anMax
array (offset from 0) is set to the number of available tiles for each letter struggles but a few short-circuit checks to ensure that we do not go over the required number of tiles
…
for (C=0;C<=anMax[2];C++) if (A+B+C<8)
…
is sufficient to run the calculation in a couple of seconds. (My first attempt, adding checks spaced out on the C, E, G, L, O, S and W was good enough.)
A little more shell scripting in awk
, such as:
awk '{print (substr($0,1,1)" && "substr($0,2,2)") ||"}' 2LetterWords
with a little bit of editing (to account for repeated letters), e.g. (for the two letter words)
if (
(A>1) || (A && B) || (A && D) || (A && E) || (A && F) || (A && G) || (A && H) || (A && I) || (A && J) || (A && K) || (A && L) || (A && M) || (A && N) ||
(A && P) || (A && R) || (A && S) || (A && T) || (A && W) || (A && X) || (A && Y) || (A && Z) || (B && E) || (B && I) || (B && O) || (B && Y) || (C && H) ||
(D && E) || (D && I) || (D && O) || (E>1) || (E && F) || (E && H) || (E && L) || (E && M) || (E && N) || (E && O) || (E && P) || (E && R) || (E && S) ||
(E && T) || (E && W) || (E && X) || (E && Y) || (E && Z) || (F && I) || (F && O) || (F && Y) || (G && I) || (G && O) || (G && U) || (H && I) || (H && M) ||
(H && O) || (H && S) || (H && U) || (I && K) || (I && L) || (I && M) || (I && N) || (I && O) || (I && P) || (I && Q) || (I && S) || (I && T) || (I && X) ||
(J && O) || (K && O) || (K && Y) || (L && O) || (M>1) || (M && O) || (M && U) || (M && Y) || (N && O) || (N && U) || (N && Y) || (O>1) || (O && P) ||
(O && R) || (O && S) || (O && T) || (O && U) || (O && W) || (O && X) || (O && Y) || (O && Z) || (P && U) || (R && U) || (S && T) || (S && U) || (T && U) ||
(U && X) || (U && Y)
) return 0;
gave some simple conditional checks to ensure the word list did not appear.
There are 309831 distinguishable combinations where none of the 2-letter words can be formed. 252242 if we ensure 2 and 3 letter words can not be formed. 251180 excluding 2,3 and 4 letter words and down to 251021 excluding the complete list.
We cannot just look at 251021 / 3199724 as our probability as different distinguishable combinations have different numbers of corresponding tile combinations. The distinguishable combinations excluding the word list tend to use the rarer tiles which means they tend to have fewer corresponding tile combinations.
We can count the number of combinations that correspond to a given distinguishable combination by looking at the number of ways the corresponding letters might have been chosen, which was calculated as:
Cr(0,A)* nCr(1,B)* nCr(2,C)* nCr(3,D)* nCr(4,E)* nCr(5,F)* nCr(6,G)* nCr(7,H)* nCr(8,I)* nCr(9,J)*
nCr(10,K)* nCr(11,L)* nCr(12,M)* nCr(13,N)* nCr(14,O)* nCr(15,P)* nCr(16,Q)* nCr(17,R)* nCr(18,S)*
nCr(19,T)* nCr(20,U)* nCr(21,V)* nCr(22,W)* nCr(23,X)* nCr(24,Y)* nCr(25,Z)
This gives us 91,592,097 combinations (of which there are 251,021 distinguishable sets) out of 16,007,560,800.