├── 12dicts_words.txt ├── LICENSE ├── README.md ├── Run.bat └── WordChainSolver.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HackerPoet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordChainSolver 2 | 3 | Word Chain Solver computes the minimal number of changes from one word to another by changing only one letter at a time. It will also generate a DOT file for analysis. See [This Video](https://youtu.be/6GuqNrV8LsQ) for a full overview. 4 | 5 | ## Using The Solver 6 | 7 | There is a default dictionary included sourced from http://wordlist.aspell.net/12dicts/ 8 | However, you may use any dictionary by replacing the word list file `WORD_LIST` with your own. 9 | By default, word lengths are 3 letters, but you can change `WORD_LEN` to any number. 10 | Once the dictionary and word length are set, just run `python WordChainSolver.py` to begin. You can choose 2 words and the program will print the optimal path to get there. It will also generate the **graph.dot** file. 11 | 12 | **WARNING:** The word list included here is completely unfiltered and may include slurs and other offensive words. You should consider using a different dictionary if you need everything clean. 13 | 14 | ## Viewing The Graphs 15 | 16 | If you'd like to make graphs like the video, follow these steps. 17 | 18 | 1. Download and install [Gephi](https://gephi.org/) 19 | 2. Open Gephi and load the DOT file 'graph.dot'. 20 | 3. In the layout tab, select **ForceAtlas 2** 21 | 4. In the ForceAtlas 2 settings, Enable the option "Stronger Gravity" and set the "Gravity" to 0.02 22 | 5. Press **Run** and watch the graph form. 23 | 6. Once the graph has mostly stopped moving, enable the "Prevent Overlap" option. 24 | 7. Press **Stop** to end the layout changes. 25 | 8. In appearance, under "Ranking", choose "Degree" and then choose a color palette. Then press **Apply** 26 | 9. In the Graph window press the icon that looks like a T to Show Node Labels. 27 | 10. Adjust the size of the labels with the slider until they fit in the nodes. 28 | 11. Adjust the color of the labels so they contrast well with the node colors. 29 | -------------------------------------------------------------------------------- /Run.bat: -------------------------------------------------------------------------------- 1 | python WordChainSolver.py 2 | pause -------------------------------------------------------------------------------- /WordChainSolver.py: -------------------------------------------------------------------------------- 1 | # This word list is sourced from http://wordlist.aspell.net/12dicts/ 2 | # using 12dicts-6.0.2/International/3of6game.txt with some preprocessing 3 | # to remove any annotations. 4 | WORD_LIST = '12dicts_words' 5 | 6 | # Change this to the length of word you'd like to try. 7 | WORD_LEN = 3 8 | 9 | # Maximum depth to compute a search. Shouldn't ever need to be higher, 10 | # but you may lower it to quit early in a very large dictionary. 11 | MAX_ITERS = 100 12 | 13 | # Convert a string to a number 14 | def make_number(word): 15 | num = 0 16 | mult = 1 17 | for w in word: 18 | num += (ord(w) - ord('A')) * mult 19 | mult *= 256 20 | return num 21 | 22 | # Convert a number to a string 23 | def make_word(number): 24 | word = "" 25 | for i in range(WORD_LEN): 26 | word += chr((number & 0xFF) + ord('A')) 27 | number >>= 8 28 | return word 29 | 30 | # Check if 2 strings differ by only 1 letter (slow) 31 | def are_pair(w1, w2): 32 | numDiffs = 0 33 | for i in range(len(w1)): 34 | if w1[i] != w2[i]: 35 | numDiffs += 1 36 | if numDiffs >= 2: return False 37 | return numDiffs == 1 38 | 39 | # Check if 2 numbers differ by only 1 letter (fast) 40 | def are_pair_num(n1, n2): 41 | numDiffs = 0 42 | for i in range(WORD_LEN): 43 | if ((n1 >> (i*8)) & 0xFF) != ((n2 >> (i*8)) & 0xFF): 44 | numDiffs += 1 45 | if numDiffs >= 2: return False 46 | return numDiffs == 1 47 | 48 | # Create a lookup table for 1 letter diffs (fastest) 49 | print('Creating Diff Lookup Table...') 50 | pair_lut = set() 51 | for i in range(WORD_LEN): 52 | for j in range(32): 53 | pair_lut.add(j << (i * 8)) 54 | 55 | print('Loading Dictionary...') 56 | all_words = [] 57 | with open(WORD_LIST + '.txt', 'r') as fin: 58 | for word in fin: 59 | word = word.strip().upper() 60 | if len(word) == WORD_LEN: 61 | all_words.append(make_number(word)) 62 | print('Loaded ' + str(len(all_words)) + ' words.') 63 | 64 | print('Finding All Connections...') 65 | all_pairs = [] 66 | for i in range(len(all_words)): 67 | w1 = all_words[i] 68 | word1 = make_word(w1) 69 | for j in range(i): 70 | w2 = all_words[j] 71 | #p = are_pair_num(w1, w2) 72 | p = (w1 ^ w2) in pair_lut 73 | if p: 74 | all_pairs.append((word1, make_word(w2))) 75 | print("Found " + str(len(all_pairs)) + " connections.") 76 | 77 | # DOT file format does not allow these keywords, make sure to change them 78 | keywords = ['NODE', 'EDGE', 'GRAPH', 'DIGRAPH', 'SUBGRAPH', 'STRICT'] 79 | def fix_keyword(w): 80 | if w in keywords: 81 | return '_' + w 82 | return w 83 | 84 | print("Writing file...") 85 | with open("graph.dot",'w') as fout: 86 | fout.write('graph words {\n') 87 | for w in all_words: 88 | word = fix_keyword(make_word(w)) 89 | fout.write(' "' + word + '";\n') 90 | for w1,w2 in all_pairs: 91 | fout.write(' "' + fix_keyword(w1) + '" -- "' + fix_keyword(w2) + '";\n') 92 | fout.write('}\n') 93 | 94 | # Main loop for solver interface 95 | print("") 96 | while True: 97 | from_word = make_number(input('From Word: ').upper()) 98 | to_word = make_number(input(' To Word: ').upper()) 99 | if from_word != 0 and not from_word in all_words: 100 | print("No connections to " + make_word(from_word)) 101 | continue 102 | if not to_word in all_words: 103 | print("No connections to " + make_word(to_word)) 104 | continue 105 | 106 | connections = {} 107 | dist = dict([(word,-1) for word in all_words]) 108 | dist[to_word] = 0 109 | is_found = False 110 | for iter in range(MAX_ITERS): 111 | print(iter) 112 | made_changes = False 113 | for w1 in all_words: 114 | if dist[w1] == iter: 115 | for w2 in all_words: 116 | if dist[w2] != -1: continue 117 | if (w1 ^ w2) not in pair_lut: continue 118 | dist[w2] = iter + 1 119 | connections[w2] = w1 120 | made_changes = True 121 | if w2 == from_word: 122 | print("Found!") 123 | is_found = True 124 | break 125 | if is_found: break 126 | if is_found or (not made_changes): break 127 | 128 | if from_word != 0: 129 | if not from_word in connections: 130 | print('Can not connect!') 131 | else: 132 | w = from_word 133 | while True: 134 | print(make_word(w)) 135 | if w == to_word: break 136 | w = connections[w] 137 | print(str(dist[from_word]) + " steps") 138 | else: 139 | for word in all_words: 140 | if dist[word] > 0: 141 | print(make_word(word) + " in " + str(dist[word]) + " steps") 142 | #else: 143 | # print(word + " no connection") 144 | --------------------------------------------------------------------------------