# Letter Boxed validator

The New York Times has a daily online game called Letter Boxed (the link is behind a paywall; the game is also described here), presented on a square as follows:

You are given 4 groups of 3 letters (each group corresponds to one side on the picture); no letter appears twice. The aim of the game is to find words made of those 12 letters (and those letters only) such that:

• Each word is at least 3 letters long;
• Consecutive letters cannot be from the same side;
• The last letter of a word becomes the first letter of the next word;
• All letters are used at least once (letters can be reused).

In this challenge, you are given the letters and a list of words. The goal is to check whether the list of words is a valid Letter Boxed solution.

## Input

Input consists of (1) 4 groups of 3 letters and (2) a list of words. It can be in any suitable format.

## Output

A truthy value if the list of words is a valid solution to the Letter Boxed challenge for those 4×3 letters, and a falsey value otherwise.

## Test cases

Groups of letters={{I,C,O}, {M,R,E}, {G,N,S}, {A,P,L}}.

### Truthy values

• PILGRIMAGE, ENCLOSE
• CROPS, SAIL, LEAN, NOPE, ENIGMA

### Falsey values

• PILGRIMAGE, ECONOMIES (can't have CO since they are on the same side)
• CROPS, SAIL, LEAN, NOPE (G and M have not been used)
• PILGRIMAGE, ENCLOSURE (U is not one of the 12 letters)
• ENCLOSE, PILGRIMAGE (last letter of 1st word is not first letter of 2nd word)
• SCAMS, SO, ORGANISE, ELOPE (all words must be at least 3 letters long).

Note that in this challenge, we do not care whether the words are valid (part of a dictionary).

## Scoring:

This , lowest score in bytes wins!

• @TFeld no letter appears twice – feersum Apr 15 at 11:06
• A truthy value if the list of words is a valid solution to the Letter Boxed challenge for those 4×3 letters, and a falsey value otherwise. For Python (and most other languages, I expect), both [] and 0 are falsey. Can we output either, or must our output be consistent? – Artemis Fowl Apr 15 at 23:35
• @ArtemisFowl Either is fine. – Robin Ryder Apr 16 at 6:46
• I thought so, but my question was: can we mix them? – Artemis Fowl Apr 16 at 15:03
• @ArtemisFowl Yes, you can mix them. – Robin Ryder Apr 16 at 15:05

# JavaScript (ES6),  130  126 bytes

Takes input as (letters)(words). Returns $$\0\$$ or $$\1\$$.

L=>W=>L.every(a=>a.every(x=>(W+'').match(x,a.map(y=>s+='|'+x+y))),p=s=1)&W.every(w=>w[2]&&p|w[0]==p&!w.match(s,p=w.slice(-1)))


Try it online!

### Step 1

We first iterate over $$\L\$$ to build a pipe-separated string $$\s\$$ consisting of all invalid pairs of letters. While doing so, we also make sure that each letter appears at least once in some word.

L.every(a =>              // for each group of letter a[] in L[]:
a.every(x =>            //   for each letter x in a[]:
(W + '')              //     coerce W[] to a string
.match(               //     and test whether ...
x,                  //       ... x can be found in it
a.map(y =>          //       for each letter y in a[]:
s += '|' + x + y  //         append '|' + x + y to s
)                   //       end of map()
)                     //     end of match()
),                      //   end of inner every()
p = s = 1               //   start with p = s = 1
)                         // end of outer every()


### Step 2

We now iterate over $$\W\$$ to test each word.

W.every(w =>              // for each word w in W[]:
w[2] &&                 //   is this word at least 3 characters long?
p |                     //   is it the first word? (p = 1)
w[0] == p &             //   or does it start with the last letter of the previous word?
!w.match(               //   and finally make sure that ...
s,                    //     ... it doesn't contain any invalid pair of letters
p = w.slice(-1)       //     and update p to the last letter of w
)                       //   end of match()
)                         // end of every()


# Python 2, 171 bytes

lambda l,w:(set(sum(l,[]))==set(''.join(w)))*all(a[-1]==b[0]for a,b in zip(w,w[1:]))*all((a in g)+(b in g)<2for x in w for a,b in zip(x,x[1:])for g in l)*min(map(len,w))>2


Try it online!

# 05AB1E, 42 bytes

εg2›}P¹εεUIεXå}ƶO}üÊP}P¹ü‚εнsθQ}P¹Jê²JêQP


Try it online!

• It's not much, but a byte can be saved by removing all P after the maps, and use )˜P at the end. 41 bytes Nice approach with ê however! Saved 2 bytes in my 05AB1E answer. – Kevin Cruijssen Apr 15 at 13:52

# Jelly, 34 bytes

