A Word Game Solver in APL I recommend reading my prior article about a similar solver in Common Lisp, before reading this one. My first thought of an APL solver was tainted by the experience of the Common Lisp solver, and was a direct copy of its structure: a function would return symbolic results processed one-at-a-time using what would effectively be a case statement, like the Masturbation interpreter I'd written. This was harder to reason about than I'd prefer, and never implemented. I easily realized much better means. The code here is Free Software, available under the GNU Affero General Public License version three. The Common Lisp solver provided something easily understood by which to guide and check my thoughts. The core of the solver function is but a single line, albeit one but under two hundred columns wide. I believe this code works properly with elements not in the domain d, simply ignoring them, but make no guarantee; error checking code in APL is such a bother, even function preconditions, so I provide none; this function should work with two-dimensional character arrays with a last dimension of five: d←'abcdefghijklmnopqrstuvwxyz' ∇ wordle l;⎕IO;g;i;s;w ⍝ I have l for list; g for guess; i for index; s for summary; and w for word. ⎕IO←i←1◊s←+/d∘.=w←l[?↑⍴l;]◊'Word: ',w now: ⎕←g←l[?↑⍴l;]◊→end⍴⍨g≡w◊i←i+1◊l←(((+/[2]l∘.=d)∧.≤(6,⍳5)[1+(s<+/d∘.=g)×s⌊+/d∘.=g])∧((+/[2]l∘.=d)∧.≥s⌊+/d∘.=g)∧(((g=w)/l)∧.=(g=w)/w)∧(((~g=w)/l)∧.≠(~g=w)/g)∧(~∨/(l∊(~g∊w)/g)))⌿l◊→now end: 'Guesses: ',⍕i ∇ I was unable to shorten the line any at all; follows is a version that uses many more lines to work: ∇ wordle l;⎕IO;g;i;s;w ⎕IO←i←1◊s←+/d∘.=w←l[?↑⍴l;]◊'Word: ',w now: ⎕←g←l[?↑⍴l;]◊→end⍴⍨g≡w◊i←i+1 l←(~∨/(l∊(~g∊w)/g))⌿l l←(((~g=w)/l)∧.≠(~g=w)/g)⌿l l←(((g=w)/l)∧.=(g=w)/w)⌿l l←((+/[2]l∘.=d)∧.≥s⌊+/d∘.=g)⌿l l←((+/[2]l∘.=d)∧.≤(6,⍳5)[1+(s<+/d∘.=g)×s⌊+/d∘.=g])⌿l◊→now end: 'Guesses: ',⍕i ∇ The question of which be better has no answer, as it depends heavily on the particular APL whether a single dictionary assignment using combined masks per loop performs better than multiple assignments which reduce the dictionary each time. The ordering of mask generations is meant to reduce the most aggressively, starting with words containing letters in the guess absent in the word, then the words with letters in locations known to lack the particular letter, then using exact letter matches, then removing words based on known letter counts. An implementation may notice these expressions are all independent, and could evaluate them in any order or in parallel; whether or not some implementation uses those combining AND expressions to trim the work, and if such would be useful, is questionable. The latter form forces its particular ordering, but it could always lose based on the circumstances. The following code will form the dictionary file into an array under GNU APL with no error checking: dictionary←⎕FIO[26] 'five-letter-words' dictionary←dictionary⍴⍨⌽5,⌊5÷⍨⍴dictionary ∧/,dictionary∊d ⍝ This line will result in one if the dictionary be valid by d, or zero otherwise. All possible discriminations are made in only five mask generations. Thinking of the discrimination in this way brought a clarity of thought to my mind which hadn't been revealed to me with the Common Lisp solver. The case in which known letters are guessed in the wrong locations, the some case with my Common Lisp solver, posed no problems to me in this APL after careful thought, for the lack of an ordering imposed by the one-at-a-time processing present in the Common Lisp solver removed them. In particular, the orderless code means there's not only no need to process exact matches before all of the other cases, but that the result is as if they were processed so regardless. It was in any case truly that single hardest case to mull over, which is why it took three of the five discriminations. I mulled over how best to handle the some case for a tad, and soon remembered the implementations of summarize I'd written and found suitable for the problem; amusingly enough, it wasn't at first clear to me how to use it. The summaries can very easily be used on the domain to sort the words, but use of the words' sorted forms hardly seemed appropriate with little thought, and such code now follows: (+/d∘.='words') 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 0 1 0 0 0 (+/d∘.='words')/d dorsw The visualization of the summary, from my mind's eye alone, made me realize the appropriate function to determine that known information was the minimum function, and this is another clarity the Common Lisp obscured from me, for hash tables are less clearly visualized. In the following example, but a single letter s is known to be in the word ``guess'' using the guess ``words'' as is now very clear: (+/d∘.='guess')⌊(+/d∘.='words') 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 All remaining demonstration code fragments use the following value for their dictionary variable, l: ⎕←l←10 5⍴'applebuzzycrashdoggyevilsfishygreatheartislesjests' apple buzzy crash doggy evils fishy great heart isles jests Firstly is the elimination of guess candidates which contain letters known to be absent in the word: (~'crash'∊'isles')/'crash' crah (~∨/(l∊(~'crash'∊'isles')/'crash'))⌿l buzzy doggy evils isles jests Second is the elimination of guess candidates which contain letters in locations known to lack them: (~'crash'='isles')/'crash' crash (((~'crash'='isles')/l)∧.≠(~'crash'='isles')/'crash')⌿l apple buzzy doggy evils fishy isles jests Thirdly is the opposite elimination of guess candidates that contain letters in known locations not: ('crash'='isles')/'isles' ((('crash'='isles')/l)∧.=('crash'='isles')/'isles')⌿l apple buzzy crash doggy evils fishy great heart isles jests Fourth and fifth are the eliminations of guess candidates based on the known range of letter counts: (+/d∘.='isles')⌊+/d∘.='crash' 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 ((+/[2]l∘.=d)∧.≥(+/d∘.='isles')⌊+/d∘.='crash')⌿l crash evils fishy isles jests ((+/d∘.='isles')<+/d∘.='crash') 1 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 ((+/d∘.='isles')<+/d∘.='crash')×(+/d∘.='isles')⌊+/d∘.='crash' 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 (6,⍳5)[1+((+/d∘.='isles')<+/d∘.='crash')×(+/d∘.='isles')⌊+/d∘.='crash'] 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 ((+/[2]l∘.=d)∧.≤(6,⍳5)[1+((+/d∘.='isles')<+/d∘.='crash')×(+/d∘.='isles')⌊+/d∘.='crash'])⌿l apple buzzy crash doggy evils fishy great heart isles jests Follows is a nicer example for the last case. The last case eliminates words based on known maximum letter counts, which only occur when the guess candidate has more letters of some kind than the word to be guessed. Comparison produces a mask for multiplication with the minimums to turn all cases to zero which should be unknown, and a mapping was the one good means I found to make a zero become six while maintaining all other values. This value six also prohibits the code from suitability for any arrays with column counts other than five, as it must be the maximum, and I cared not to adjust this further; a five could've been used, since a five will never occur in this code, but I preferred six: 6,⍳5 6 1 2 3 4 5 ((+/d∘.='scone')<+/d∘.='penny') 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 (+/d∘.='scone')⌊+/d∘.='penny' 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 ((+/d∘.='scone')<+/d∘.='penny')×(+/d∘.='scone')⌊+/d∘.='penny' 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1+((+/d∘.='scone')<+/d∘.='penny')×(+/d∘.='scone')⌊+/d∘.='penny' 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 (6,⍳5)[1+((+/d∘.='scone')<+/d∘.='penny')×(+/d∘.='scone')⌊+/d∘.='penny'] 6 6 6 6 6 6 6 6 6 6 6 6 6 1 6 6 6 6 6 6 6 6 6 6 6 6 In any case, replacing the six and iota mapping with the following may make the function generic so: (1↓⍴l),⍳1↓⍴l 5 1 2 3 4 5 With all five discriminations combined, equivalent to set intersection, another guess may be made to learn more or find the correct word; I've found the dictionary usually reduced to one after solving: l←(~∨/(l∊(~'crash'∊'isles')/'crash'))⌿l l←(((~'crash'='isles')/l)∧.≠(~'crash'='isles')/'crash')⌿l l←((('crash'='isles')/l)∧.=('crash'='isles')/'isles')⌿l l←((+/[2]l∘.=d)∧.≥(+/d∘.='isles')⌊+/d∘.='crash')⌿l l←((+/[2]l∘.=d)∧.≤(6,⍳5)[1+((+/d∘.='isles')<+/d∘.='crash')×(+/d∘.='isles')⌊+/d∘.='crash'])⌿l l evils isles jests I wanted to demonstrate the function called upon a dictionary with all five-letter combinations, but got a ``WS FULL'' error, meaning the workspace exhausted itself; regardless, this code should do it: wordle ⎕AV[(⎕AV⍳'a')+⍉26 26 26 26 26⊤⍳26⋆5] ⍝ A much smaller test shows this performs quite badly. Follows now are traces of that first form of this function; unfortunately, I see no good way to omit output of the working dictionary l by substituting it for its dimensions, without altering the line, and so chose output which trims the working dictionary particularly well during the first iteration: dictionary←⎕FIO[26] 'five-letter-words' dictionary←dictionary⍴⍨⌽5,⌊5÷⍨⍴dictionary ⍴dictionary 14043 5 ∧/,dictionary∊d 1 wordle dictionary Word: cammy blems wordle[2] blems wordle[2] →⍬ wordle[2] 2 wordle[2] acoma adami adamo adamu ajami amama arama arima aroma atami cammy carmo chama champ chima chimp chump comma cramp crimp crump cuomo dogma dohmh dotma drama dummy duomi duomo fdump firma firmr fiumi foamy forma froma fromc fromm frump gamma gaumr gaumy gizmo gormy grama gramm gramp grimm grimy grump gummo gummy haimi hammy hazmi himmy hmmmm hommy horma houma irama itami izumi jammu jammy jimmu jimmy jorma jumma kamma kammu kanmx karma kidmi kurmi kvdmr maimi mamma mammy marmi marmu miami mihmj momma mommy mummy muuma namma naomi ngoma niomm njama norma oimmm oiqmm omnmi omomk onoma onuma onyma orama oromo padma parma parmi piqmm pommy prima primm primo promo ptkmx pygmy qiamp qimmm qimmn qirmm qiumm quima raimi raymo razmi rccmv rcnmv rimmm riomm rommy roomy rummy tammy thimi thump thymi timmy tmhmm tommi tommo tommy tramp tromp trump tummy ukpmc umami whamo wormy wramc yummy wordle[2] →2 rimmm wordle[2] rimmm wordle[2] →⍬ wordle[2] 3 wordle[2] cammy comma dummy gamma gummo gummy hammy hommy jammu jammy jumma kamma kammu namma pommy tammy tommo tommy tummy yummy wordle[2] →2 cammy wordle[2] cammy wordle[2] →3 Guesses: 3 wordle dictionary Word: korea pared wordle[2] pared wordle[2] →⍬ wordle[2] 2 wordle[2] abres acres agree aires amref arres aurea ayres corea korea werea wordle[2] →2 amref wordle[2] amref wordle[2] →⍬ wordle[2] 3 wordle[2] corea korea werea wordle[2] →2 korea wordle[2] korea wordle[2] →3 Guesses: 3 wordle dictionary Word: hurls hutch wordle[2] hutch wordle[2] →⍬ wordle[2] 2 wordle[2] huana huang huape huard hubby hubei hubel huber hubie hueso hueys huffs huffy huile hujra huley hulks hulls human humez humid humor humps humpy humus hunan hundi hungy hunks hunky hunny huqiu hurdy hurls hurly huron hurri hurry hurva husks husky hussy wordle[2] →2 huffy wordle[2] huffy wordle[2] →⍬ wordle[2] 3 wordle[2] huana huang huape huard hubei hubel huber hubie hueso huile hujra hulks hulls human humez humid humor humps humus hunan hundi hunks huqiu hurls huron hurri hurva husks wordle[2] →2 huqiu wordle[2] huqiu wordle[2] →⍬ wordle[2] 4 wordle[2] huana huang huape huard hubel huber hueso hujra hulks hulls human humez humor humps hunan hunks hurls huron hurva husks wordle[2] →2 humor wordle[2] humor wordle[2] →⍬ wordle[2] 5 wordle[2] huard hujra hurls hurva wordle[2] →2 hujra wordle[2] hujra wordle[2] →⍬ wordle[2] 6 wordle[2] hurls wordle[2] →2 hurls wordle[2] hurls wordle[2] →3 Guesses: 6 .