Ṫ=¥/ƝḢ€Ạȧ⁸Fe€ⱮZḄ;IẠƊȧF}fƑF{
ẈṂ>2ȧç


A dyadic Link accepting the words on the left and the letter groups on the right which yields 1 if valid and 0 if not.

Try it online! Or see the test-suite.

import Data.List
l&w=all((>2).length)w&&c w&&all(l!)w&&(h l)%(h w)
h=concat
l%w=null[x|x<-l,xnotElemw]
l!(a:b:c)=a#l?(b#l)&&l!(b:c)
l!_=1>0
Just a?Just b=a/=b
_?_=1<0
c#l=findIndex(elem c)l
c _=1>0


Try it online!

Not the best score. Some Haskell guru will probably be able to get this under 100 bytes.

### Usage

["ICO","MRE","GNS","APL"]&["CROPS", "SAIL", "LEAN", "NOPE", "ENIGMA"]


### Explanation

import Data.List
l&w = all((>2).length)w &&      -- Every word has length > 2
c w &&                    -- Every word ends with the same letter as the next one starts with
all(l!)w &&               -- For every word: Consecutive letters are on different sides (and must exist on a side)
(h l)%(h w)               -- All letters are used

h=concat                        -- Just a shorthand

l%w=null[x|x<-l,xnotElemw]    -- The letters of l, with all letters of w removed, is empty

l!(a:b:c)=a#l?(b#l)&&l!(b:c)    -- Sides of the first two letters are different, recurse from second letter
l!_=1>0                         -- Until fewer than 2 letters remain

Just a?Just b=a/=b              -- Both sides must be different
_?_=1<0                         -- And must exist

c#l=findIndex(elem c)l          -- Find the side of letter c

c(a:b:t)=last a==head b&&c(b:t) -- Last letter of the first word must be same as first letter of second word, recurse starting from second word
c _=1>0                         -- Until there are fewer than 2 words


# Jelly, 30 bytes

FQṢ=Ṣ},i@€€’:3Iʋ,Ẉ>2ɗ,Ṫ=Ḣ}ɗƝ{Ȧ


Try it online!

A dyadic link that takes the list of words as left argument and the flattened list of letters in the box as the right argument. It returns 1 for true and 0 for false.

### Explanation

F                               | Flatten the word list
Q                              | Unique
Ṣ                             | Sort
=                            | Is equal to
Ṣ}                          |   The sorted letterbox letters
,        ʋ                | Pair this with the following:
i@€€                     |   The index of each letter of each word in the letterbox
’                    |   Decrease by 1
:3                  |   Integer divide by 3
I                 |   Differences between consecutive ones (will be zero if any two consecutive letters in a word from same side of box)
,   ɗ           | Pair everything so far with the following:
Ẉ>2            |   Whether length of each input word is greater than 2
,    ɗƝ{   | Pair everything so far with the following, applied to each neighbouring pair of the input word list
Ṫ         |   Last character of first word
=Ḣ}      |   Equals first character of second
Ȧ  | Flatten all of the above and check there are no false values


# 05AB1E, 373533 32 bytes

εSk3÷üÊ}¹ü)εнsθQ}²{¹SêQ¹€g3@)˜P


-2 bytes by taking inspiration of the ê approach @Emigna used in his 05AB1E answer.

Takes the string of words as first input, and the flattened list of twelve letters as second input.

Explanation:

ε      }    # Map over the strings y of the (implicit) input-list of words:
S          #  Convert word y to a list of characters
k         #  Get the index of each character in the (implicit) input-list of letters
3÷       #  Integer-divide each index by 3
üÊ     #  Check for each pair of integers that they are NOT equal
¹ü)         # Get all pairs of strings from the input-list of words
ε     }  # Map over the pair of words:
       #  Push both to the stack
н      #  Leave only the first character of the second word
sθ    #  And the last character of the first word
Q   #  And check if they are equal
²{          # Push the input-list of letters, and sort them
¹S        # Push the input-list of words, converted to characters and flattened
ê       # And then uniquified and sorted as well
Q      # And check if both lists of characters are the same
¹€g         # Get the length of each string in the input-list of words
3@       # Check for each length if it's >= 3
)˜          # Then wrap everything on the stack into a list, and deep flatten it
P         # And check if everything is truthy by taking the product
# (which is output implicitly as result)


A different Haskell variation, exactly the same size as @Paul Mutser's :)

import Data.List
f x=filter(\a->length a>1)$concatMap subsequences x g=nub.concat.f p l(x:y)=foldl(\(m,n)c->(c,n&&length c>2&&(not$any(isInfixOfc)(f l))&&last m==head c))(x,True)y
z l w=null(g l\\g w)&&null(g w\\g l)&&(snd$p l w)  Try it online! ### Ungolfed -- generate all invalid substrings f :: [String] -> [String] f xs = filter (\x -> length x > 1)$ concatMap subsequences xs

-- utility function to flatten and remove duplicates
g :: [String] -> String
g  = nub $concat$ f

-- verify that all conditions are satisfied along the list
p :: [String] -> [String] -> (String, Bool)
p l (x:xs) = foldl (\(m,n) c -> (c , n && length c > 2 && (not $any (isInfixOf c)(f l)) && last m == head c)) (x, True) xs -- put all the pieces together and consume input z :: [String] -> [String] -> Bool z l w = null (g l \\ g w) && null (g w \\ g l) && (snd$ p l w)


# Java (JDK), 190 bytes

g->w->{var v=0<1;int x=0,l,i=0,j,p,z,y=w[0][0];for(;i<w.length;i++)for(l=w[i].length,v&=y==w[i][0]&l>2,j=0,p=-9;j<l;v&=z>=0&z/3!=p/3,x|=1<<1+(p=z))z=g.indexOf(y=w[i][j++]);return v&x==8190;}


Try it online!

## Explanations

g->w->{     // Lambda accepting letter groups as a string and a list of words, in the form of an array of char arrays.
var v=0<1;     // Validity variable
int x=0,       // The letter coverage (rule 4)
l,         // The length of w[i]
i=0,       // The w iterator
j,         // The w[i] iterator
p,         // The previous group
z,         // The current group
y=w[0][0]; // The previous character
for(;i<w.length;i++) // For each word...
for(
l=w[i].length,     // make a shortcut for the length
v&=y==w[i][0]&l>2, // check if the last character of the previous word is the same as the first of the current.
// Also, check if the length is at least 3
j=0,               // Reset the iteration
p=-9               // Set p to an impossible value.
;
j<l                //
;
v&=z>=0&z/3!=p/3,  // Check that each letter of the word is in the letter pool,
//  and that the current letter group isn't the same as the previous one.
x|=1<<1+(p=z)      // After the checks, assign z to p,
//  and mark the letter of the pool as used.
)
z=g.indexOf(y=w[i][j++]); // Assign the current letter to y so that it contains the last at the end of the loop.
//  and fetch the position of the letter in the pool.
return v&x==8190; // Return true if all matched
//  and if the rule 4 is enforced.
}


# Charcoal, 63 bytes

⌊⁺⁺⁺⭆η›Ｌι²⭆⪫ηω№⪫θωι⭆⪫θω№⪫ηωι⭆η⭆ι⎇μ¬⁼Φθ№νλΦθ№ν§ι⊖μ∨¬κ⁼§ι⁰§§η⊖κ±¹


Try it online! Link is to verbose version of code. Explanation:

⌊⁺⁺⁺


Concatenate the below expressions and output 0 if any of them includes a 0 otherwise 1.

⭆η›Ｌι²


For each word in the solution output whether its length is at least 3.

⭆⪫ηω№⪫θωι


For each letter in the solution output whether it appears in the puzzle.

⭆⪫θω№⪫ηωι


For each letter in the puzzle output whether it appears in the solution.

⭆η⭆ι⎇μ¬⁼Φθ№νλΦθ№ν§ι⊖μ∨¬κ⁼§ι⁰§§η⊖κ±¹


For each letter in the solution check that the previous letter is not in the same group, unless it is the first letter of a word, in which case check that it is equal to the last letter of the previous word, unless it is the first letter of the solution, in which case just ignore it.

# Ruby, 126 bytes

->l,w{(/(_|^)..(_|$)/!~s=w*?_)&&!!s.chars.uniq[12]&&/__|^_|_$|(_.*)\1/!~s.gsub(/(.)_\1/,'\1').chars.map{|x|l.grep(/#{x}/)}*?_}


Try it online!

• Nice, when I first saw the challenge I tried to do something similar, but gave up with a score somewhere in 140-ies. BTW, save a byte by dropping parentheses after grep. – Kirill L. Apr 17 at 18:04
• This doesn't work when the last word is 1 or 2 letters long, e.g. puts f[l,['PILGRIMAGE','ENCLOSE','EG']] returns true instead of false. – Robin Ryder May 4 at 11:58
• You are right, fixed. – G B May 4 at 21:15

# Python 2, 168 156 bytes

lambda l,w,J=''.join:(set(J(w))==set(J(l)))*all((v<1or u[-1]==v[0])*u[2:]*(2>(x in p)+(y in p))for u,v in zip(w,w[1:]+[0])for x,y in zip(u,u[1:])for p in l)


Try it online!

Returns 1 for truthy, 0` for falsey.